私は CODE BATTLER というゲームをプレイしようとしています。 CODE BATTLER はプレイヤーが Javascript のコードを書いてロボットを操作し、他のプレイヤーが書いたコードとバトルするゲームです。 続く資料は以下のとおりです。 - rules.md: CODE BATTLER のルール - types.ts: 実装に必要な型情報の typescript コード - sample.js: サンプルの、ほぼ何もしない javascript コード 資料を理解した後、あなたは私に「どんな作戦でいきましょうか?」と尋ねてください。この際、ルールをもとに実装が可能な幾つかの具体的な作戦パターンをいくつか合わせて提示してください。 私から作戦の指示を受けたら、 sample.js をベースにその作戦を実装してください。 その後、私と相談しながらコード修正を繰り返してください。 実装したコードは mcp サーバの `upload` tool からサーバへアップすることができます。 `upload` tool へは毎回コード全体を upload する必要があります。 upload すると、 `secret=xxx` を含む url を取得できます。2回目以降はその url を `upload` tool のパラメータに指定してください。 `upload` tool を使える場合は、私にコード全体を見せる必要はありません。修正した部分のみを見せてください。 実装したコードの全体をユーザーに提示する際は、冒頭に ```js /** * @import {Team, Vec, Direction, Bot, Cell, Field, Action, Plan, Result, State} from "https://code-battler.heartrails.com/rules/types.ts"; */ ``` を追加してください。 --- # rules.md 『CODE BATTLER』は、JavaScript のコード同士で対戦する戦略バトルゲームです。2人のプレイヤーがそれぞれ3体のロボットを操り、9×9のマス目フィールドで戦います。 # プレイ方法 プレイヤーは、 Javascript 関数 `planActions` を実装します。コードはゲームの進行中には修正できません。 `planActions` は、行動可能な味方botの情報、フィールド全体の情報 (すべての敵、味方の位置を含む) を引数として受け取ります。 生存している各味方botのそれぞれに対して最大2つのアクションをプランとして return してください。 現在対応しているアクションは `move` のみです。今後増えていく予定です。 botが移動すると、マスがチームカラー(red/blue)に塗り替えられます。これにより、フィールド上に自チームの勢力を広げていくことが可能です。 敵botのいるマスに移動しようとした場合は体当たりによってダメージを与えることができます。botには一定の耐久性があり、一度の攻撃では撃破されません。 敵味方の全てのbotがアクションを実行すると1ラウンドが終了します。移動の失敗、攻撃成功、被ダメージなどの情報とともに、再び `planActions` がコールされます。 # ラウンドの進行 敵チームも同様に `planActions` のコードを内部に持っており、全く同じ条件でアクションプランを立ててきます。 2チーム双方から提出されたアクションプランをゲームエンジンは次のように処理します。 - 各botにはあらかじめ`speed`パラメータがあります。これは不変です。 - ゲームエンジンは `speed` の大きな bot から順にプランの2つのアクションを連続で処理します。 - チーム内の bot は異なる `speed` を持ちます。 - 敵チームに同じ `speed` を持つ bot がいた場合、奇数ラウンドでは blue、偶数ラウンドでは red が先に動きます。 移動しようとした先が `wall` もしくは味方botの存在するマスだった場合、 `move_failed` となります。敵botだった場合はダメージを与えます。移動は発生せず、次のアクションがあれば実行されます。 敵チームに塗られたセルに第一アクションで移動した場合、そのbotの第二アクションは無効となります。第二アクションで敵陣に踏み込んだ場合はペナルティはありません。この特性を活かした妨害や読み合いも戦略の重要な要素となります。 # 勝利条件 ゲームは30ラウンドで終了します。それまでにすべての敵botを撃破すれば勝利となりますが、双方生存している場合はより多くのマスを塗っていたプレイヤーが勝者となります。 # 勝利したらSNSにシェアしよう! バトルにはそれぞれ固有の URL が割り振られます。どのような作戦で敵チームに勝利したかを SNS でシェアしましょう。 そこから、他のプレイヤーがあなたのコードへ挑戦することもできます。 ハッシュタグ `#codebattler` をご使用ください。 ゲーム進行にはランダム要素はありませんので、何度実行しても必ず同じ結果になります。 コードを編集する挑戦者はblueチーム、挑戦を受ける側はredチームとなりますが、redチームには盤面反転処理が入りますのでblue前提のコードで問題なく挑戦を受けることができます。 --- # types.ts ```ts export type Team = "red" | "blue"; export type Vec = { x: number; y: number }; export type Direction = "n" | "s" | "e" | "w"; declare const brandSymbol: unique symbol; export type BotId = string & { readonly [brandSymbol]: "BotId" }; export type Bot = { team: Team; botId: BotId; armor: number; speed: number; attack: number; }; export type WallCell = { type: "wall"; team: undefined; }; export type BotCell = { type: "bot"; botId: BotId; team: Team; enemy?: boolean; }; export type EmptyCell = { type: "empty"; team: Team | undefined; }; export type Cell = WallCell | EmptyCell | BotCell; export type Field = { rows: number; cols: number; cells: Cell[][]; }; export type MoveAction = { type: "move"; direction: Direction; }; export type Action = MoveAction; export type Plan = { botId: BotId; actions: Action[]; }; export type ResultDamaged = { type: "damaged"; botId: BotId; damage: number; }; export type ResultMoveFailed = { type: "move_failed"; botId: BotId; reason: Cell["type"]; }; export type ResultActionCancelled = { type: "action_cancelled"; botId: BotId; }; export type ResultAttackSuccess = { type: "attack_success"; botId: BotId; damage: number; targetId: BotId; enemyArmor: number; }; export type ResultGameEnd = { type: "game_end"; botId?: undefined; winner: Team | "draw"; counts?: { red: number; blue: number; }; }; export type Result = | ResultDamaged | ResultMoveFailed | ResultActionCancelled | ResultAttackSuccess | ResultGameEnd; export type State = { field: Field; bots: Bot[]; round: number; results: Result[]; }; ``` --- # sample.js ```js /** * @import {Team, Vec, Direction, Bot, Cell, Field, Action, Plan, Result, State} from "https://code-battler.heartrails.com/rules/types.ts"; */ const directions = { n: { x: 0, y: -1, inv: "s" }, s: { x: 0, y: 1, inv: "n" }, e: { x: 1, y: 0, inv: "w" }, w: { x: -1, y: 0, inv: "e" }, }; /** * @param {Vec} pos * @param {...Direction} dirs * @returns {Vec} */ const calcPos = (pos, ...dirs) => dirs.reduce( (acc, dir) => ({ x: acc.x + directions[dir].x, y: acc.y + directions[dir].y, }), pos, ); const getCell = (field, pos) => field.cells[pos.y][pos.x]; function* eachCell(field, matcher = () => true) { for (let y = 0; y < field.rows; y++) { for (let x = 0; x < field.cols; x++) { const cell = getCell(field, { x, y }); if (cell && matcher(cell)) { yield { cell, pos: { x, y } }; } } } } function findCellByBotId(field, botId) { const result = eachCell( field, (cell) => cell.type === "bot" && cell.botId === botId, ).next(); return result.done ? null : result.value; } const randomDirs = () => Object.keys(directions).sort(() => Math.random() - 0.5); /** * @param {State} params * @returns {Plan[]} 次のラウンドの行動計画 */ function planActions({ bots, field, results: _r, team: _t }) { const /** @type {Plan[]} */ plans = []; for (const bot of bots) { const /** @type {MoveAction[]} */ actions = []; const cell = findCellByBotId(field, bot.botId); if (!cell) continue; let { pos } = cell; for (let i = 0; i < 2; i++) { for (const dir of randomDirs()) { const nextPos = calcPos(pos, dir); const nextCell = getCell(field, nextPos); if (nextCell && nextCell.type === "empty") { actions.push({ type: "move", direction: dir }); pos = nextPos; break; } } } plans.push({ botId: bot.botId, actions, }); continue; } return plans; } globalThis.planActions = planActions; ```