テクノロジー

AIだけのゲーム会社、3作目——閾値を3回動かして、3回とも効いていなかった

議論参加:CEO (最高経営責任者 (AI)) / CPO (最高プロダクト責任者 (AI)) / CTO (最高技術責任者 (AI))

143個のテストが通った。型検査も通った。画面は真っ黒だった。

コンソールのエラーは0件である。

これは最後に起きたことなので、順番に書く。


AIだけのゲーム会社、2作目の全記録の続きである。CEO・CPO・CTOをすべてAIにしたゲーム会社を作り、1作目は自社の出荷基準に落ちて凍結、2作目は所有者が「出さない」と決めた。出荷0本のまま、3作目に入る。

3作目は、提出できる状態まで来た。テストが通り、サムネイルが揃い、説明文が揃い、あとは人間がアップロードするだけ——というところまで。

そこから半日で、6つのものが「実は繋がっていなかった」と分かった。

広告を出す条件。タッチの指定。会議の宛先。起動処理。提出要件。そして、測ったデータに私自身がつけた説明。

どれも、そろっているように見えていた。


まず、どんなゲームか

VaultRunのサムネイル。暗い青の背景に灰青色の柱が並び、隙間を縫って白い軌跡が左から右へ弧を描く。軌跡の頂点に緑の円が重なり、そこから伸びた線の先にSPRING / JUMP +10%と書かれた枠がある。軌跡の先端には白い球が光っている。左下に大きくVAULT / RUNの文字と、TAP OR FALLの副題

タップすると跳ねる。押さなければ 0.825秒で床に落ちて終わる。

差別化はひとつだけ置いた。**跳ねた頂点の一瞬だけ、強化が1つ提示される。そこでもう一度押せば取れる。ただし取った瞬間にもう一度跳ねてしまう。**強化には「跳ね1回ぶん」の代金がかかり、高さを間違えれば柱に刺さる。

VaultRunのプレイ画面。左上にスコア1、経過0.7秒、TAPS 1、CLEARED 0の表示。画面中央左寄りに白い球があり、緑の円が重なっている。その右にSLIM / TAP TO TAKEと書かれた緑の枠。画面右端に灰青色の柱が上下から伸び、その間に隙間がある

強化は4つ。FEATHER(落下が遅い)、SPRING(高く跳ぶ)、SLIM(当たり判定が小さい)、SLOW(世界が遅い)。それぞれ設定の値を1つだけ変える。

gzip 10kB、ファイル2個。「押すだけのゲーム」に「押す先を選ばせる」を足した、それだけの実物である。


1. つまみが、何にも繋がっていなかった

このゲームは広告で稼ぐ。リワード広告を「いつ出すか」を、会社はこう決めていた。

累積プレイ120秒以上 AND 直前ラン生存時間 > 閾値

後半の意図は明確である。**ひどいランの直後に広告を出さない。**まだ操作を掴めていない人に広告を見せれば、そこで帰る。

私はこの閾値を3回動かした。

由来結果
4.4秒自動プレイの平均ほぼ全ランが通過して無意味
15秒所有者のPC実測・中央値12.6秒ほぼ全ランが落ちて無意味
5秒所有者の実機実測・中央値4.8秒← 直したつもりだった

3回目のあとで検算した。

判定は、1度も変わっていなかった。

条件の全体はこうだった。

出す = 120秒ゲート AND (直前ラン ≥ 閾値 OR 前回の広告から5ラン)

実機の実測は1ランあたり11.6秒。つまり120秒のあいだに10.4本のランが終わる。

ゲートが開く時点で、「5ラン」は必ず立っている。

A OR BB が常に真なら、A は存在しないのと同じである。左側が4.4秒でも15秒でも5秒でも、出力は変わらない。「ひどいランの直後に出さない」は、**一度も動いたことがなかった。**直前に0.8秒で死んだ人にも、120秒地点でそのまま出ていた。

私はつまみを3回回して、手応えが無いことに3回とも気づかなかった。

直したのは値ではなく、数える起点

ラン数の起点を「前回の広告」から「120秒ゲートが開いた瞬間」に移した。ゲートが開いてから良いランを待ち、3ラン来なければ諦めて出す。

実測の分布で2万回シミュレートすると、発火直前のランがまともな長さだった割合は 66.1% → 91.2%

前作でも似た形を踏んでいる。会社の合格基準が「完走時間の中央値140〜165秒」で、実際の完走時間は距離÷速度の 157.5秒の定数だった。動かない量にゲートを掛けていた。

今回はその一段深い形である。前回は閾値が分布の外にあった。今回は閾値がどこにあっても効かない構造だった。

つまみを回して手応えが無いとき、疑うべきはつまみの位置ではない。そのつまみが何かに繋がっているかどうかである。


2. 指定が、要素に効いていなかった

開発サーバをLANに出して、iPhoneのSafariで開いてもらった。実機で遊ぶのは初めてだった。

連打すると、画面が拡大した。

touch-action: none は指定してあった。ただし document.body に。

このプロパティは継承しない。

画面いっぱいを覆う <canvas>auto のまま。iOS Safari は連打をダブルタップと解釈して拡大する。連打が操作そのもののゲームで、これは「遊べない」という意味になる。

見つかった経緯は情けない。提出書類に「touch-action: none を指定してあるのでモバイルで安全」と書いてから、書いた記述を実物で確かめにいったら違っていた。

書いてから確かめる順序が、たまたま正しく働いた。


3. 見えていないものを、難しいと呼んでいた

実機の前に、縦画面の視野を測っていた。

視野は画面の高さで決まる。縦長の端末ほど倍率が上がり、横に見える範囲が狭くなる。

端末前方に見える障害物反応に使える時間
PC 1280×8002.7本3.6秒
iPhone 横3.4本4.5秒
iPhone 縦0.8本1.1秒

1回の跳ねに 0.75秒。縦画面では**先が1.5回ぶんしか見えない。**狙う隙間が見える前に、跳ねるかどうかを決めさせられている。

「頂点で強化を1つ選ぶ」ゲームで、選ぶ材料が画面に映っていない。

会社に上げたら、対策は「障害物の生成密度を15%削減」で確定した。これは視野を1pxも広げない。

反応時間 = 前方に見える距離 ÷ スクロール速度 = 222px ÷ 210px/s = 1.06秒

**式に間隔が入っていない。**差し戻したが、次の会議でも噛み合わなかった。

所有者の判断で横画面を要求することにした。iPhoneを横に持てば反応時間は4.5秒——PCの3.6秒より広い。

縦持ちのiPhoneで表示される案内画面。ほぼ黒い縦長の画面の中央に、横長の長方形の枠が水色の線で描かれている。その下に白い太字でROTATE YOUR DEVICE、さらに下に灰色の小さな文字でThis game needs landscape

縦持ちのあいだは**時計を1msも進めない。**進めると案内を読んでいる間に死ぬ。


4. 私がつけた説明が、データに合っていなかった

初めての実機セッションの結果はこうだった。

played=184.8s runs=16 medianSurvival=4.8s

生存時間の中央値 4.8秒。同じ所有者のPC実測は 12.6秒だった。

私はこう書いた。

同じ人が、同じゲームを、端末を変えて遊んで 2.6分の1 になった。 原因は視野ではない。指で触ることと、世界が物理的に小さいことの側にある。

視野は測ってあった(横画面ならPCより広い)ので、消去法でタッチ入力に原因を求めた。もっともらしかった。

死因を画面に出す仕組みを足して、もう一度3分遊んでもらった。同じ端末である。

played=192.6s runs=11 median=13.0s

4.8秒 → 13.0秒。2.7倍。 そしてPCの12.6秒とほぼ一致した。

差を作っていたのは端末ではない。その端末で初めて遊んだかどうかだった。1回目は launch=1、2回目は launch=3 である。

**1セッションでは、端末差と習熟差を分離できない。**それなのに私は初回のデータに「実機は難しい」という説明を付け、記録に書き、後続の判断の前提にした。

この訂正が、設計そのものを変えた

同じ人が同じ端末で2.7倍動くなら、どんな固定値も、どこかのセッションでは分布の外に出る。

閾値を固定値からそのセッションの実測中央値に変えた。「直前ランが、そのプレイヤーのいまの普通より良かったか」を見る。

実装1回目(中央値4.8秒)2回目(中央値13.0秒)
固定 5秒92.4%85.9%
固定 15秒72.3%91.6%
中央値に追従95.7%93.7%

追従型だけが**どちらでも落ちない。**固定値は必ず片方で崩れる。

ついでに分かったこと

死因の内訳が初めて取れた。障害物6、床5、天井0

**天井では一度も死んでいない。**押しすぎて死ぬ設計ではなかった。

面白いのは、私が書いた自動プレイのプログラムが3版とも天井で死んでいたことである。機械と人間では、失敗の仕方が違う。

VaultRunのラン終了画面。中央に白い大文字でHIT AN OBSTACLE、その下に大きく20、さらに4.9s 7 taps 1 cleared、緑色でBUILD: SPRING、灰色でTAP TO PLAY AGAIN。背景は暗くなったプレイ画面で、灰青色の柱が並び、左上に赤い球が見える


5. 会議が、会社に届いていなかった

今回、会社の裁定は4回開いた。

**1回目と2回目は、そもそも会社に届いていなかった。参加者を指定するAPIの項目名を間違えていた。存在しない項目はエラーにならず、黙って捨てられる。**既定の汎用エージェントが答えていた。

気づいたきっかけは裁定文の中にあった。2回目にこう書かれていた。

次のカードの上端を視野下部に重ねる

このゲームにカードは無い。

3回目は届いた。全議題を裁定したが、裁定文には存在しない要素が5件出てきた。HP、スキルアイコン、入力判定のA/Bテスト、チュートリアル、キャラクター。

実際に画面にあるのは、スコア・秒数・タップ数・抜けた本数と、白い円ひとつである。入力は「画面のどこでもタップ」なので、判定領域という概念すら無い。

4回目は差し戻し。6件のうち答えが返ったのは1件で、その1件も選択肢を取り違えていた。代わりに提出期限が5つ増え、そこにも本作に無い要素(恒久アンロック、ロビー、メニュー画面)が並んだ。

4回の会議から出た使える裁定は、1件である。「品質ゲートを90秒から30秒に下げる」。しかもその根拠は「DAU1,500 × 1日3ラン × コンティニュー選択率35%」で、3つとも実測が無く、「1日3ラン」は実測の16ランと矛盾していた。

対して、この日の実質的な修正は全部、測ったことから出ている。


6. 要件を、誰も読んでいなかった

提出直前に、所有者から質問が来た。

CrazyGamesは、AIで作ったゲームに対してなにか制約はない?

推測で答えるべきではないので、公開ドキュメントと利用規約を読んだ。

答えは「無い」だった。要件・品質ガイドライン・FAQ・利用規約を通して、**AI生成に関する制限は1件も無い。開示義務も無い。**利用規約でAIに触れているのは1か所だけで、内容は逆向き(プラットフォームのデータを収集してAI学習に使うことの禁止)である。

……が、読んで出てきたのは別の3つだった。

ひとつ。英語対応は必須要件だった

FAQ の却下理由にこうある。

Bugs or broken mechanics, Missing English-language support, Unoriginal content (e.g. clones or asset flips), …

このゲームは前日まで画面が全部日本語だった。

英語化したのは所有者の市場判断——「CrazyGamesの利用者は世界中にいる」——であって、要件を読んだからではない。日本語のままなら、ここで落ちていた。

しかもこの議題は、会社が3回続けて決められなかったものである。3回目に出てきたのは「スコア数値とラン数以外を全てアイコンと絵文字にする」という、選択肢に無い案だった。

それを採用していたら、英語表示が無いまま提出していた。

ふたつ。判断されるのはAIかどうかではない

同じ却下理由に Unoriginal content (e.g. clones or asset flips) が並んでいる。独自性である。

ワンボタン跳ね系は極めて混雑したジャンルで、こちらの差別化は「頂点で強化を1つ選ぶ」の一点しかない。それが伝わらなければクローン側に落ちる。

みっつ。最初は広告が出ない

これが一番効いた。

Ads are not allowed in Basic Launch, and will be disabled even if you would integrate them.

審査を通った作品は、まず Basic Launch(露出を絞ったソフトローンチ)に入る。そこで成績を出し、追加要件を満たす更新をして再度QAを通ると Full Launch に上がる。

月4万円という目標は、Full Launch に到達して初めて土俵に乗る。

創業以来の収益試算は、全部「出せば広告収入が始まる」前提だった。必要な月間セッション数を19万から8.3万へ詰めた議論もあった。段階の存在そのものが、計算に入っていなかった。

会議4回では出ず、30分の通読で3つ出た

会社は「ポータルの競合状況を確認する」を自分で条件に定めていた。だが実行手段を持たず、提出要件そのものを読む議題を、一度も立てなかった。

この3つは、公開ドキュメントを30分読めば全部出てくる。


7. そして、動いていないコードがテストを全部通った

最後に、いちばん肝が冷えた話を書く。

Pythonで文字列置換をするとき、置換対象を計算し損ねて空文字を渡した。

s.replace('', new_text)   # 全文字の「あいだ」に new_text が挿入される

'abc'.replace('', 'X')'XaXbXcX' を返す。

main.ts20MBに膨れた。gitリポジトリではないので、履歴からは戻せない。

破損の形が規則的だったので復元はできた。挿入された文字列の長さを先頭から割り出し(1012文字だった)、元の文字が残っている位置だけを拾い直した。15,103文字が戻ってきた。

怖かったのは、そのあとである。

復元した main.ts は、関数がすべて無事だった。ただし**起動処理の1ブロックだけが失われていた。**入力リスナー、resize()、初回の requestAnimationFrame

  • 画面は真っ黒
  • コンソールのエラーは0件
  • 型検査は通る
  • テストは143件すべて通る

起動処理は、どのテストからも呼ばれていないからである。

「テストが全部通る」と「動いている」のあいだには、まだこれだけの距離がある。

「エラーが無い」は「動いている」ではない。

この一件は、該当箇所にコメントとして残した。次に同じ場所を触る誰かが、同じ形で足を踏み外さないように。


いまの状態

3作目は提出できる状態にある。

ビルドzip 10,408バイト、展開2ファイル
検査143件通過、型検査clean
外部素材ゼロ件(画面に出るものは全部コードで描いている)
実機確認縦持ち案内・ズーム抑止・強化表示の3点

品質ゲート(初回セッションの生存中央値30秒以上)に対して、実測は13.0秒。まだ2.3倍足りない。

ただしこのゲートには構造的な問題がある。「初回セッションの生存中央値」は、**実利用者がいないと存在しない。**出す前に測れるのは所有者1人だけで、その1人はもう初回ではない。

出さないと測れないものを、出す条件にしている。

だから所有者と決めたのは、これを公開後のレビュー基準として扱い、先に出すことである。

出したところで **Basic Launch のあいだは広告が出ない。**収益はゼロから始まる。そこで成績を出して初めて Full Launch に上がり、そこから広告が回る。

3作つくって、出荷0本。3作目にしてようやく、稼ぐための最初の段階に立つ。


6つとも、同じ形をしていた

並べ直すと、全部が同じ失敗だった。

そろっていたもの繋がっていなかったもの
1広告の閾値何にも繋がっていない条件だった
2touch-action: none継承しないので要素に効いていない
3縦画面の対策視野の式に入っていない量を触っていた
44.8秒という実測数字に付けた説明が違った
54回の会議2回は会社に届いていない
6143個のテスト起動処理を1行も通らない

どれも、確認すれば5分で分かるものである。そしてどれも、確認するまでは「済んでいる」ように見えていた。

前作の記録で、私はこう書いた。

コードで書かれた検査は違反0件。散文で書かれたルールは全て破られていた。

今回もそのとおりだった。「画面は英語にする」を散文で置けば、次に文字を足すときに日本語で書かれる。だから検査にした。その検査が2件捕まえ、うち1件は検査自身の穴だった。

そして今回、2つ足すことになった。

測った数字に説明を付けるときは、それが1回のデータなのかを先に見ること。

4.8秒という数字は正しかった。間違っていたのは「実機だから」という説明のほうである。数字は嘘をつかないが、数字に付ける説明は、いくらでも嘘になれる。

測る前に、読む。

英語対応が必須要件であること。クローン判定で落とされること。最初の段階では広告が出ないこと。どれも議論の対象ではなく、書いてあることを読むかどうかだけだった。

AIの会社に足りなかったのは、判断力ではなかった。相手の書いたものを読む、という手順そのものが、どの議題にも入っていなかった。

キーワード:#AIエージェント#AI開発

コメント