chinchiroのロジック解説(5)

Enquirerによる行動選択

今回はEnquirerを使った行動選択の部分について解説します。

Enquirer
まず、Enquirerのインストールです。 npm install enquirer 次に、トップレベルでEnquirerのインポートを宣言します。 import Enquirer from 'enquirer' これで準備OK!Enquirerを使ったメソッドを作っていきます。

async decide_action () {
  const actions = ['throw dice', 'ikasama', 'pass', 'surrender']
  const question = {
    type: 'select',
    name: 'action',
    message: 'What are you gonna do?',
    choices: actions
  }
  const answer = await Enquirer.prompt(question)
  return answer.action
}

Enquirer.prompt()でEnquirerのメソッドを作ります。内容はquestionの部分になります。

questionの中の各項目を解説すると、

  • type:selectは選択肢から選ぶ形式です。他には直接文字を入力するinputなどがあります。
  • name: 後で値を取り出す時に使います。
  • message:選択肢を選ぶときに表示される質問のメッセージです。
  • choises:選択肢の部分です。最初にactionsに選択肢を定義していますが、直接choisesに選択肢の配列を書いてもOKです。

最後にreturn answer.actionで選んだ選択肢を文字列として返しています。この.actionは先ほどname:で定義したものです。

chinchiroではこの文字列を参照して、条件分岐でそれぞれのアクションを行うようにしています。

chinchiroのロジック解説(4)

サイコロを振るchin chiro rinの演出

今日は、コマンドラインに文字の演出が入る部分の解説をします。

chinchiro
今回はansi escapeの処理を使うので、npmのansi-escape-sequencesをインストールしておきます。

npm install ansi-escape-sequences

そうしたら、ansi escapeを使うファイルのトップレベルで、インポートを宣言します。

import ansi from 'ansi-escape-sequences'

これでひとまず準備はOK。 コード部分の解説に移ります。

  async sleep (time) {
    const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
    await _sleep(time)
  }

  async chinchirorin () {
    const texts = ['chin', ' chiro', ' rin']
    for (const text of texts) {
      process.stdout.write(text)
      await this.sleep(500)
    }
    process.stdout.write(ansi.erase.inLine(1) + ansi.cursor.nextLine(1))
  }

まず、上のsleep()メソッドについてです。演出のメソッドであるchinchirorinを作るにあたり、進行を止めておくメソッドが必要です。そうでないと全ての処理が一瞬で終わってしまい、何が起きたかわからなくなってしまいます。 sleepメソッドによって、処理を止めて段階的にゲームの進行が表示されるように出来ます。

メソッドchinchirorintextsで定義した配列の文字列を、0.5秒ごとに順番に表示するメソッドです。

特に注目してもらいたいのが、最後のprocess.stdout.write(ansi.erase.inLine(1) + ansi.cursor.nextLine(1)の部分です。 ここのansi.erase.inLine(1)で表示したchin chiro rinの文字列を消して、ansi.cursor.nextLine(1)でカーソルを元の位置に戻しています。 なぜ文字を消すかといえば、進行に関係ない演出の部分なので、消してログを見やすくするためです。 ログを見ればゲームの進行がわかるような情報を残すようにしています。

chinchiroのロジック解説(3)

役の判定

今日は役の判定の解説をします。

//diceEyesはサイコロの目3つの配列
  judge_role (diceEyes) {
    if (this.zorome(diceEyes) && diceEyes[0] === 1) {
      console.log('pinzoro')
      return 13
    } else if (this.zorome(diceEyes)) {
      console.log(diceEyes[0] + 'no zorome')
      return 6 + diceEyes[0]
    } else if (this.hifumi(diceEyes)) {
      console.log('hifumi')
      return -1
    } else if (this.jigoro(diceEyes)) {
      console.log('jigoro')
      return 7
    } else if (this.nome(diceEyes)) {
      const diceNumber = this.nannome(diceEyes)
      console.log(diceNumber + 'nome')
      return diceNumber
    } else {
      console.log('menasi')
      return 0
    }
  }

サイコロの目のが3つ入った配列diceEyesを条件判定のメソッドにかけ、画面に役を表示しreturnで数値を返します。

returnで役の名前でなく数値を返す理由は、役の強さの勝ち負けの判定に使うためです。

ヒフミの時に負の数を使ったのは、マイナスの役であることがわかるようにと思ってあえてそうしました。

役の条件判定メソッドは次の通りです。

zorome = (diceEyes) => {
    return diceEyes[0] === diceEyes[1] && diceEyes[1] === diceEyes[2]
  }

  hifumi = (diceEyes) => {
    return diceEyes.includes(1) && diceEyes.includes(2) && diceEyes.includes(3)
  }

  jigoro = (diceEyes) => {
    return diceEyes.includes(4) && diceEyes.includes(5) && diceEyes.includes(6)
  }

  nome = (diceEyes) => {
    return (diceEyes[0] === diceEyes[1]) || (diceEyes[0] === diceEyes[2]) || (diceEyes[1] === diceEyes[2])
  }

  nannome = (diceEyes) => {
    if (diceEyes[0] === diceEyes[1]) {
      return diceEyes[2]
    } else if (diceEyes[1] === diceEyes[2]) {
      return diceEyes[0]
    } else if (diceEyes[0] === diceEyes[2]) {
      return diceEyes[1]
    }
  }

nannomeは条件判定ではなく、通常の目が出た時の役の強さの判定に使っています。

chinchiroのロジック解説(2)

サイコロの表示部分

最初は[4,5,6]のように数字で表示していたのですが、やはり味気ないのでサイコロの目の表示をするようにしました。 ロジックを説明します。コードのオブジェクトは、それぞれ1〜6まであるのですが、見やすいように今回は5だけにしています。

//diceEyesはダイスの目が3つ入った配列
 printDice (diceEyes) {
    const diceAA1 = {5: '  | ●   ● |' }
    const diceAA2 = {5: '  |   ●   |' }
    const diceAA3 = {5: '  | ●   ● |' }
    console.log('  +-------+  +-------+  +-------+')
    process.stdout.write(`${diceAA1[diceEyes[0]]}`)
    process.stdout.write(`${diceAA1[diceEyes[1]]}`)
    console.log(`${diceAA1[diceEyes[2]]}`)
    process.stdout.write(`${diceAA2[diceEyes[0]]}`)
    process.stdout.write(`${diceAA2[diceEyes[1]]}`)
    console.log(`${diceAA2[diceEyes[2]]}`)
    process.stdout.write(`${diceAA3[diceEyes[0]]}`)
    process.stdout.write(`${diceAA3[diceEyes[1]]}`)
    console.log(`${diceAA3[diceEyes[2]]}`)
    console.log('  +-------+  +-------+  +-------+')
  }

サイコロ表示
このように表示したいので、3つのサイコロの上の線をconsole.logで表示。(+---...の部分) 次にdiceAA1(サイコロの目を上中下3つに分けた上の部分だけ)をprocess.stdout.writeで左のサイコロから順番に繋げて表示、右のサイコロまできたらconsole.logで表示して改行するようにしました。これをdiceAA2(サイコロの目の真ん中の段)、diceAA3(下の段)と繰り返し、最後にサイコロの下の線を表示することでサイコロを3つ横に表示することができました。

chinchiroのロジック解説(1)

サイコロ部分のコード解説

今回はchinchiroのサイコロ部分のコード解説をします。

通常のサイコロ

class NormalDice {
  roll () {
    return Math.floor(Math.random() * 6) + 1
  }
}

Math.random()は0以上1未満のランダムな数を返すので、これを6倍することで0〜6未満のランダムな数を得られます。 このままだと0から5.999...ですが、Math.floor()で小数点以下を切り捨てると0〜5を返すようになります。 最後に最小値の1を加えることで、1〜6をランダムに返すダイスの機能を実現しました。

ジゴロサイ

class JigoroDice {
  constructor () {
    this._lisk = 0
  }

  get lisk () {
    return this._lisk
  }

  set lisk (probability) {
    this._lisk = probability
  }

  roll () {
    return Math.floor(Math.random() * 3) + 4
  }
}

4、5、6しか出ないイカサマ用のサイコロです。基本的には通常のと同じ考え方ですが、範囲が3(4〜6なので)、最小値が4になっています。 また、コンストラクタにイカサマがバレる確率の設定用の、リスクの数値を持たせました。 さらに、外部から確率を変動できるようゲッターとセッターでアクセスできるようにしました。

ピンゾロサイ

class PinzoroDice {
  roll () {
    return 1
  }
}

1しか出ないサイコロなので、単純に1を返すようになっています。

出たサイコロの目を配列にする

  async throw_dice () {
    const diceEyes = []
    for (let i = 1; i < 4; i++) {
      await diceEyes.push(this.dice.roll())
    }
    this.printDice(diceEyes)
    return diceEyes
  }

Userクラスの方で、thwow_diceメソッドを用意します。 今回役を作るのに必要な3回分のサイコロの目を、配列として入れることにしました。 このサイコロの目の表示はprintDice()メソッドで行います。 次回は、この表示用のprintDiceメソッドを解説していきます。

npm「chinchiro」公開

チンチロ

フィヨルドブートキャンプの課題でnpmの公開をしました。

作ったのは漫画カイジで有名な、ダイスゲームの「チンチロ」です。

https://www.npmjs.com/package/chinchiro

chinchiro
chinchiro

作成にあたり、「賭博破戒録カイジ福本伸行)・講談社」の地下チンチロのルールを基にしました。原作と細かいところは違いますが、大体同じようなルールになったのではないかと思います。

ルール

  • サイコロを3つ振って役を作り、役の強い方の勝ち。
  • 親は交代制で嫌ならパスできる。(原作では親は2回までですが、chinchiroでは1回交代にしました)
  • 誰かの持ち金がなくなった回で終了。その時点で手持ちの1ばん多い人の勝ち。

補足

  • 原作と違い、ションベンは無し。
  • イカサマができるが、バレるとペナルティあり。
  • 子でもパスできる。

ぜひ遊んでみてください! 次回からchinchiroのコードの解説をします。

Sinatraメモアプリ作り:JSONファイルでのデータ操作の仕方

Sinatraを使ったメモアプリを作るにあたり、作ったメモを保存したり取り出したりする必要があります。 今回はSQLなどのデータベースを使うのではなく、ファイルに読み書きをしてデータ操作をする方法を書いていきます。

準備

まず、アプリの実行ファイル(memo.rbなど)があるディレクトリに、publicというディレクトリを用意します。 次に、そのpublicディレクトリの中にdata.jsonというファイルを作成して入れておきます。 このdata.jsonファイルにメモの入力内容を読み書きしていきます。

保存するデータの構造を考える

Rubyで読み書きをするプログラムを書くまえに、保存するデータの構造を考えてみます。 今回は簡単なメモアプリなので、記録するデータはメモのタイトルと内容の2つになります。

そのため、{'title':'メモのタイトル', 'body':'メモの内容'}というハッシュ形式を取ります。 配列ではなくハッシュを使うのは、必要な情報を指定して取り出すのに適しているからです。

さらに、このままだと特定のメモの表示や削除などがしにくいので、メモにIDを持たせます。 よって、{'ID':{'title':'メモのタイトル', 'body':'メモの内容'}}の二次元ハッシュの形式にします。 複数のメモがある場合、{'ID1':{'title':'メモ1のタイトル', 'body':'メモ1の内容'}, 'ID2':{'title':'メモ2のタイトル', 'body':'メモ2の内容'},...}のようになります。こうすることで、メモのIDを指定してデータを取り出すことができるようになります。 IDには、オブジェクトID(object_idで取得)を使うといいと思います。

データの書き込みをする

{'ID':{'title':'メモのタイトル', 'body':'メモの内容'}}の形式にハッシュを加工できたら、それをファイルに書き込んでいきます。

@data = {'ID':{'title':'メモのタイトル', 'body':'メモの内容'}}の場合、

File.open('public/data.json', 'w') { |file| JSON.dump(@data, file) }

これで書き込みが出来ます。

データの読み込みをする

保存したデータを読み込む場合は、

File.open('public/data.json') { |file| @data = JSON.load(file) }

これで@dataに読み込んだデータが入ります。

データを追加する

データの追加の場合、まずはファイルを呼び出します。

File.open('public/data.json') { |file| @data = JSON.load(file) }

@data = {'ID1':{'title':'メモ1', 'body':'メモ1内容'}を取得しました。

このハッシュに、新しい要素を追加します。

@data[`ID2`] = {'title':'メモ2', 'body':'メモ2内容'}

結果は次のようになります。 @data = {'ID1':{'title':'メモ1', 'body':'メモ1内容'}, 'ID2':{'title':'メモ2', 'body':'メモ2内容'}}

最後に、要素を追加したハッシュでJSONファイルを書き換えます。

File.open('public/data.json', 'w') { |file| JSON.dump(@data, file) }

これで新しいメモのデータを追加できました。

データを変更する

まずJSONファイルを読み込み、@dataを取得します。(やり方は上にあるため省略) 次にハッシュのキーをIDで指定して、値の上書きをします。

@data[`ID1`] = {'title':'メモ1変更!', 'body':'メモ1内容変更!'}

結果は次のようになります。

{'ID1':{'title':'メモ1変更!', 'body':'メモ1内容変更!'}, 'ID2':{'title':'メモ2', 'body':'メモ2内容'}} JSONファイルに書き込んだら変更終了です。

データを削除する

JSONファイルを読み込みます。

データを削除する場合は、ハッシュのdeleteメソッドを使います。引数にキーとしてIDを指定することで、IDとそれに紐づいたハッシュを削除できます。 @data = {'ID1':{'title':'メモ1', 'body':'メモ1内容'}, 'ID2':{'title':'メモ2', 'body':'メモ2内容'}}の場合、

@data.delete('ID1')

を実行すると、結果は

@data = {'ID2':{'title':'メモ2', 'body':'メモ2内容'}}

となります。 最後にJSONファイルに上書きして削除完了です。

終わりに

以上でJSONファイルの操作の解説は終了です。 簡単なメモアプリならSinatraJSONファイルで実現できるので、ぜひ試してみてください。