あまブログ

ドキドキ......ドキドキ2択クイ〜〜〜〜〜〜〜ズ!!

【Node.js】フラッシュ暗算ゲームを作成する

CLIフラッシュ暗算ができるnpmパッケージを作成しました。

www.npmjs.com

npmパッケージの公開方法は以下を参照ください。

ama-tech.hatenablog.com

この記事では、JavaScriptフラッシュ暗算ゲームを作成する方法を解説します。

1. 実行環境

2. フラッシュ暗算ゲームの仕様

  1. 桁数、表示回数、表示間隔を選択する
  2. カウントダウンの後に問題を出題する
  3. 回答を入力する
  4. 正解を表示する

3. ソースコード

index.js

#! /usr/bin/env node

const { prompt } = require("enquirer");

async function main() {
  const terms = await flashNumbers();
  await checkAnswer(terms);
}

async function flashNumbers() {
  const options = await getOptions();
  const terms = [];

  await countDown();

  for (let i = 0; i < options.displayCount; i++) {
    const prevNum = terms.slice(-1)[0];
    let num = getNumber(options);
    while (num === prevNum) {
      num = getNumber(options);
    }
    terms.push(num);

    await displayNumber(num);

    await new Promise((resolve) =>
      setTimeout(resolve, options.displayInterval * 1000)
    );
  }

  process.stdout.clearLine(0);
  process.stdout.cursorTo(0);

  return terms;
}

async function getOptions() {
  return await prompt([
    {
      type: "input",
      name: "digits",
      message: "Number of Digits",
      validate: isPositiveInteger,
      initial: 1,
    },
    {
      type: "input",
      name: "displayCount",
      message: "Display Count",
      validate: isPositiveInteger,
      initial: 10,
    },
    {
      type: "input",
      name: "displayInterval",
      message: "Display Interval(seconds)",
      validate: isPositiveNumber,
      initial: 1,
    },
  ]);
}

function isPositiveInteger(input) {
  const num = Number(input);
  return Number.isInteger(num) && num > 0
    ? true
    : "Please input a Positive Integer";
}

function isPositiveNumber(input) {
  const num = Number(input);
  return num > 0 ? true : "Please input a Positive Number";
}

async function countDown() {
  const texts = [
    "\x1b[31mReady\x1b[0m",
    "\x1b[33mSet\x1b[0m",
    "\x1b[32mGo!\x1b[0m",
  ];
  for (const text of texts) {
    process.stdout.clearLine(0);
    process.stdout.cursorTo(0);
    process.stdout.write(text);
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
}

function getNumber(options) {
  return Math.floor(
    Math.random() * Math.pow(10, options.digits) * 0.9 +
      Math.pow(10, options.digits - 1)
  );
}

async function displayNumber(num) {
  process.stdout.clearLine(0);
  process.stdout.cursorTo(0);
  process.stdout.write(String(num));
}

async function checkAnswer(terms) {
  const correctAnswer = terms.reduce((sum, term) => sum + term);
  const answer = await inputAnswer();
  const result =
    Number(answer.answer) === correctAnswer
      ? "\x1b[32mCorrect!\x1b[0m"
      : "\x1b[31mWrong...\x1b[0m";
  const yourAnswer =
    Number(answer.answer) === correctAnswer
      ? `\x1b[32m${answer.answer}\x1b[0m`
      : `\x1b[31m${answer.answer}\x1b[0m`;

  console.log(result);
  console.log(`your answer: ${yourAnswer}`);
  console.log(`correct answer: \x1b[32m${correctAnswer}\x1b[0m`);
  console.log(`${terms.join(" + ")} = ${correctAnswer}`);
}

async function inputAnswer() {
  const question = {
    type: "input",
    name: "answer",
    message: "Please enter your answer",
    validate: isPositiveInteger,
    initial: 10,
  };

  return await prompt(question);
}

main();

4. ポイント解説

4-1. enquirer

参考:enquirer

promptメソッドの引数にquestionオブジェクトを渡す

  • questionオブジェクトのプロパティ:Prompt Options
    • type
      • promtのタイプを指定(ここではInput Promptを使用)
    • name
      • promptメソッドの戻り値のオブジェクト({digits: '1', displayCount: '10', displayInterval: '1'})のプロパティのキーを指定
    • message
      • ターミナルに表示されるメッセージを指定
    • validate
      • ユーザーの入力値のバリデーションを行うメソッドを指定
      • メソッドの戻り値は真偽値と文字列。文字列をエラーメッセージに使う。
    • initial
      • promptメソッドの戻り値のオブジェクト({digits: '1', displayCount: '10', displayInterval: '1'})のプロパティの値の初期値を指定

4-2. 正数、正の整数のバリデーション

  • 正数(Positive Number):0より大きい数(少数を含む)
  • 整数(Integer):自然数, 0, 負数の総称(少数を含まない)

4-2-1. 正数のバリデーション

function isPositiveNumber(input) {
  const num = Number(input);
  return num > 0 ? true : "Please input a Positive Number";
}
  • Number(input)で文字列を数値に変換
  • num > 0で正数かどうかを判定

4-2-2. 正の整数のバリデーション

 function isPositiveInteger(input) {
  const num = Number(input);
  return Number.isInteger(num) && num > 0
    ? true
    : "Please input a Positive Integer";
}

4-3. ターミナル出力の上書き表示

async function displayNumber(num) {
  process.stdout.clearLine(0);
  process.stdout.cursorTo(0);
  process.stdout.write(String(num));
}

4-4. タイマー処理

// 1秒おきに表示
const timer = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function main() {
  for (let i = 1; i <= 10; i++) {
    await timer(1000);
    console.log(i);
  }
}
main();

4-5. 乱数生成

function getNumber(options) {
  return Math.floor(
    Math.random() * Math.pow(10, options.digits) * 0.9 +
      Math.pow(10, options.digits - 1)
  );
}

例:4桁のランダムな数値を生成(options.digitsが4の場合)

Math.random() * 10000 * 0.9 + 1000 Math.floor()
最小値 0 0 0 1000 1000
最大値 0.99999999 9999.9999 8999.9991 9999.9991 9999
  • 2列目:* 10000* Math.pow(10, options.digits)
  • 3列目:* 0.9により最大値の一桁目が1になるが最後にMath.floor()で切り捨てるので問題なし
  • 4列目:+ 1000+ Math.pow(10, options.digits - 1)

4-6. 連続して同じ数字が表示されないようにする

async function flashNumbers() {
  const terms = [];
  // 省略
  for (let i = 0; i < options.displayCount; i++) {
    const prevNum = terms.slice(-1)[0];
    let num = getNumber(options);
    while (num === prevNum) {
      num = getNumber(options);
    }
    terms.push(num);
    // 省略
  }
  // 省略
}
  • terms.slice(-1)[0]で配列の末尾の値を取得
    • 初回は配列が空なのでprevNumundefined
  • while (num === prevNum)numprevNumと異なる値になるまでgetNumber()を繰り返す

【参考】