連載「クリシンクエストをつくる」· 第2編/全8編
正解を、ブラウザに渡さない — 見抜くゲームを成立させる設計
“ソースを見れば答えがわかる”をどう防いだか。秘密は「隠す」のではなく「渡さない」ことでした
見抜くゲームは、答えがどこかに置いてあれば成立しません。ブラウザは中身がすべて見えてしまう——だからクリシンクエストは「正解をクライアントに渡さない」設計にしました。サーバー権威という考え方を、開発の裏側からやさしくお話しします。
連載「クリシンクエストをつくる」· 第2編/全8編
“ソースを見れば答えがわかる”をどう防いだか。秘密は「隠す」のではなく「渡さない」ことでした
見抜くゲームは、答えがどこかに置いてあれば成立しません。ブラウザは中身がすべて見えてしまう——だからクリシンクエストは「正解をクライアントに渡さない」設計にしました。サーバー権威という考え方を、開発の裏側からやさしくお話しします。
前回は、AIに「本当っぽい嘘」を語らせる設計のお話でした。今回はその続きです——その嘘を、プレイヤーに先回りして見られないようにするお話を、一緒に見ていきましょう。
クリシンクエストは「3体のうち、どれが嘘つきか」を質問で見抜くゲームです。ということは、答えがどこかに置いてあった瞬間、ゲームは壊れてしまいますよね。見抜く前に「見つけられて」しまうからです。
考えてみると、これは謎解きや脱出ゲームと同じ事情です。答えのカードが床に落ちていたら、どんなによくできた謎でも、もう謎として成り立ちません。クリシンクエストにとっての「いちばんの敵」は、巧妙なズルをする誰かではなく、設計者である自分がうっかり答えを置き忘れてしまうこと——そんなふうにも言えるかもしれません。
クイズアプリで、こんな経験はありませんか。答えが知りたくて、ブラウザの「ページのソースを表示」を押したら、正解がそのまま書いてあった——。笑い話のようですが、これは初心者だけの失敗ではないんです。ブラウザに送ったものは、すべて見られる。このWebの大原則を見落とすと、だれでもこうなってしまいます。
実は、私も最初の試作では同じ落とし穴に近いことをしかけていました。「画面に表示しなければバレないだろう」と、つい思ってしまうんですね。でも、見抜くゲームにとって、これは致命的です。答えがどこかに置いてある時点で、それはもう「見抜く」体験ではなく「探す」作業になってしまいますから。だからこそ、ここはいちばん最初に、しっかり考え抜いておく必要がありました。
ここはしっかり押さえておきたいところです。利用者のブラウザに届いたデータは、例外なく中身を見られます。
つまり、「答えをブラウザに送っておいて、画面では JavaScript で隠す」は、まったく意味がないんですね。隠したつもりでも、データ自体はそこにあるからです。これは前回の「AIに“言うな”と頼むだけでは不十分」というお話と、まったく同じ構造をしています。お願いや見た目の細工では、秘密は守れない。
守る方法はひとつだけです。最初から渡さないこと。
クリシンクエストの問題データには、実はたくさんの「答え」が含まれています。どれが嘘つきか(correct_answer)、その嘘の中身(false_claim)、検証済みの事実(verified_facts)、種明かしの解説(correct_explanation)——。
これらを、まるごとブラウザに渡すことは、絶対にしません。代わりに、ブラウザへ送るのは「公開版」だけです。問題ごとに、こう線を引いています。
渡してよいもの(公開版に含む)
絶対に渡さないもの
コードの中では、この線引きを publicQuestion(公開版を作る関数)という一箇所に集約しています。「答えを消し忘れる」という事故を防ぐには、通り道を一本にするのがいちばん効くんですね。
では、答えはどこにいるのでしょうか。
ゲームを始めると、サーバーがセッションを作ります。これは「この対局はこの問題、正解はこのAI」と書いた封筒のようなものです。封筒はサーバーの引き出し(データベース)にしまわれ、ブラウザが受け取るのは「公開版の問題」と「セッションの番号」だけ。
その後、プレイヤーが質問しても、投票しても、やり取りはすべてセッション番号を通じてサーバーが処理します。AIの返答を作るのも、最後の採点も、サーバー側です。答えの本体は、最後まで引き出しから出てこないんですね。
たとえるなら、受験会場で問題用紙だけが配られて、模範解答は試験官の手元の封筒にしまわれている、あの感じに近いかもしれません。受験者がどれだけ机の上を探しても、解答はそこにはない。クリシンクエストも同じで、ブラウザという「机の上」には、答えにつながるものを最初から置いていないわけです。
種明かしは、どのタイミングで起きるのでしょうか。
答えは「投票が完了して初めて、サーバーが正解・嘘の内容・解説を返す」です。コードにもそのままコメントを書いてあります——「投票が完了するまで、答えは一切出さない」と。
だから、先に答えをのぞくルートが存在しません。通信を盗み見ても、投票前のやり取りには答えが含まれていないからです。見抜いて、投票して、その結果としてはじめて、嘘の正体が明かされる。ゲームの順番が、そのまま設計の順番になっているわけです。
これは単なる不正対策ではない、と思っています。
クリシンクエストの価値は、正解を当てること自体ではなく、自分で問いを立てて見抜いていくプロセスにあります。もし答えが事前に取れてしまえば、その経験そのものが消えてしまいますよね。技術的な「渡さない」設計は、つきつめれば、学びの体験を守るための設計なのだと思います。
そしてこれは、画面の外でも同じではないでしょうか。答えを先に見てしまうと、人は「自分で考えたつもり」になってしまいます。AIの結論を読んだだけで分かった気になるのも、構造は同じです。答えへの近道をあえて塞ぐことが、考える力を育ててくれる。皮肉なようですが、本当だと感じています。
秘密を守る確実な方法は、暗号でも、見た目の細工でもありません。そもそも渡さないことです。プロンプトに「言うな」と書くより、AIに最小限しか教えない。JavaScriptで隠すより、ブラウザに最初から送らない。お願いではなく、構造で守る。
地味ですが、これがいちばん効きます。クリシンクエストの“公正さ”は、派手な仕掛けではなく、この一本の線引きの上に立っているんですね。
ここでも、私とAIの分担ははっきりしていました。コードを書くのはAIです。私の仕事は、「答えはクライアントに渡さない」という原則を決めること。そして——ここが大事なのですが——AIが書いた実装やAIの返答が、うっかり正解を漏らしていないかを、疑い、問い直して確かめることでした。AIは指示すれば素早く実装してくれますが、鵜呑みにはしません。出力を疑うのは、いつも人間(私)の役割です。皮肉なことに、それは「AIの答えを確認せず信じてしまう」という、このゲームが鍛えようとしている弱点そのものへの戒めでもあるんですね。もしあなたが、AIの答えをそのまま使ってしまいそうになったとき、この「一度疑ってみる」を思い出してもらえたらと思います。
次回は、このゲームを支える土台——個人開発で、どうやって本番環境をつくったか(Vercel と Supabase への移行記)を書きます。一緒に見ていきましょう。
— クリシンクエスト開発記・第2回