import {
createLocalSession,
defineGame,
defineEvent,
type LocalGameSession,
type PlayerIDOf,
} from "@openturn/core";
export type TicTacToeCell = "X" | "O" | null;
export interface TicTacToeState {
board: TicTacToeCell[][];
}
export interface PlaceMarkArgs {
row: number;
col: number;
}
type TicTacToeResult = { draw?: true; winner?: "0" | "1" };
const PLAYER_MARKS: Record<"0" | "1", "X" | "O"> = { "0": "X", "1": "O" };
export const ticTacToeMachine = defineGame({
maxPlayers: 2,
events: {
place_mark: defineEvent<PlaceMarkArgs>(),
},
initial: "play",
selectors: {
boardFull: ({ G }) => isBoardFull(G.board),
winnerMark: ({ G }) => getWinner(G.board),
},
setup: (): TicTacToeState => ({ board: createEmptyBoard() }),
states: {
play: {
activePlayers: ({ match, position }) =>
[match.players[(position.turn - 1) % match.players.length]!],
control: () => ({ status: "playing" }),
label: ({ match, position }) => `Player ${currentPlayer(match.players, position.turn)} to play`,
},
won: {
activePlayers: () => [],
control: () => ({ status: "won" }),
label: "Winner",
metadata: ({ G }) => [{ key: "winnerMark", value: getWinner(G.board) }],
},
drawn: {
activePlayers: () => [],
control: () => ({ status: "drawn" }),
label: "Draw",
},
},
transitions: ({ transition }) => [
transition("place_mark", {
from: "play",
to: "won",
label: "place_mark_to_won",
turn: "increment",
resolve: ({ G, event, playerID }) => {
const board = placeMark(G.board, event.payload.row, event.payload.col, playerID);
if (board === null) return null;
const winner = getWinner(board);
if (winner === null) return null;
return {
G: { board },
result: { winner: winner === "X" ? "0" : "1" } satisfies TicTacToeResult,
};
},
}),
transition("place_mark", {
from: "play",
to: "drawn",
label: "place_mark_to_drawn",
turn: "increment",
resolve: ({ G, event, playerID }) => {
const board = placeMark(G.board, event.payload.row, event.payload.col, playerID);
if (board === null || getWinner(board) !== null || !isBoardFull(board)) return null;
return {
G: { board },
result: { draw: true } satisfies TicTacToeResult,
};
},
}),
transition("place_mark", {
from: "play",
to: "play",
label: "place_mark_continue",
turn: "increment",
resolve: ({ G, event, playerID }) => {
const board = placeMark(G.board, event.payload.row, event.payload.col, playerID);
if (board === null || getWinner(board) !== null || isBoardFull(board)) return null;
return { G: { board } };
},
}),
],
views: {
player: ({ G, match, position }, playerID) => ({
board: G.board,
currentPlayer: currentPlayer(match.players, position.turn),
myMark: PLAYER_MARKS[playerID] ?? null,
}),
public: ({ G, match, position }) => ({
board: G.board,
currentPlayer: currentPlayer(match.players, position.turn),
}),
},
});
export function createTicTacToeMachineSession(): LocalGameSession<typeof ticTacToeMachine> {
return createLocalSession(ticTacToeMachine, {
match: { players: ticTacToeMachine.playerIDs },
});
}