> ## Documentation Index
> Fetch the complete documentation index at: https://openturn.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Add an AI bot to a game

> Drop a computer opponent into any seat — random, heuristic, or search-based — without changing the engine or the game logic.

You have an authored game and you want a player to be able to play against the computer. The bot layer is built for exactly this. Five lines of bot code, three lines of wiring.

## Step 1 — Add the `legalActions` hook (one time, per game)

The bot runtime needs to know which moves a seat may legally play. Add an enumerator to `defineGame`:

```ts {5-18} theme={null}
import { defineGame, turn } from "@openturn/gamekit";

export const ticTacToe = defineGame({
  maxPlayers: 2,
  setup: () => ({ board: [[null, null, null], [null, null, null], [null, null, null]] }),
  legalActions: ({ G, derived }, playerID) => {
    if (!derived.activePlayers.includes(playerID)) return [];
    const out = [];
    for (let row = 0; row < 3; row += 1) {
      for (let col = 0; col < 3; col += 1) {
        if (G.board[row]![col] === null) {
          out.push({ event: "placeMark", payload: { row, col }, label: `(${row},${col})` });
        }
      }
    }
    return out;
  },
  turn: turn.roundRobin(),
  moves: ({ move }) => ({ /* ... */ }),
  views: { /* ... */ },
});
```

The engine never reads this field. Only the bot runtime does. Authors who don't ship bots can omit it.

If you cannot or do not want to modify the game definition, a bot can ship its own enumerator via the `enumerate` field — see [Reference: bot](/docs/reference/bot#legal-action-enumeration).

## Step 2 — Define the bot

```ts theme={null}
// bots/random.ts
import { defineBot } from "@openturn/bot";
import type { ticTacToe } from "../game";

export const randomBot = defineBot<typeof ticTacToe>({
  name: "random",
  decide: ({ legalActions, rng }) => rng.pick(legalActions),
});
```

`rng` is forked from the snapshot's RNG and salted by bot name + seat + turn, so two bots on the same snapshot get different (but reproducible) streams.

## Step 3 — Attach to a session

For a single bot opponent:

```ts theme={null}
import { createLocalSession } from "@openturn/core";
import { attachLocalBots } from "@openturn/bot";
import { ticTacToe } from "./game";
import { randomBot } from "./bots/random";

const raw = createLocalSession(ticTacToe, { match: { players: ticTacToe.playerIDs } });

const { session, isBot, whenIdle, detachAll } = attachLocalBots({
  session: raw,
  game: ticTacToe,
  bots: { "1": randomBot },           // seat "1" is the computer
});
```

**Use the returned `session`, not `raw`, in your game loop.** The facade notifies the runner on every dispatch.

## Step 4 — Drive the loop

```ts theme={null}
while (true) {
  const snapshot = session.getState();
  if (snapshot.meta.result !== null) break;

  const active = snapshot.derived.activePlayers[0]!;

  if (isBot(active)) {
    await whenIdle(active);             // wait for the bot to dispatch
    continue;
  }

  // human input...
  const move = await readMoveFromStdin();
  session.applyEvent(active, "placeMark", move);
}

detachAll();
```

`whenIdle(playerID)` resolves once the runner has finished thinking and dispatched (or errored). The bot watches the snapshot autonomously — you do not call `decide` yourself.

## Recipes

### Heuristic bot — score and sort

```ts theme={null}
export const heuristicBot = defineBot<typeof ticTacToe>({
  name: "heuristic",
  decide({ legalActions }) {
    const score = (a) => {
      const { row, col } = a.payload;
      if (row === 1 && col === 1) return 3;       // center
      if (row !== 1 && col !== 1) return 2;       // corners
      return 1;                                    // edges
    };
    return [...legalActions].sort((a, b) => score(b) - score(a))[0]!;
  },
});
```

### Search-based bot — alpha-beta minimax

For deterministic optimal play, recurse with `simulate`:

```ts theme={null}
import { defineBot, simulate } from "@openturn/bot";
import { ticTacToe } from "../game";

const OTHER: Record<string, string> = { "0": "1", "1": "0" };

function score(snapshot, me) {
  const r = snapshot.meta.result;
  if (r?.draw) return 0;
  if (r?.winner === me) return 10;
  if (r?.winner) return -10;
  return null;     // non-terminal
}

function search(snapshot, toMove, me, depth, alpha, beta) {
  const terminal = score(snapshot, me);
  if (terminal !== null) return terminal - Math.sign(terminal) * depth;

  const moves = enumerate(snapshot, toMove);
  if (moves.length === 0) return 0;

  if (toMove === me) {
    let best = -Infinity;
    for (const m of moves) {
      const sim = simulate(ticTacToe, snapshot, toMove, m);
      if (!sim.ok) continue;
      best = Math.max(best, search(sim.next, OTHER[toMove], me, depth + 1, alpha, beta));
      alpha = Math.max(alpha, best);
      if (alpha >= beta) break;
    }
    return best;
  }
  let best = Infinity;
  for (const m of moves) {
    const sim = simulate(ticTacToe, snapshot, toMove, m);
    if (!sim.ok) continue;
    best = Math.min(best, search(sim.next, OTHER[toMove], me, depth + 1, alpha, beta));
    beta = Math.min(beta, best);
    if (alpha >= beta) break;
  }
  return best;
}

export const minimaxBot = defineBot<typeof ticTacToe>({
  name: "minimax",
  decide({ legalActions, snapshot, playerID }) {
    if (snapshot === null) return legalActions[0]!;   // hosted host: no full snapshot
    let bestAction = legalActions[0]!;
    let bestScore = -Infinity;
    for (const action of legalActions) {
      const sim = simulate(ticTacToe, snapshot, playerID, action);
      if (!sim.ok) continue;
      const s = search(sim.next, OTHER[playerID]!, playerID, 1, -Infinity, Infinity);
      if (s > bestScore) { bestScore = s; bestAction = action; }
    }
    return bestAction;
  },
});
```

The `enumerate(snapshot, toMove)` helper re-runs the same legal-action logic against a simulated snapshot — define it inline or factor it out. See the [tic-tac-toe bot tutorial](/docs/tutorials/tic-tac-toe-bot) for the complete file.

## Verifying

A few moments worth running before you ship:

* **Unit:** call your `decide` with a hand-built `DecideContext` and assert the chosen action is in `legalActions`.
* **Integration:** play many random-vs-random matches and assert every match terminates with a legal `winner | draw`. The runner notification bus is easy to wire wrong; this catches it.
* **Deterministic regression:** fix `seed` on `createLocalSession` and assert the full move log is identical across runs. Useful to detect accidental non-determinism (e.g. a bot that calls `Math.random` instead of `rng`).

The repo's `examples/games/tic-tac-toe/bots` package has both unit and integration test suites you can crib from.

For a richer reference, [`examples/games/splendor/bots`](https://github.com/openturn-io/openturn/tree/main/openturn/examples/games/splendor/bots) ships three bot tiers (`random`, `greedy`, `strategic`) for a full-scale 2–4 player game. The `strategic` bot plans around nobles, engine balance, reserves, and opponent threats — useful as a template for your own heuristic bots that need to weigh multiple objectives in a non-trivial state space.

## Cloud play

In cloud `/play`, run bots as separate processes that connect to the room over WebSocket using the same protocol as a human:

```ts theme={null}
import { createHostedClient } from "@openturn/client";
import { attachHostedBot } from "@openturn/bot";

const client = createHostedClient({ /* roomID, playerID, getRoomToken */ });
await client.connect();

const runner = attachHostedBot({
  client,
  playerID: "1",
  bot: randomBot,
  game: ticTacToe,
});
```

`simulate` is unavailable on hosted hosts. Search-based bots run as in-process bots (CLI, server-side sidecar) where the full snapshot is reachable.

The recommended topology and supervisor pattern is in [Concepts: bots](/docs/concepts/bots#cloud-deployment).

## Related

* [Concepts: AI bots](/docs/concepts/bots) — design rationale.
* [Reference: `@openturn/bot`](/docs/reference/bot) — full API.
* [Tutorial: tic-tac-toe with bots](/docs/tutorials/tic-tac-toe-bot) — full walkthrough.
