開発日記⑨ Vitestで麻雀の点数計算をテストする

前回までの開発

前回は、難易度を選んでオーラス条件をランダム生成する機能を追加した。
これで、4人分の点数を毎回手入力しなくても、ボタンを押して条件を試せるようになった。
ただ、ここまで点数計算の処理が増えてくると、画面を操作するだけですべて確認するのは難しくなってきた。
特に麻雀の点数移動には、

  • 本場
  • 供託
  • 親と子
  • ロンとツモ
  • 流局時のテンパイ人数
  • 同点時の順位

など、間違えやすい条件が多い。
そこで今回は、Vitestを使って点数計算のテストを書くことにした。

Vitestを使ってテストする

今回は、テスト用のライブラリとしてVitestを使う。
テストの基本的な形は次のようになる。

import {
  describe,
  expect,
  it,
} from "vitest";

それぞれの役割は、だいたい次のとおり。

  • describe:関連するテストをまとめる
  • expect:実際の結果が期待どおりか確認する
  • it:何を確認するテストなのかを書く

たとえば、単純な計算なら次のようなイメージになる。

it("1と2を足すと3になる", () => {
  expect(1 + 2).toBe(3);
});

関数へ値を渡し、返ってきた結果が想定した値と一致するかを確認する。

テスト用の点数を用意する

複数のテストで同じ点数状況を使うため、基準となる点数を用意した。

const baseScores: Scores = {
  east: 32000,
  south: 24500,
  west: 23000,
  north: 20500,
};

この点数なら順位は、

  1. 東家
  2. 南家
  3. 西家
  4. 北家

となる。
同じ点数を何度も書かずに済むため、それぞれのテストでこのbaseScoresを使用する。

順位計算をテストする

最初に、点数が高い順に正しく並ぶか確認した。

const ranking =
  calculateRanking(baseScores);

expect(ranking[0]).toMatchObject({
  seat: "east",
  score: 32000,
  rank: 1,
});

1番目の要素が東家で、32,000点、1着になっていることを確認している。
同じように、南家、西家、北家についても期待した順位になっているか確認する。
また、全員が同点の場合のテストも追加した。

const ranking = calculateRanking({
  east: 25000,
  south: 25000,
  west: 25000,
  north: 25000,
});

expect(
  ranking.map((row) => row.seat),
).toEqual([
  "east",
  "south",
  "west",
  "north",
]);

このアプリでは、同点の場合に東家・南家・西家・北家の順で上位にする。
通常の点数だけ確認していると、同点時の処理を変更した際に間違いへ気付きにくい。
こうした特殊な条件こそ、テストへ残しておく意味がありそう。

ロン後の点数をテストする

次は、ロンによる点数移動を確認する。
テストでは、南家が東家から3,900点をロンした状況にした。
さらに、1本場と供託1本を設定する。

const result = applyRon(
  baseScores,
  "south",
  "east",
  3900,
  1,
  1,
);

1本場なので、東家が支払う点数は次のようになる。

3,900点 + 300点
= 4,200点

東家は32,000点から27,800点になる。
南家は4,200点に加えて供託の1,000点も受け取るため、24,500点から29,700点になる。

expect(result.east).toBe(27800);
expect(result.south).toBe(29700);
expect(result.west).toBe(23000);
expect(result.north).toBe(20500);

和了者と放銃者だけでなく、関係のない2人の点数が変わっていないことも確認する。

ツモ後の点数をテストする

ツモは、親と子で支払い方が異なるため、複数のテストを用意した。
まずは子のツモ。

const result = applyTsumo(
  baseScores,
  "south",
  2000,
  1000,
  2,
  1,
);

南家が和了者で、親が2,000点、子が1,000点を支払う。
さらに2本場なので、それぞれの支払いへ200点を加算する。
供託1本も南家へ加える。

expect(result.east).toBe(30800);
expect(result.south).toBe(30100);
expect(result.west).toBe(21800);
expect(result.north).toBe(18300);

南4局では北家が親なので、北家だけがほかの2人より多く支払う。
もう一つ、北家が親ツモした場合も確認した。

const result = applyTsumo(
  baseScores,
  "north",
  2000,
  2000,
  1,
  0,
);

親ツモなので、ほかの3人が同じ金額を支払う。
1本場を含め、全員が2,100点ずつ支払う結果になっているか確認する。
親ツモと子ツモを分けてテストすることで、親の判定を間違えた場合にも気付きやすくなった。

流局時の点数移動をテストする

流局は、テンパイ人数によって受け取る点数と支払う点数が変わる。
今回は、次のケースをテストした。

  • 1人テンパイ
  • 2人テンパイ
  • 全員テンパイ

1人テンパイの場合は、テンパイ者が3,000点を受け取り、ほかの3人が1,000点ずつ支払う。

const result = applyDraw(
  baseScores,
  ["south"],
);

expect(result.east).toBe(31000);
expect(result.south).toBe(27500);
expect(result.west).toBe(22000);
expect(result.north).toBe(19500);

2人テンパイの場合は、テンパイ者が1,500点ずつ受け取り、ノーテン者が1,500点ずつ支払う。
全員テンパイの場合は点数移動がないため、結果が元の点数と同じであることを確認する。

expect(result).toEqual(baseScores);

必要なロン点もテストする

点数移動だけでなく、目標順位に必要な点数の計算も確認した。
基準となる点数では、南家は24,500点で2着、東家は32,000点で1着。
南家がトップを目指して東家からロンする場合、必要な最低ロン点は3,900点になる。

const requiredPoints =
  findRequiredRonPoints(
    baseScores,
    "south",
    "east",
    "top",
    0,
    0,
  );

expect(requiredPoints).toBe(3900);

また、ロンとツモを含めた達成条件の中から、最も低い和了点を求める処理もテストした。

const minimumRequiredPoints =
  getMinimumRequiredWinPoints(
    baseScores,
    "south",
    "top",
    0,
    0,
  );

expect(
  minimumRequiredPoints,
).toBe(3900);

ランダム条件の難易度判定にも使っている処理なので、ここが壊れるとEasy・Normal・Hardの分類までおかしくなる。
画面に表示される数字だけでなく、その数字を求める元の処理をテストしておくことにした。

テストを実行する

テストの実行には、次のコマンドを使う。

npm run test

こちらはテストを起動したままにして、ファイルを変更すると再実行する。

一度だけ実行したい場合は、次のコマンドを使う。

npm run test:run

すべて成功すれば、現在書いているテストについては期待どおり動いていることが確認できる。
もちろん、テストに書いていない条件まで正しいと保証されるわけではない。
それでも、機能を追加したあとに以前の計算が壊れていないか、まとめて確認できるようになった。

今回できたこと

今回の進捗は以下のとおり。

  • Vitestでテストを実行できるようにした
  • 点数順の順位計算をテストした
  • 同点時の席順をテストした
  • ロン時の本場と供託をテストした
  • 子のツモによる点数移動をテストした
  • 親ツモによる点数移動をテストした
  • 流局時のノーテン罰符をテストした
  • 目標順位に必要なロン点をテストした
  • 最低和了点の計算をテストした

画面上ではほとんど変化がないけれど、点数計算を安心して変更するための土台が少しできた。
麻雀は条件分岐が多いため、今後不具合が見つかったときも、その条件をテストとして追加していきたい。
次は、増えてきた状態管理と計算処理を整理するため、Reactのカスタムフックへ処理を分けた部分についてまとめる。

牛一。

コメント

タイトルとURLをコピーしました