カレーのライスをtech忘れ

odmishienのtechメモ

TypeScriptで簡単な対話形式のCLI符計算アプリを作ってみる

仕事ですぐに使えるTypeScript を読んだ。最近はインプットばかりで、何かを作ることもできていなかったので、久しぶりに手を動かしてみることにした。

  • TypeScriptを書く
  • npm packageデビューする
  • まずはWebなどではなく、CLIでシンプルに作る

この辺りを目標にした。最近は麻雀にハマっていて(天鳳か雀魂をやっている)、符計算の簡単なCLIがあれば良さそうと思ったので作ることにした。

符計算について

麻雀に明るくない方向けに軽く符計算について説明をする。

麻雀は 4つのメンツ(3個 or 4個のセット)と1つのアタマ(2個のセット) の合わせて 14~18個の牌を揃えるゲームである。その際、役などを数えたりして得点計算をする。「符」とは役がそこまで大きくない場合の得点を判定するための数値で、その計算を「符計算」と呼ぶ。これが結構複雑で、初心者殺し感がある。Wikipediaにも複雑な表が置いてある。

ja.wikipedia.org

符は

  • メンツの種類
  • アタマの種類
  • ガリの待ち方(両面待ちとか)
  • ガリ方(ツモ or ロン)
  • 鳴いたかどうか

などから算出される。また、一部の役(七対子など)は符が一律で決まっていることがあるが、今回のアプリでは考慮していない。

できたもの

https://user-images.githubusercontent.com/25533384/92455643-eb4d8480-f1fc-11ea-88b4-53b30e581ff2.gif

github.com

npmにもパッケージ化して置いてあるので、すぐにご利用できます。

www.npmjs.com

CLIフレームワークについて

TS(というかNode.js)でCLIを作る際に使うフレームワークについては何も知らない状態でスタートした。5分くらい調べて、Herokuが作っている oclif が良さそうと思って採用した。 oclifは対話形式でCLIアプリケーションに合った雛形を作ってくれる。

singlemulti が選べる。 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!

インタラクティブなプロンプトを実現する

この記事がとても参考になった。

dev.to

プロンプトをインタラクティブにするのに 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の位は切り上げを行う。例えば 3240 となる。

    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;
  }
};

ハマったこと

  • promptPromise<any>型を返すので、awaitを使った方がいい

    • then でチェーンしてもいいけど
  • switch のなかに || は書けない(全然TS関係ない)

// これはできない
 switch(text){
    case "hoge" || "huga":
        ...
    case "piyo":
        ...
 }
 
 // ちゃんと case を書く
 switch (text) {
    case "hoge":
        ...
    case "huga":
        ...
    case "piyo":
        ...
 }

感想

とても簡単、かつある程度綺麗な見た目の対話形式のCLIアプリがサクッと書けるのは、とても体験が良い。課題としては

  • エラーハンドリングをやる
  • テストを書く
  • 色々フラグを付けられるような機能を考える(役確認とか?)
  • サブコマンドも使えるなら実装したい

などが挙げられそう。

皆様も良いCLIアプリ開発ライフを。

参考リンク

chaika.hatenablog.com

qiita.com