砂に残る波紋の輪と半分埋まった金貨

砂場に石を落とす。波紋が広がって、埋まっているものにぶつかった輪だけが残る。輪の本数で「何マス先にいくつ埋まっているか」は分かるけれど、方角は分からない。だから石をもう1つ落として、輪の交わりを読む。そうやって金貨の場所を推理して、掘る。

「となりの埋蔵金」というブラウザゲームを作った。ひとりで遊ぶほかに、ネットで知らない人と対戦できる。実物はここにある。

となりの埋蔵金を遊ぶ

コードはいつもどおりAI(Claude)に書いてもらって、私は遊んではダメ出しをする係。サーバーに置く作業まで、今回は全部AIがやった。この記事は、公開した日に起きたことと、公開する前に見つけた穴の話。

波紋では「何かが埋まっている」までしか分からない

最初に決めた数字だけ書いておく。盤面は6×6。埋まっているのは金貨1枚とはずれ3つ。石は5個まで。掘れるのは2回。この数字は、作る前に「5個の石で金貨のありうる場所が何マスまで絞れるか」を全数え上げで測って決めた。4〜6マス。機械的に1マスに確定する盤面は無く、真の場所が候補から外れる盤面も無い。

ただ、波紋は金貨とはずれを見分けられない。だから最後は4択の当てずっぽうになる。これに気づいたのは、自分で遊んで「CPUとやっているのと変わらない」と言ったときだった。AIの検証は「何かが埋まっているマス」を数えていて、「どれが金貨か」を数えていなかった。

そこで足したのが、埋める人が「ひとこと」を添える仕組み。「金貨は左半分にある」。ホントでもウソでもいい。掘る人には文だけが見え、真偽は掘り終わるまで伏せられる。左右・上下のひとことは候補をおよそ半分に削るので、掘る側の判断は「信じるか、逆を読むか」の1点に集まる。埋める側は、どのひとことを選ぶか、ウソをつくか、そのためにどこに置くか。置き場所が初めて意味を持つ。予測できないものの源が、乱数から人の判断に移った。

ネットに置くなら、お宝の場所をブラウザに渡せない

ひとりで遊ぶ版は、1台のブラウザの中で全部が動いていた。金貨の場所もブラウザが持っている。これをそのままネットに置くと、開発者ツールで答えが見える

だから対戦版では、お宝の場所はサーバーだけが持つ。波紋の読み(何マス先にいくつあるか)もサーバーが計算して、各自には見てよい分だけを返す。置き先は普通のレンタルサーバーで、常時つながる仕組みは使えない。なのでブラウザが1.2秒おきに「何か変わった?」とサーバーに聞きにいく作りになった。部屋はJSONファイル1つ。データベースは無い。

公開する前に見つけた穴: 「できません」は答えを教えている

作り替えの途中で、AIが1つ穴を見つけた。

自分の「はずれ」を置くとき、すでに何かが埋まっているマスを選ぶと「そこにはもう何か埋まっています」と断っていた。同じ画面を回しているうちは隣で見ているので問題にならない。でも別々の端末になった瞬間、全マスを順に押すだけで、相手の金貨の場所が全部分かる

直し方は、断り文句をぼかすことではなかった。断る場面そのものを消す。お宝は全員が置き終わってから空いているマスに埋める順番に変え、重なったら重なったまま許す(実際に2つあるので嘘にならない)。「できません」と言う設計は、その時点で漏れている。

それから、プレイヤーの名前。知らない人が書く文字は、そのまま画面に出すと悪さができる。サーバー側と画面側の両方で無害化して、名前を出すところでは必ずそれを通す決まりにした。身内で回しているあいだは気にしていなかったものが、入口を開けた瞬間に全部、数え直しになる。

上げた

サーバーへの転送は、AIが直接やった。上げる前に「何も転送せず、上げるものを並べるだけ」で1回走らせて、中身を見てから本番。新しいフォルダに置くだけで、既存のファイルには触れない作りにしてある。

上げてから確かめた。ページが開くこと。部屋のデータが外から見えないこと。PHPが動いて部屋が作れること。サーバー側の試験を本番に向けて走らせて全部通ること。2つのブラウザで実際に対戦して、相手の石の波紋がこちらの画面にも正しい本数と半径で出ること。ここまでは画素で測って合っていた。

「はじめる」を押しても、始まらない

ところが遊ぼうとしたら、「はじめる」を押しても始まらなかった。

原因は、1.2秒ごとの問い合わせと、ボタンを押して手を送る処理が、同じ1つの札を取り合っていたこと。「いま通信中」を表す札が1枚しかなく、問い合わせが飛んでいる最中にボタンを押すと、その操作が黙って捨てられていた。

手元では一度も出なかった。手元のサーバーは往復5ミリ秒なので、1.2秒のうち当たる隙は0.4%。試験を何十回走らせても引かない。本番は往復40ミリ秒で、隙は3%。すぐ出た。

手元で出ないのは「無い」ではなく、「隙が狭いだけ」だった。

直し方は、札を2つに分けること。人が押した操作はいつでも通す。止めるのは二度押しのときだけ。自動の問い合わせのほうを譲らせる。順番が入れ替わって古い返事が後から届いても、置いたばかりの石が消えないように版番号で古いものを捨てる。直したものを上げ直して、同じ手順で通ることを確かめた。

確かめるのに本番で部屋をいくつか作ったので、全部消した。いま部屋のフォルダは空だ。

同じ規則が2か所にある

もう1つ、これは仕組みの話。同じ遊び方の決まりが、ブラウザ側のコードとサーバー側のコードの2か所に書いてある。片方だけ直すとズレる。なので照合するスクリプトを作って、手を入れたら最後にそれを走らせてから終わる、と決めた。決まりを1か所にまとめられればいいのだけれど、言語が違うのでそうはいかない。ズレるものは、ズレていないことを毎回確かめる。

遊ぶとき

リンクを開いて「ネットで集まる」→「だれかと遊ぶ」と押すと、いま誰かが待っている部屋に入れる。誰もいなければ自分が開いて待つ。一覧に出るのは「45秒以内に問い合わせてきた人が座っている部屋」だけにした。誰もいない部屋を出すと、入っても何も起きず、遊ぶ気を折るので。知り合いと遊ぶなら「部屋を作る」で4文字の合言葉が出る。合言葉を打たずに入れるリンクも一緒に出る。

1つだけ注意。同じブラウザの普通のタブを2つ開くと、同じ席に座る。ひとりで2人分を試すときは、2つ目をシークレットウィンドウで開いてほしい。

残っているもの

技術の話は、だいたい片づいた。残っているのは、人が遊んで面白いかどうか。1試合が何手で終わるのか、先手は有利なのか、4人で波紋が重なって読めなくならないか。ここは私にも、AIにも、手元では測れない。

それと、最大の未確認は「同時に2人来るか」だ。対戦ゲームは、相手がいて初めて動く。

となりの埋蔵金を遊ぶ