【AI Studio】Gemini 3でJSロジック同士を戦わせるゲーム作ってみた
【🎄Merry Christmas🎄 WWWAVE アドベントカレンダー 12/4の記事です】
皆様こんにちわ
この記事を開いてくれてありがとうございます
今回は11月に導入されたGemini3の凄さを少し試してみようかと思います
AIStudioを使ってJSロジック同士を戦わせるゲームを作る
プレイヤーがNPCの動きをJavascriptで書いて戦わせるゲーム
これは前バージョンで作ろうとしてなかなか上手く行かなかったものです
ユーザーがコードを書く場所など細かい要件が伝わらず失敗した記憶があります
今回Gemini3を使って再チャレンジします!!
プロンプトは下記です(ちなみにこれもGemini3に纏めてもらいました)
プロンプト
AI Studio用要件定義書:プログラミング対戦ゲーム「Code Arena」
ゲーム概要
プレイヤーがJavaScriptでキャラクター(NPC)の思考ロジックを記述し、2〜4人のプレイヤーがアリーナで戦う、トップダウン視点の2D対戦アクションゲーム。最後まで生き残ったプレイヤーが勝利となる。
ゲーム進行とターンシステム
ゲームはリアルタイムではなく、厳密なターン制で進行する。
最大ターン数: ゲームが膠着しないよう、最大ターン数(例: 100ターン)を設ける。これに達した場合、引き分けとする(または生存者全員勝利)。
ターンの流れ:
毎ターン、生きている全てのキャラクターの思考ロジック(JS関数)が順番に実行される。
エネルギーチャージボーナス: 前のターンに「エネルギーを貯める」を選択したキャラクターは、このターンにロジックが連続で2回実行される。
全員の行動決定後、ゲームエンジンがアクションの結果(移動、攻撃のヒット判定など)を一括で処理し、盤面を更新する。
キャラクターのアクション
キャラクターは毎ターン、思考ロジックの結果として、以下のいずれか1つのアクションを選択する。
移動: 上下左右いずれか1マス移動する。障害物や他のキャラがいる場合は移動できない。
ビーム発射: 向いている方向(または指定方向)に一直線にビームを撃つ。射線上のキャラクターは即座に消滅する。壁は貫通しない。
地雷設置: 自分の現在位置に地雷を設置する。
エネルギーを貯める: そのターンは何も行動しないが、次のターンに2回行動できる権利を得る。
重要なゲームルール(情報格差と消滅)
消滅条件: ビームに当たる、または地雷が設置されているマスを踏むと即座に消滅(敗退)する。
地雷の情報秘匿(最重要):
画面表示(ビューワー): 観戦者が見るゲーム画面上には、全ての地雷が表示される。
プログラムへの情報提供(API): プレイヤーの思考ロジックに渡される盤面情報において、「他人が設置した地雷」の情報は完全に削除される。 自分の設置した地雷の位置のみ把握できる。
プログラミングインターフェース(API)の仕様
プレイヤーのJSコードは、サンドボックス化された環境で実行される。ゲームエンジンは、プレイヤーのコードに以下の情報を渡し、アクションの返却を期待する。
プレイヤーのコードが受け取る情報 (context オブジェクト):
turnCount: 現在のターン数。
me: 自身の情報(位置座標 {x, y}、残エネルギー状態、自分が設置した地雷の座標リスト)。
enemies: 敵キャラクターのリスト(それぞれの位置座標 {x, y})。※敵の地雷情報は含まない。
obstacles: 壁などの障害物の座標リスト。
mapParams: マップのサイズ情報。
プレイヤーのコードが返す情報:
アクション命令(例: { action: "move", direction: "up" } や { action: "charge" } など)。
エラーハンドリングと「エンスト」
プレイヤーが記述したコードの実行時に問題が発生した場合、そのキャラクターは「エンスト」状態となる。
対象となるエラー:
無限ループ(実行タイムアウトで検知)。
文法エラーや実行時例外(try-catchで検知)。
不正なアクション命令の返却。
エンストの結果: そのターン、キャラクターは何も行動しない。
視覚表現: エンストしたキャラクターは、煙が出る、色が灰色になるなどの視覚的なフィードバックを行う。
開発ステップの指示(AIへの依頼事項)
まずは、以下の優先順位でプロトタイプを作成してください。
HTML Canvasを使ったグリッドベースのマップと、四角いキャラクターの描画。
基本的なターン進行システムと、ランダムに移動するだけのダミーBotの実装。
プレイヤーコードへの情報受け渡し(API)と、上記の「地雷の情報秘匿」ロジックの実装。
...すると2分くらいで作成されました!!
プレビューはこんな感じです

控えめに言って超進化してます...
右下の Quick API Reference なんてプロンプトでは言及してないのに追加してくれています
これはどうゆうアプリでどう遊ぶのかを完全に理解してくれているということです
しかもユーザーにコードを書かせる部分にもGeminiが使えるようになっています...!!(至れり尽くせり!!)
気になったのはコード上4人キャラクターまで考慮されてますがviewには追加する項目がないことくらいでしょうか。
適当なロジックをgeminiにつくってもらって動かすとこんな感じです

そして...
ついでなので僕と勝負させます!!!!
プレイヤー1のロジックは頑張って自分で書いてみます
プレイヤー2はGeminiに書かせます("最強のロジックを書いてください")
プレイヤー1 (自分で書いた) ※見ないで!
const me = context.me;
const targetEnemy = context.enemies[0];
const dx = context.enemies[0].x - me.x;
const dy = context.enemies[0].y - me.y;
let direction = null;
if (dx > 0) {
direction = "right";
} else if (dx < 0) {
direction = "left";
} else if (dy > 0) {
direction = "down";
} else if (dy < 0) {
direction = "up";
}
return { action: "beam", direction: direction };
プレイヤー2 (Gemini作成)
const isValidPosition = (x, y, mapParams, obstacles) => {
if (x < 0 || x >= mapParams.width || y < 0 || y >= mapParams.height) {
return false; // Out of bounds
}
for (const obs of obstacles) {
if (obs.x === x && obs.y === y) {
return false; // Is an obstacle
}
}
return true;
};
const getValidMoveDirections = (me, mapParams, obstacles) => {
const directions = [];
const possibleMoves = {
"up": { x: me.x, y: me.y - 1 },
"down": { x: me.x, y: me.y + 1 },
"left": { x: me.x - 1, y: me.y },
"right": { x: me.x + 1, y: me.y }
};
for (const dir in possibleMoves) {
const targetPos = possibleMoves[dir];
if (isValidPosition(targetPos.x, targetPos.y, mapParams, obstacles)) {
directions.push(dir);
}
}
return directions;
};
const isEnemyOnSameAxis = (me, enemies) => {
for (const enemy of enemies) {
if (enemy.x === me.x || enemy.y === me.y) {
return true;
}
}
return false;
};
const getBeamDirectionIfAligned = (me, enemies) => {
for (const enemy of enemies) {
if (enemy.x === me.x) { // Same column
if (enemy.y < me.y) return "up";
if (enemy.y > me.y) return "down";
}
if (enemy.y === me.y) { // Same row
if (enemy.x < me.x) return "left";
if (enemy.x > me.x) return "right";
}
}
return null; // No enemy aligned
};
const me = context.me;
const enemies = context.enemies;
const obstacles = context.obstacles;
const mapParams = context.mapParams;
// Strategy:
// 1. If charged: Prioritize beaming an enemy. If no enemy to beam, move randomly.
// 2. If not charged: Check if an enemy is on the same row/column. If so, charge.
// 3. Otherwise (not charged, no enemy on axis): Move randomly.
if (me.isCharged) {
const beamDir = getBeamDirectionIfAligned(me, enemies);
if (beamDir) {
return { action: "beam", direction: beamDir };
} else {
// No enemy to beam, so move randomly to use one of the charged moves
const validMoves = getValidMoveDirections(me, mapParams, obstacles);
if (validMoves.length > 0) {
const randomDir = validMoves[Math.floor(Math.random() * validMoves.length)];
return { action: "move", direction: randomDir };
}
// Fallback if stuck even when charged, though unlikely
return { action: "charge" };
}
} else if (isEnemyOnSameAxis(me, enemies)) {
// Enemy is on the same axis, so charge up for a future beam
return { action: "charge" };
} else {
// No charge, no enemy on axis, so move randomly
const validMoves = getValidMoveDirections(me, mapParams, obstacles);
if (validMoves.length > 0) {
const randomDir = validMoves[Math.floor(Math.random() * validMoves.length)];
return { action: "move", direction: randomDir };
}
// Fallback if stuck and cannot move
return { action: "charge" };
}
果たして結果は!!
.
.
.
.

僕の勝ち!!
わはは!笑
相手方向にビームを撃ち続ければ負けることはないというクソゲーなのだ!
そこに気付けないようじゃ まだまだ成長が足りんな!
...ごめんなさい
クソゲーなのは僕のせいです
連続してビームできる仕様がゴミでした
まとめ
Gemini 3は「別次元」になっていた
APIリファレンスを勝手に作ってくれたり、複雑な要件を推論で補完して実装する力は、明らかに以前のバージョンとはレベルが違います
しかし「ビーム連打のクソ仕様」は回避できないので
人間も精進しましょう
株式会社ウェイブのエンジニアによるテックブログです。 弊社では、電子コミック、アニメ配信などのエンタメコンテンツを自社開発で運営しております! wwwave.jp/service/
Discussion