TypeScriptで簡単な対話形式のCLI符計算アプリを作ってみる
仕事ですぐに使えるTypeScript を読んだ。最近はインプットばかりで、何かを作ることもできていなかったので、久しぶりに手を動かしてみることにした。
- TypeScriptを書く
- npm packageデビューする
- まずはWebなどではなく、CLIでシンプルに作る
この辺りを目標にした。最近は麻雀にハマっていて(天鳳か雀魂をやっている)、符計算の簡単なCLIがあれば良さそうと思ったので作ることにした。
符計算について
麻雀に明るくない方向けに軽く符計算について説明をする。
麻雀は 4つのメンツ(3個 or 4個のセット)と1つのアタマ(2個のセット) の合わせて 14~18個の牌を揃えるゲームである。その際、役などを数えたりして得点計算をする。「符」とは役がそこまで大きくない場合の得点を判定するための数値で、その計算を「符計算」と呼ぶ。これが結構複雑で、初心者殺し感がある。Wikipediaにも複雑な表が置いてある。
符は
などから算出される。また、一部の役(七対子など)は符が一律で決まっていることがあるが、今回のアプリでは考慮していない。
できたもの
npmにもパッケージ化して置いてあるので、すぐにご利用できます。
CLIフレームワークについて
TS(というかNode.js)でCLIを作る際に使うフレームワークについては何も知らない状態でスタートした。5分くらい調べて、Herokuが作っている oclif が良さそうと思って採用した。 oclifは対話形式でCLIアプリケーションに合った雛形を作ってくれる。
single
と multi
が選べる。 multi
はサブコマンドを作りたい時に使うらしい。今回はサブコマンド要らなそうなので single
を選択した。
🐑🌙 npx oclif single fucalc-ts _-----_ ╭──────────────────────────╮ | | │ Time to build a │ |--(o)--| │ single-command CLI with │ `---------´ │ oclif! Version: 1.16.1 │ ( _´U`_ ) ╰──────────────────────────╯ /___A___\ / | ~ | __'.___.'__ ´ ` |° ´ Y ` ? command bin name the CLI will export fucalc-ts ? description the app for calculating fu in mahjong. ? author odmishien ? Select a package manager npm ? TypeScript Yes ? Use eslint (linter for JavaScript and Typescript) Yes ? Use mocha (testing framework) No ? Add CI service config circleci (continuous integration/delivery service)
色々と質問に答えると、npm install
などはもちろん、READMEなどもいい感じに作ってくれる。カスタマイズ自分でしたい人にはちょっとお節介かもしれないくらいである。実行後は以下のようなディレクトリ構成となる。
🐑🌙 tree -L 1 . ├── README.md ├── bin ├── node_modules ├── package-lock.json ├── package.json ├── src └── tsconfig.json 3 directories, 4 files
src
の中には index.ts
ができていて、実行が可能。
🐑🌙 ./bin/run hello world from ./src/index.js!
インタラクティブなプロンプトを実現する
この記事がとても参考になった。
プロンプトをインタラクティブにするのに inquirer を使う。プロンプトで聞くことのできる質問のタイプも色々用意されていて便利。今回は List
形式しか使わない。
Command
の中の run
というメソッドの中に prompt
を定義する。 prompt
には質問のオブジェクトを Array
にして渡すと、上から順番に聞いてくれる。
import { Command } from "@oclif/command"; import { prompt } from "inquirer"; class sampleCommand extends Command { async run() { const userInput = await prompt([ { type: "list", name: "animal", message: "好きな動物は?", choices: [ { name: '🐢 かめさん', value: 'turtle', }, { name: '🐇 うさぎさん', value: 'rabbit', }, ], }, { type: "list", name: "fruit", message: "好きな果物は?", choices: [ { name: '🍎 りんご', value: 'apple', }, { name: '🍇 ぶどう', value: 'grape', }, { name: '🍊 オレンジ', value: 'orange', }, ], }, ]); console.log(userInput); } } export = sampleCommand;
ユーザーの入力は userInput
に入る。
./bin/run ? 好きな動物は? > 🐢 かめさん 🐇 うさぎさん ? 好きな果物は? > 🍎 りんご 🍇 ぶどう 🍊 オレンジ { animal: "turtle", fruit: "apple", }
符を計算する
ここからはかなり素朴で、ユーザーの入力を元に switch
文で符を計算していく。符は基本点のようなものが20点あるので、初期値は 20
である。
また、最終的に1の位は切り上げを行う。例えば 32
は 40
となる。
let fu = 20; fu += calc.calcMen(userInput.men1); fu += calc.calcMen(userInput.men2); fu += calc.calcMen(userInput.men3); fu += calc.calcMen(userInput.men4); fu += calc.calcAtama(userInput.atama); fu += calc.calcMachi(userInput.machi); fu += calc.calcAgari(userInput.agari); fu += calc.calcMenzen([ userInput.men1, userInput.men2, userInput.men3, userInput.men4, ]); const ceiledFu = calc.calcCeilFu(fu); console.info(`\n🀄️あなたの手は ${ceiledFu} (合計:${fu}) 符です🀄️`);
calc...
はそれぞれの条件(メンツとかアタマとか)に合わせて関数を作っている。型安全に作るなら、独自の型を作ったりして、VSCodeで補完を利かせまくるのがいいのだろうが、面倒でしていない。
例えば、待ち方から符を計算する calcaMachi
は以下のようになっている。
const calcMachi = (machiType: string): number => { switch (machiType) { case "ryanmen": return 0; break; case "shanpon": return 0; break; case "tanki": return 2; break; case "kanchan": return 2; break; case "penchan": return 2; break; default: return -1; break; } };
ハマったこと
prompt
はPromise<any>
型を返すので、awaitを使った方がいいthen
でチェーンしてもいいけど
switch
のなかに||
は書けない(全然TS関係ない)
// これはできない switch(text){ case "hoge" || "huga": ... case "piyo": ... } // ちゃんと case を書く switch (text) { case "hoge": ... case "huga": ... case "piyo": ... }
感想
とても簡単、かつある程度綺麗な見た目の対話形式のCLIアプリがサクッと書けるのは、とても体験が良い。課題としては
- エラーハンドリングをやる
- テストを書く
- 色々フラグを付けられるような機能を考える(役確認とか?)
- サブコマンドも使えるなら実装したい
などが挙げられそう。