テクノロジー

テストは6回落ちて、6回とも実装は正しかった――Claude Opus 5に麻雀ゲームを作らせた編集部が、バグの見つかり方を検証する

議論参加:ナオ (生成AIの実力を取材する技術記者) / カエデ (テスト設計と品質保証のエンジニア) / ハルカ (ゲームAIと評価関数を扱う機械学習エンジニア) / ユウタ (AIでツールを量産する個人開発者) / ミサ (ユーザー体験と信頼性の研究者) / サトシ (雀荘歴の長いアマチュア雀士・ドメイン検証担当) / テツオ (事実確認を担う編集者)

麻雀の点数計算は、実装のバグが1件も出なかった。全37役、役満13種、ダブル役満4種、符計算の例外3種、そして教科書に載っている点数早見表57マスとの照合まで、最初に書いたコードのまま通った。着手前に「本アプリ最大の難所」と宣言していた箇所である。

代わりに手が止まったのは、CPUが半荘を打って和了率4%だった場面と、盤面のスクリーンショットを撮って「赤ドラを残して普通の5を切る」ができないと気づいた場面だった。どちらも麻雀の知識とは無関係な種類の失敗だ。

編集部は今回、Claude Opus 5に日本のリーチ麻雀のスタンドアロンアプリを設計から実装まで作らせ、その全工程を記録した。うまくいったところは短く済ませ、うまくいかなかったところを中心に検証する。

結論:テストが落ちた6件は、6件とも実装ではなくテストの期待値のほうが誤っていた。一方で、実際に見つかった実装バグ10件を検出したのは、統計・実行時間の違和感・スクリーンショットであり、テストは1件も見つけていない。「全部緑」は、検証できている状態とはまったく別物だった。

何を作らせたか

技術記者のナオさんが、まず前提を整理した。

「作らせたのは四人東南戦のリーチ麻雀を、フル実装で。全37役と役満13種、ダブル役満4種、食い下がり、符計算、包。フリテン3種、搶槓、流局6種、流し満貫。個性の違うCPUが5体、初心者向けのアドバイス機能、LAN対戦。牌の画像は著作権対応でぜんぶコード生成のSVGです。スタックはTauri v2 + TypeScript(strict、any禁止)」(ナオ)

Phase 0からPhase 9まで10段階に分け、各フェーズの終わりに「一発で通ったもの」「詰まったもの」「自力で気づけなかったもの」を記録させ、テストが落ちた状態のコミットも履歴に残す運用にした。

最終的にTypeScript 7,165行、テスト156件、コミット10本(うち失敗状態3本を意図的に残置)。

雀韻のタイトル画面。墨色の背景に金のグラデーションで「雀韻」の題字が置かれ、その下に「JAKUIN」の欧文と、「一人打ち(CPU 3 人)」「牌デザインを見る」の2つのボタンが並ぶ

「記録の運用が今回の肝ですね。ふつうに作らせると、直した後のきれいな状態しか残らない。失敗コミットを消させないルールを最初に入れたから、後から検証できる」(テツオ)

なお、本記事に載せているスクリーンショットはすべて、シードを固定した同じ配牌から撮っている。記事のために別途「同じ盤面を何度でも再現できるディープリンク」を実装させ、ヘッドレスブラウザで撮影した。数字と同じで、画像も再現できないと検証にならないためだ。

テストが6回落ちて、6回とも実装は正しかった

Phase 1(シャンテン計算)で3件、Phase 2(点数計算)で3件、テストが落ちた。合計6件。そして6件とも、実装ではなくテストの期待値のほうが間違っていた。

先に断っておくと、点数計算そのものは正しく動いている。下は実際の和了画面で、立直1飜 + ドラ1飜の2飜40符を3,900点と計算し、リーチ棒1,000点を回収して+4,900点になっている。

和了の結果画面。「ロン」「自分 の和了」の見出しの下に和了時の手牌14枚が並び、立直1飜・ドラ1飜の内訳、「2 飜 40 符 3,900 点」、各家の増減(自分+4,900/下家-3,900/対面0/上家0)が表示されている

品質保証エンジニアのカエデさんは、テストの結果に最初に反応した。

「6件中6件がテスト側の誤り、というのは普通じゃないです。私の経験だと、書いたばかりのコードでテストが落ちたら、8割方は実装が悪い。それが0割だった」(カエデ)

自分の手計算がコードより弱かった

最も分かりやすい1件が、2233445566778sという13枚の手牌の待ちだった。テストには「5s・8sの二面待ち」と書かれ、実装は「2s・5s・8sの三面待ち」を返した。

AssertionError: expected '2s5s8s' to be '5s8s'

雀士のサトシさんに、紙の上で検算してもらった。

「2sを足すと22334455667788s。これ、22を雀頭にして234345567678と1枚ずつずらして取れば和了ってます。三面待ちで合ってる。実装が正しいです」(サトシ)

「この階段状の分解、目で数えるときに一番抜けやすいんですよ。私も卓の上でやられたことがある。逆に言うと、コードのほうが人間より正確に数えてる」(サトシ)

残り2件も同じ性質だった。通常形の最大シャンテン数を6と書いた(正解は8)。九蓮宝燈の和了形の分解が複数あると書いた(正解は1通りで、「待ちが9種あること」と「分解が複数あること」を混同していた)。

「役Xだけが成立する手牌」を作るのは、役Xの実装より難しい

Phase 2の3件は、性質が違った。

テスト症状原因
大四喜expected 4 to be 2門前で4つの風牌暗刻を作ると、四暗刻単騎も同時に成立して4倍役満になっていた
混老頭 + 対々和expected [ '四暗刻単騎' ] to include '混老頭'同上。役満が成立したので通常役が数えられなかった
赤ドラ手牌がnull123m0m6m…と書いて13枚しかない手牌を作っていた

「これは麻雀やってる人間なら『そりゃそうだ』って話です。門前で刻子を4つ並べたら四暗刻ですよ。対々和や混老頭のテストを門前で書いたら、ほぼ必ず踏む」(サトシ)

「面白いのは、これがドメイン知識の欠落として出ていることですね。役の実装は正しく書けているのに、その役だけを切り出した検証用の手牌が作れない。実装の能力とテスト設計の能力は別物だという、かなり分かりやすい実例です」(カエデ)

1件目は結果的に「役満同士が正しく複合している」ことの証明になったので、テストを直すのではなく複合を確認するテストとして残し、切り分けたテストを別途追加している。

ここが最も危険な分岐点だった

「怖いのはここです。テストが落ちたとき、最もやってはいけないのは『テストを実装の出力に合わせて書き換えて緑にする』こと。しかも今回のように実装が正しくてテストが間違っているケースは、エラーメッセージだけ見ると実装のバグに見える」(カエデ)

AIに自動で修正させたら、どっちに倒れると思いますか」(ナオ)

「テストのほうを書き換える確率は、決して低くないと思います。エラーの文面上は実装が悪く見えるので、実装を直そうとして、直せなくて、最終的に期待値を出力に合わせる。それをやったら、以降の点数計算はぜんぶずれた土台の上に乗ります」(カエデ)

今回は6件とも手で検算してから直している。その上で、手書きの期待値に頼らない検算手段が追加された。

  • 定義に忠実な低速の参照実装(「1枚加えて和了形になる牌があるか」「ツモ切りを1回して1シャンテン進むか」)との突き合わせ
  • 最適化版と素朴版をランダム32,000件で照合
  • 「シャンテン数は1枚のツモで1より多く進まない」という不変条件を27,200パターンで検査
  • 麻雀の教科書に載っている点数早見表57マスをそのまま書き下して比較

「点数早見表との照合が一番効いています。57件が一発で全部一致した。これは実装のロジックとは独立に作れる期待値なので、人間が手で作る期待値よりずっと信頼できる」(カエデ)

「1万局を例外なく完走」というテストが、ほとんど何も見ていなかった

Phase 3で局進行の状態機械を書き、ランダムに打つCPUで1万局を回すテストが書かれた。一発で緑になり、例外0件、点棒の総量も全局で保存されている。

しかし実行時間が不自然に短かった。1万局が100ミリ秒未満で終わっている。疑って統計を取った結果がこれだ。

  ryuukyoku:exhaustive     3847 (96.2%)
  ryuukyoku:kyuushu          72 (1.8%)
  ron                        57 (1.4%)
  tsumo                      22 (0.5%)
  ryuukyoku:suukaikan         2 (0.1%)

  和了率: 2.0%

「和了が2%。ランダムに切ってたらテンパイまで行かないので当然なんですが、これだと点数計算も支払いも連荘も供託の回収も、1万局中79回しか通ってない。一番壊れやすい経路を、ほとんど検証していない状態です」(カエデ)

牌効率だけで打つ選択関数を追加して回し直すと、こうなった。

  ron                      2370 (59.3%)
  ryuukyoku:exhaustive      825 (20.6%)
  tsumo                     803 (20.1%)

  和了率: 79.3%
  和了 3208 件 / 平均 4560 点

「役の出現頻度も見せてもらいましたが、立直1192、断幺九535、役牌が合計970、平和220、七対子233。実戦の感覚とかなり近い並びです。ここまで来ればテストとして意味がある」(サトシ)

ついでに、常に真になるテストも書かれていた

同じフェーズに、こんなテストが混じっていた。

it('局の平均の長さが現実的(1 人あたり 5 巡以上)', () => {
  const avgDiscards = results.reduce((sum, r) => sum + (r.tenpai.length > 0 ? 1 : 1), 0) / N;
  expect(avgDiscards).toBeGreaterThan(0);
});

(r.tenpai.length > 0 ? 1 : 1)。三項演算子の両側が1なので、常に1を返します。つまりこれは『1 > 0』を確認しているだけのテストです」(ユウタ)

「テスト名だけ読むと意味がありそうに見えるのが厄介ですね。レビューで流し読みしたら絶対に気づかない」(ミサ)

このテストは削除された。

和了率4%のCPU

Phase 6で思考エンジンが作られた。危険度推定(現物・筋・壁・字牌の枚数)を実装し、5体のキャラクターにパラメータを割り当て、120半荘の自動対戦を回した結果がこれだ。

キャラクター          平均順位  和了率  放銃率  平均打点  トップ率  ラス率
速攻の ジン            1.775     1.7%    0.7%     5017     55.0%   10.0%
見習いの ハル           2.683     0.7%    0.8%     8114     19.2%   30.8%
門前の ソウ            2.742     1.0%    0.4%     9480     12.5%   26.7%
雀聖 レン             2.800     0.7%    0.3%     7857     13.3%   32.5%

4人合計の和了率が4.1%。Phase 3で牌効率だけの打ち手が79%を出していたので、思考エンジンを足したら和了しなくなったことになる。

機械学習エンジニアのハルカさんが、原因の評価式を指摘した。

const progress = (4 - r.shanten) * 30 + r.ukeire.count * 2;

「これは典型的な事故です。シャンテン1つの差が30点。一方で序盤の受け入れ枚数は40枚から60枚あるので、右の項は最大120点になる。受け入れの広い2シャンテンが、1シャンテンより高く評価される。CPUは手が進みそうな形を選び続けて、いつまでもテンパイしない」(ハルカ)

修正はこうなった。

// シャンテン数は受け入れ枚数より必ず優先されなければならない
const progress = -r.shanten * 1000 + r.ukeire.count * 3;

同じ120半荘での修正後。

門前の ソウ            2.125    22.0%    9.9%     7081     39.2%   14.2%
雀聖 レン             2.142    22.4%    9.3%     6126     33.3%   15.8%
速攻の ジン            2.625    16.4%   13.6%     4818     17.5%   24.2%
見習いの ハル           3.108    10.7%   16.0%     6076     10.0%   45.8%

「係数の重み付けは、コードとしては何も間違っていないんですよ。型も通るし、例外も出ないし、盤面でも普通に牌を切っている。数値のスケール感という、コードに書かれていない前提を外しただけです」(ハルカ)

「防ぐ方法はありますか」(ナオ)

「係数に単位を持たせることですね。『シャンテン1つ = 1000点、受け入れ1枚 = 3点』のように意味のある尺度にしておけば、302という値を並べた瞬間に不自然だと分かる。今回の修正後の式は、まさにそうなっています」(ハルカ)

最強に設定したキャラが、1000半荘で2位だった

パラメータを6つ用意してキャラに名前を付けた時点では、それぞれが設定通りに振る舞うように見えていた。実測すると外れていた。

調整前の1000半荘(座席の有利不利を打ち消すため、半荘ごとに席順をローテーション)。

キャラクター          平均順位  和了率  放銃率  平均打点  トップ率  ラス率  平均点
門前の ソウ            2.095    22.1%   10.5%     6755     39.1%   13.7%    31043
雀聖 レン             2.181    21.7%    9.7%     5892     34.5%   15.9%    30147
速攻の ジン            2.634    16.0%   13.4%     4816     17.0%   24.3%    22580
見習いの ハル           3.090    11.4%   15.3%     6005      9.4%   46.1%    16231

「このアプリで最も強い相手」と設定した雀聖レンが2位だった。

「1000半荘での平均順位の標準誤差は約0.035です。0.086の差は誤差では説明できません。本当に負けている」(ハルカ)

「原因は数字に出ていますね。レンの平均打点5892に対してソウが6755。門前で打点を作るほうが期待値が高い環境だった、と」(サトシ)

「あと、レンのリスク許容度を0.4に設定していたのが効いています。押し引きを絞りすぎて和了率がソウを下回った。21.7%対22.1%。守備を厚くした分だけ順位が落ちるという、麻雀としてはよくある話がそのまま出ています」(ハルカ)

レンを門前寄りに振り直し(鳴き頻度0.42→0.22、打点志向0.55→0.72、リスク許容度0.4→0.55)、600半荘で再計測した。

雀聖 レン             2.043    24.2%   10.5%     6391     39.2%   13.7%    32185
門前の ソウ            2.217    21.8%   10.6%     6705     34.3%   15.8%    29653
速攻の ジン            2.667    15.9%   13.6%     5083     17.8%   26.5%    21853
見習いの ハル           3.073    11.2%   15.6%     6204      8.7%   44.0%    16310

意図した並びになった。

「重要なのは、調整前の数字を消さずに残していることです。ふつうはチューニング後のきれいな表だけが記事に載る。『最強と名付けたキャラが2位でした』を先に書かせるルールにしたのが効いた」(テツオ)

「守備特化キャラが全指標で1位」という前提も間違っていた

もう1件、「鉄壁のミオが最も放銃率が低い」というテストも落ちている。

放銃率: wall 8.2% / novice 14.8% / rusher 12.4% / master 6.9%

「守備特化のミオより、雀聖レンのほうが放銃率が低い。これはパラメータのミスじゃなくて、レンが守備も上手いから当たり前なんです。前提のほうが間違っている」(ハルカ)

「テストは『ミオは押しの強い見習い・速攻より放銃率が低い』に直して、レンとの比較は入れないことにしています。これは実装を直すのではなくテストの前提を直した正当なケースですね。さっきの6件とは性質が違う」(カエデ)

型もテストも見つけられなかったUIバグ

Phase 5で盤面が作られ、ブラウザで起動して初めて分かったバグが4件ある。型チェックは通り、テストも145件全部緑で、コードを読んでも不自然に見えない状態で起きていた。

現在の対局画面がこれだ。右側が初心者向けのアドバイスパネルで、推奨打牌のそれぞれに「なぜそれを切るのか」の日本語が付く。

対局画面。中央の緑の卓を4人が囲み、上下左右に河が並ぶ。中央のパネルに「東1局 0本場」「ドラ表示」「残り39」、右側のアドバイスパネルに「シャンテン 1」「受け入れ 7種20枚」「押し引き 慎重に」と、推奨打牌1位の4索、2位の1索、3位の8索が理由文つきで並んでいる

UX研究者のミサさんが、この画面に至るまでに潰したバグを、いちばん重いものから挙げた。

「(a) 同じ種類の牌が1枚しかクリックできない。エンジン側が『同じ種類の牌を2枚持っていても選択肢は1つ』に重複排除していて、UIは牌のIDで照合していた。だから2枚目以降が押せない牌として灰色になる」(ミサ)

「(b) がもっと深刻で、その重複排除が赤ドラと普通の5を同じものとして扱っていた。赤5を残して普通の5を切る、という麻雀で最も基本的な選択ができない」(ミサ)

サトシさんの反応が速かった。

「それは麻雀として成立してないですよ。赤5は1飜ぶんの価値があるので、どちらを切るかは常に考える。この状態でリリースしたら、打てる人は10秒で気づいて閉じます」(サトシ)

「でもこれ、型でもテストでも検出できないんですよね。エンジンは仕様どおりに動いていて、UIも仕様どおりに描画している。仕様そのものが麻雀として間違っている」(ユウタ)

「気づいたきっかけは、盤面の牌が1枚だけ灰色になっていたことです。スクリーンショットを1枚撮っただけ。逆に言えば、撮らなければ最後まで残った」(ミサ)

残り2件は表示の問題だった。座席ごとに卓を回転させたため、対面のプレイヤー名と点数が180度回転して読めなくなった(牌だけ回転させ、文字は逆回転させて正立させた)。また、回転させるとDOMの並び順が視覚的に反転するため、河が卓の外側に並んでいた。実際の麻雀卓では河は必ず内側にある。

同じフェーズで、索子(竹の牌)が点にしか見えないという問題もスクリーンショットで発覚している。棒の幅が高さの0.36倍しかなく細すぎたのと、3索に汎用の配置(対角線)を使ったため階段状に並んで不自然だったのが原因だ。

牌のデザインは、正直まだイマイチだ

牌の絵柄は著作権対応のため、既存の牌画像もフォントの麻雀絵文字も使わず、全34種をコード生成のSVGで描かせた。外部アセットは0件である。

方針としては成功しているが、**出来上がったデザインそのものは、率直に言って売り物の水準に達していない。**編集部で並べて見ながら、問題点を洗い出した。

雀韻の牌一覧。萬子9種、筒子9種、索子9種、字牌7種、赤ドラ3種、および通常・ツモ牌・選択不可・横向き・裏向きの各状態が並んでいる

「まず索子です。2索3索はいいんですが、6索から9索が『棒の束』にしか見えない。実際の牌なら本数がひと目で分かるように配置と色に工夫があるんですが、これは等間隔に並べているだけなので、河に小さく並んだときに数えるのが面倒です」(サトシ)

「7索と8索を一瞬迷いますね。7索が『上に1本+下に2×3』、8索が『2×4』なので、縮小されると区別がつきにくい」(ミサ)

筒子にも同じ指摘が出た。

「筒子の丸を青と緑の交互にしているのは、既存デザインに寄せないための工夫だと思うんですが、交互だとまとまりが見えないんですよ。実物は色でグループを作って個数を読ませている。ここは独自性を出すところじゃなかった」(サトシ)

「7筒・8筒・9筒は丸が小さくなりすぎていますね。牌のサイズに対して絵柄が小さく、余白が多い」(ミサ)

字牌については、白の扱いが議論になった。

「白を『二重枠』で描いたのは独自の解ですが、初心者には空白の牌にしか見えません。用語集で『白』を説明していても、盤面で見て何か分からなければ意味がない」(ミサ)

「打つ側からしても、河に白があるかどうかは重要な情報なので、ここは分かりやすさを優先すべきところです」(サトシ)

赤ドラの表現も統一されていない。

「5萬は数字全体が赤なので一目で分かるんですが、5筒と5索は真ん中の1個だけが赤なんです。同じ『赤ドラ』なのに認識のしやすさが揃っていない。統一されていない記号は、使う側の負荷になります」(ミサ)

そして全体の質感について、最も本質的な指摘が出た。

「一番の問題は、全部フラットな塗りだということです。本物の麻雀牌は、彫って着色しているので必ず彫りの陰影がある。この牌には陰影がなくて、厚みの表現も下端の細い帯だけ。だから並べたときに『印刷された紙』に見えて、『牌』に見えないんです」(ミサ)

「一索の竹の若芽も、著作権を避ける判断としては正しいんですが、無難すぎて地味ですね。伝統的な鳥の意匠って、あれ1枚だけ華やかだから場が締まるんですよ。今の図案には、その『特別感』がない」(サトシ)

「萬子の『萬』も、システムフォントのゴシック体をそのまま置いているだけなので、和の雰囲気が出ていません。しかもフォントを同梱していないので、環境によって字形が変わります。これは見た目の問題であると同時に、未対応項目でもある」(ユウタ)

ナオさんが論点を整理した。

「著作権を避けるという制約は満たしているし、34種を破綻なく描き分けてもいる。『要件は満たしたが、良くはない』という状態ですね。牌の見た目は、麻雀ゲームでプレイヤーが最も長く見続ける部分なので、ここが平板なままだと『売り物』にはならない」(ナオ)

「面白いのは、これがバグではないことです。仕様どおりに動いていて、テストも通り、著作権の要求も満たしている。誰も間違っていないのに良くない。この種の欠落は、AIに『作れ』と指示するだけでは絶対に埋まらない領域だと思います」(ミサ)

編集部として挙がった改善案は、彫りの陰影を入れる、索子と筒子を色でグループ化して本数を読ませる、白に何らかの識別要素を足す、赤ドラの表現を統一する、OFLフォントを同梱して字形を固定する、の5点だった。いずれも今回は未対応である。

麻雀と無関係だった足止め

個人開発者のユウタさんは、詰まった箇所の内訳に注目した。

「麻雀のルールでは1回も詰まってないんですよ。詰まったのは全部、麻雀と関係ないところ」(ユウタ)

TypeScriptの型設定と、増減演算子が併用できない

src/core/shanten.ts(44,5): error TS2532: Object is possibly 'undefined'.
src/core/parse.ts(62,5): error TS2532: Object is possibly 'undefined'.

noUncheckedIndexedAccessを有効にするとc[i]の型がnumber | undefinedになる。読み出しはc[i]!で回避できるが、c[i]!--は非nullアサーションを代入対象に使えないため通らない。シャンテン計算はカウント配列の増減が主処理なので、この書き方が全域に出てくる。

at() / inc() / dec()のヘルパを作って全部置き換えています。結果的にdec(c, i, 3)で『刻子を1つ取り除く』意図が読める形になったので、可読性はむしろ上がった。厳しい型設定に殴られて、たまたま良い方向に転んだ例ですね」(ユウタ)

22ミリ秒の壁

打牌候補の評価が22ms/回だった。14通りの打牌 × 34種の受け入れ判定で476回シャンテン計算を呼ぶため、1回54µsなら26msになる計算だ。

「この速度だと1000半荘のシミュレーションに3時間以上かかります。つまりPhase 6のCPU検証が実行不可能になる。ここで気づけたのが大きい」(ハルカ)

面子が色をまたがない性質を使い、萬子・筒子・索子・字牌の4グループに分割。グループごとの結果を「牌の並びの5進数符号」でメモ化し、(面子数, 雀頭の有無) → 最大搭子数のプロファイルをDPで合成する方式に変更した。

変更前変更後
シャンテン計算54.4 µs/回1.9 µs/回
打牌候補の全評価22.00 ms/回0.72 ms/回

「素朴版を消さずに残して、ランダム32,000件で最適化版と完全一致することをテストしています。最適化で静かに壊すのが一番怖いので、これは正しい判断」(カエデ)

Tauriのビルドが2回失敗した

error: proc macro panicked
  = help: message: The `frontendDist` configuration is set to `"../dist"` but this path doesn't exist
  = help: message: failed to open icon .../src-tauri/icons/icon.png: No such file or directory

tauri::generate_context!()はビルド時にフロントエンドの出力とアイコンの実体を要求する。フロントを先にビルドし、アイコンはコードで生成(zlibでPNGを直接書き出し)して解決した。

「外部アセットを使わない方針が、結果的にアイコンにも適用された形ですね。PNGをzlibで直接書き出すのは、やろうと思えばできるけど普通は思いつかない解き方です」(ユウタ)

難所の予想は、7つ中6つ外れた

着手前に「難しいと予想する箇所」を7つ宣言させておいた。答え合わせがこれだ。

予想した難所結果
符計算(多面待ちの分解ごとの評価)一発で通過
平和の判定(三暗刻との排他)一発で通過
多面待ちの全列挙(九蓮宝燈の9面待ち)一発で通過
フリテン3種の区別一発で通過
カンの処理(搶槓・包・カンドラのタイミング)一発で通過
鳴きの優先順位・ダブロン一発で通過
CPUの押し引き唯一当たった(評価関数の係数で大失敗)

「麻雀の点数計算は『複雑』って言われますけど、複雑さの質が分岐が多いだけで各分岐は単純という種類なんですよね。符の計算式も役の条件も、全部が明文化されている」(サトシ)

「仕様が確定していて分岐が多いだけの複雑さは、生成AIが最も得意とする形に近いです。逆に、仕様が書かれていないもの――数値のスケール感、麻雀を打つ人の常識、画面の読みやすさ――で全部落ちている」(ナオ)

何が実際にバグを見つけたのか

編集部が最も重要だと判断したのが、この整理だ。

気づいた手段見つけたもの
テストの失敗なし(テストが落ちた6件はすべてテスト側の誤りだった)
統計を取ったCPUの評価関数の係数ミス / 1万局テストが和了を2%しか通していない / 難易度と強さの不一致
実行時間の違和感1万局が100ms未満で終わっていた(統計を取るきっかけ)
スクリーンショット索子が点に見える / 同種牌が押せない / 赤ドラを残せない / 他家の表示が読めない向き / 河が卓の外側 / 和了画面に手牌が出ない / アドバイスの「現物」誤表示
型チェックnoUncheckedIndexedAccessと増減演算子の非互換
ビルドエラーTauriのフロント出力とアイコンの不足

「テストが落ちた6件は1件も実装バグじゃなくて、実装バグ10件はぜんぶ統計・実行時間・スクショで見つかっている。かなり極端な結果です」(ナオ)

「誤解してほしくないのは、これは『テストを書くな』という話ではないことです。点数早見表57マスとの照合、参照実装との突き合わせ、不変条件の検査――これらはちゃんと機能しました」(カエデ)

「機能しなかったのは、自分で期待値を書くタイプのテストです。書いた本人の理解を超えられない。麻雀のように書き手より仕様が複雑な領域では、期待値そのものが誤る確率が無視できない」(カエデ)

「それと『例外が出ないことを確かめるテスト』。1万局のテストがまさにそれで、名前は立派だけど和了を2%しか通していなかった。書いた本人が安心するだけで、検出力がほとんどない」(ミサ)

サトシさんは、別の角度から補足した。

「打てる人が1回見れば分かることが、いくつもありました。赤ドラの件も、河が外側に並んでいるのも。ドメインを知っている人間が10秒画面を見るのが、テスト100件より効いた場面はあったと思います」(サトシ)

未対応のまま残ったもの

テツオさんが、記録から未対応のリストを読み上げた。

  • 打牌・ツモ・点数移動のアニメーション(盤面が瞬間的に切り替わる)
  • 和了の演出(役満でも見た目が変わらない)
  • 効果音(外部素材を使わない方針のため、合成音を含めて未着手)
  • LAN対戦のロビー画面(プロトコル・ホスト・クライアント・Rustの通信層は実装とテスト済みだが、ホストを探して選ぶUIが無いため人が始められない)
  • 牌譜の再生画面(保存形式とイベント記録は実装済み)
  • 設定画面、クレジット画面、フォントの同梱(字形が環境依存のまま)
  • 牌デザインの作り込み(彫りの陰影、索子・筒子の可読性、白の識別、赤ドラの表現の統一)
  • tauri buildによる配布ビルドの検証、実機2台でのLAN対戦の検証

「『見た目がかっこいい、売り物になるレベル』という要求に対して、いちばん遠いのはアニメーションと演出です。動くことと気持ちよいことの差が、そのまま残っている」(ミサ)

「LAN対戦は惜しいですね。プロトコルもホストもクライアントもRustの通信層も動いてテストも通っているのに、『ホストを選ぶ画面』が無いから人が始められない。技術的には完成しているのに、製品としては0点です」(ユウタ)

もう一度作るとしたら

議論の最後に、次に同じことをやるならどうするかを整理した。

「Phase 1の直後に統計を取る仕組みを入れることです。今回、実装バグを見つけたのはほぼ全部が統計と目視でした。それを最初から常設にする」(カエデ)

「UIを前倒しする。赤ドラを選べない問題は、盤面を1回表示すれば即座に分かるものでした。コアを完成させてからUIに降りる順序が、この種のバグを最後まで隠してしまう」(ミサ)

「評価関数の係数に意味のある単位を持たせる。『シャンテン1つ = 1000点』のような尺度なら、係数ミスは書いた瞬間に不自然だと分かります」(ハルカ)

「テストの期待値を手で書かない。参照実装・不変条件・既知の数値表との照合の3つに寄せたほうが、書く量あたりの検出力がはるかに高い」(カエデ)

ナオさんが議論を締めた。

「麻雀のルールという『巨大だが確定した仕様』を実装する能力は、正直、想像より高かったです。7,165行を10フェーズで書き切って、点数計算にバグが1件も出ないのは、人間のチームでもそう簡単ではない」(ナオ)

「逆に、自分の書いたものが正しいかを自分で確かめる部分――テストの期待値を作り、検証が実際に機能しているかを疑い、画面を見て違和感を持つ――が、そのまま弱点として残りました。作る力と、確かめる力の差が、今回いちばんはっきり見えたところです」(ナオ)

キーワード:#Claude Opus 5#ゲーム開発#テスト設計#評価関数#シャンテン計算

コメント