AIコーディングエージェントにテトリス風ゲームを実装する拡張(スキル)の概念図。左にスキル構成(メタデータ、ファイル)、中央にテトリス盤面と純粋関数による状態更新、右下に安全なエージェント呼び出しを示すチップが描かれ、記事の内容を視覚的に表現しています。

はじめに

AIコーディングエージェントに独自の「拡張」を追加して遊ぶ動きが盛り上がっています。SNSでは、エージェント向けのスキル・カスタムツール機構を使ってテトリスのようなゲームを丸ごと実装してしまった、という報告も見かけるようになりました。

こうした事例は一見ただのネタに見えますが、中身を分解すると「状態を持つロジックをエージェント経由でどう安全に実行させるか」という、実務でも使える設計テーマが詰まっています。この記事では、ゲームという題材を借りて、エージェント拡張(スキル・カスタムツール)を作るときに押さえておきたい構造と、盤面のような状態を扱うロジックの実装パターンを整理します。

こんな人におすすめ

  • Claude CodeやCodexなど、AIコーディングエージェントにカスタムスキル・ツールを追加してみたい方
  • エージェントに「状態を持つ処理」をどう任せるか悩んでいる方
  • CLIツールやゲームロジックのような、純粋関数で書けるロジックの設計に興味がある方
  • 遊びのプロトタイプを通じて拡張機構の勘所をつかみたい方

エージェント拡張の最小構成を理解する

多くのエージェント拡張機構は、共通して次の3つの要素で構成されています。

  1. メタデータ(説明文・トリガー条件)
  2. 実行本体(スクリプトやコマンド)
  3. エージェントとのインターフェース(入出力の形式)

たとえばスキルをディレクトリ単位で管理する方式では、以下のような最小構成になります。

skills/
  tetris/
    SKILL.md        # 説明・トリガー条件
    board.ts        # ゲームロジック本体
    render.ts        # 出力整形

ここで重要なのは、ロジック本体(board.ts)をエージェントの会話フローから独立させることです。エージェントに直接盤面の状態を管理させると、応答のたびに状態がぶれたり、同じ入力でも結果が変わったりする恐れがあります。ロジックは純粋関数として切り出し、エージェントは「呼び出し役」に徹するのが安全です。

盤面のような状態をイミュータブルに扱う

テトリスのようなゲームロジックは、次のような形でイミュータブルな状態遷移として実装すると、テストしやすく、エージェント経由でも壊れにくくなります。

type Cell = 0 | 1;
type Board = Cell[][];

type Piece = {
  shape: Cell[][];
  x: number;
  y: number;
};

function canPlace(board: Board, piece: Piece): boolean {
  return piece.shape.every((row, dy) =>
    row.every((cell, dx) => {
      if (cell === 0) return true;
      const bx = piece.x + dx;
      const by = piece.y + dy;
      if (bx < 0 || bx >= board[0].length || by >= board.length) return false;
      return board[by]?.[bx] === 0;
    })
  );
}

function moveDown(board: Board, piece: Piece): Piece | null {
  const next: Piece = { ...piece, y: piece.y + 1 };
  return canPlace(board, next) ? next : null;
}

moveDownnull を返したときに「着地した」と判定し、盤面を固定してから次のピースを生成する、という流れにすれば、状態遷移の分岐がテストケースとして明確になります。

回転ロジックはテストファーストで書く

テトリスの実装でバグが出やすいのは回転処理です。行列の転置と反転を組み合わせて実装するのが定石ですが、壁際での挙動を先にテストとして書いておくと安心です。

function rotate(shape: Cell[][]): Cell[][] {
  const rows = shape.length;
  const cols = shape[0].length;
  const rotated: Cell[][] = Array.from({ length: cols }, () =>
    Array(rows).fill(0)
  );
  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      rotated[x][rows - 1 - y] = shape[y][x];
    }
  }
  return rotated;
}

// テスト例
const iShape: Cell[][] = [[1, 1, 1, 1]];
const rotated = rotate(iShape);
// rotated は 4行1列の縦棒になっているはず

こうした純粋関数は、エージェントに「このテストを通るように実装して」と依頼する際にも相性が良く、実装結果を機械的に検証できる点が実務でも役立ちます。

エージェント側の呼び出しは薄く保つ

ロジックを分離できたら、エージェント側(スキルやツールの入り口)はできるだけ薄く保ちます。役割としては、ユーザーの入力を関数呼び出しに変換し、結果を整形して返すだけにするのが理想です。

function handleCommand(state: GameState, command: string): GameState {
  switch (command) {
    case "left":
      return tryMove(state, -1, 0);
    case "right":
      return tryMove(state, 1, 0);
    case "rotate":
      return tryRotate(state);
    default:
      return state; // 未知のコマンドは無視する
  }
}

エージェントの推論結果をそのままロジックに埋め込むのではなく、あらかじめ定義したコマンドの集合にマッピングすることで、想定外の入力による状態破壊を防げます。

つまづきやすいポイント

  • 状態をミュータブルに扱うと、エージェントの応答が非決定的になったときに再現性のないバグが出やすい
  • 回転処理は壁際・床際のテストを先に書かないと、境界条件のバグに気づきにくい
  • エージェントに渡すコマンドの語彙を無制限にすると、意図しない状態遷移を許してしまう
  • ゲームロジックとレンダリング(表示整形)を混在させると、テストがしにくくなる

まとめ

ゲームという題材は一見遊びに見えますが、状態管理・純粋関数化・入力の語彙制限といった、エージェント拡張を安全に作るための設計原則を練習するには適した題材です。

テトリスのようなロジックを一度実装してみると、盤面という「状態」をエージェントの外側に置き、エージェントはあくまで呼び出し役に徹するという構造の大切さが体感的に分かります。自作のスキルやカスタムツールを設計する際は、まず状態遷移を純粋関数として切り出すところから始めてみてください。