branch:
server.ts
3921 bytesRaw
import { openai } from "@ai-sdk/openai";
import { Agent, callable, routeAgentRequest } from "agents";

import { generateObject } from "ai";
import { z } from "zod";

type Player = "X" | "O";

type played = Player | null;

export type TicTacToeState = {
  board: [
    [played, played, played],
    [played, played, played],
    [played, played, played]
  ];
  currentPlayer: Player;
  winner: Player | null;
};

export class TicTacToe extends Agent<Env, TicTacToeState> {
  initialState: TicTacToeState = {
    board: [
      [null, null, null],
      [null, null, null],
      [null, null, null]
    ],
    currentPlayer: "X",
    winner: null
  };

  @callable()
  async makeMove(move: [number, number], player: Player) {
    if (this.state.currentPlayer !== player) {
      throw new Error("It's not your turn");
    }
    const [row, col] = move;
    if (this.state.board[row][col] !== null) {
      throw new Error("Cell already played");
    }
    const board: TicTacToeState["board"] = this.state.board.map((row) =>
      row.map((cell) => cell)
    ) as TicTacToeState["board"];
    board[row][col] = player;
    this.setState({
      ...this.state,
      board,
      currentPlayer: player === "X" ? "O" : "X",
      winner: this.checkWinner(board)
    });

    if (this.state.winner) {
      return;
    }
    // also return if the board is full
    if (this.state.board.every((row) => row.every((cell) => cell !== null))) {
      return;
    }

    // now use AI to make a move
    const { object } = await generateObject({
      model: openai("gpt-4o"),
      prompt: `You are playing Tic-tac-toe as player ${player === "X" ? "O" : "X"}. Here's the current board state:

${JSON.stringify(board, null, 2)}

Game rules and context:
- You are playing against ${player}
- Empty cells are null, X's are "X", O's are "O"
- Board positions are [row, col] from 0-2
- You need to respond with a single move as [row, col]
- Winning patterns: 3 in a row horizontally, vertically, or diagonally

Strategic priorities (in order):
1. If you can win in one move, take it
2. If opponent can win in one move, block it
3. If center is open, take it
4. If you can create a fork (two potential winning moves), do it
5. If opponent can create a fork next turn, block it
6. Take a corner if available
7. Take any edge

Analyze the board carefully and make the optimal move following these priorities.
Return only the [row, col] coordinates for your chosen move.`,
      schema: z.object({
        move: z.array(z.number())
      })
    });
    await this.makeMove(
      object.move as [number, number],
      player === "X" ? "O" : "X"
    );
  }

  checkWinner(board: TicTacToeState["board"]): Player | null {
    const winningLines = [
      // rows
      [
        [0, 0],
        [0, 1],
        [0, 2]
      ],
      [
        [1, 0],
        [1, 1],
        [1, 2]
      ],
      [
        [2, 0],
        [2, 1],
        [2, 2]
      ],
      // columns
      [
        [0, 0],
        [1, 0],
        [2, 0]
      ],
      [
        [0, 1],
        [1, 1],
        [2, 1]
      ],
      [
        [0, 2],
        [1, 2],
        [2, 2]
      ],
      // diagonals
      [
        [0, 0],
        [1, 1],
        [2, 2]
      ],
      [
        [0, 2],
        [1, 1],
        [2, 0]
      ]
    ];
    for (const line of winningLines) {
      const [a, b, c] = line;
      if (
        board[a[0]][a[1]] &&
        board[a[0]][a[1]] === board[b[0]][b[1]] &&
        board[a[0]][a[1]] === board[c[0]][c[1]]
      ) {
        return board[a[0]][a[1]] as Player;
      }
    }
    return null;
  }

  @callable()
  async clearBoard() {
    this.setState(this.initialState);
  }
}

export default {
  async fetch(request: Request, env: Env) {
    return (
      (await routeAgentRequest(request, env, { prefix: "some/prefix" })) ||
      new Response("Not found", { status: 404 })
    );
  }
} satisfies ExportedHandler<Env>;