tictactoe.fun

API & MCP

Everything this site does is an HTTP call you can make too. You can list the games, start one, make moves, and ask the same computer you play here for its move. It answers in JSON, it's free, and there's no key or anything to sign up for.

The API lives under https://tictactoe.fun/api/v1. The whole contract is /api/v1/openapi.yaml, an OpenAPI document you can read or point a client generator at. This page is the short version.

If you'd rather have an AI assistant play, skip to MCP.

Limits

  • best and analyze on Connect Four and Ultimate Tic-Tac-Toe run a search, and the server runs only a few at a time. If they all stay busy for four seconds, or too many searches are already waiting, you get a 503 with the code busy, and Retry-After says how many seconds to wait.
  • In MCP, those same searches spend from a session's budget, and the server holds only so many sessions at once. The MCP section below has both figures.
  • POST /api/v1/rooms answers a 503 with the code room_cap when the server already has as many rooms open as it holds.

Try it with curl

A game is the moves played so far. You send the list, and the server plays it through the same engine this site runs and tells you where the game stands. There's nothing to keep between calls but the list.

A move is a square, row then column, counted from 0 at the top left. On a 3×3 board 1.1 is the middle and 0.2 the top right corner. In a URL the moves go in moves, separated by commas, and no moves at all is the empty board:

moves=, the empty board.

Each step below draws the board its moves reach.

Start with the list of games. Each one has an id you'll use in every other call. Here you see just the first game.

curl https://tictactoe.fun/api/v1/games
{"games": [{"id": "ttt", "name": "Tic-Tac-Toe", "players": 2}]}

See the moves

legal tells you where a list of moves leaves the game. With no moves, that's the empty board:

curl 'https://tictactoe.fun/api/v1/games/ttt/legal?moves='
{"game": "ttt", "start": 0, "moves": [],
 "board": [["", "", ""], ["", "", ""], ["", "", ""]],
 "turn": "X", "over": false,
 "legal": [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]]}

board is the rows from the top, and legal lists every move you can make, written the way you send one back. In JSON a move is [row, column].

Make a move

To play, post the moves so far and the one to add. X takes the middle:

curl -X POST https://tictactoe.fun/api/v1/games/ttt/moves -d '{"moves": [], "move": [1, 1]}'
{"game": "ttt", "start": 0, "moves": [[1, 1]],
 "board": [["", "", ""], ["", "X", ""], ["", "", ""]],
 "turn": "O", "over": false}
moves=1.1, X in the middle. The next few steps ask about this board.

The answer's moves has your move on the end. Send it back unchanged next time, and you never have to track the game yourself.

over is always there. While the game goes on, turn says who moves next. Once it's over, result is the winner, X or O, or draw. Here X finishes the top row:

moves=0.0,1.0,0.1,1.1, X to move with two in the top row.
curl -X POST https://tictactoe.fun/api/v1/games/ttt/moves -d '{"moves": [[0, 0], [1, 0], [0, 1], [1, 1]], "move": [0, 2]}'
{"game": "ttt", "moves": [[0, 0], [1, 0], [0, 1], [1, 1], [0, 2]],
 "over": true, "result": "X", "legal": []}
moves=0.0,1.0,0.1,1.1,0.2, three in the top row, so X has won.

Ask for the best move

curl 'https://tictactoe.fun/api/v1/games/ttt/best?moves=1.1'
{"game": "ttt", "moves": [[1, 1]], "move": [0, 0], "source": "table"}

source says where the answer came from. table and book are proven, so that's perfect play. search and mcts are strong, but they don't prove anything, and random is the easy level's pick.

For a weaker opponent, add level=easy or level=medium. Pass a seed and you get the same move every time. Leave it out and the server picks one and tells you which.

curl 'https://tictactoe.fun/api/v1/games/ttt/best?moves=1.1&level=easy&seed=7'
{"game": "ttt", "moves": [[1, 1]], "move": [0, 1], "source": "random",
 "level": "easy", "seed": 7}

Rate every move

analyze rates each legal move for the player making it: the outcome with best play from both sides, in how many moves the game has left, and best on the ones worth playing. Here you see just the first two moves.

curl 'https://tictactoe.fun/api/v1/games/ttt/analyze?moves=1.1'
{"game": "ttt", "moves": [[1, 1]], "turn": "O", "evaluations": [
  {"move": [0, 0], "value": 0, "outcome": "draw", "in": 8, "best": true},
  {"move": [0, 1], "value": -5, "outcome": "loss", "in": 6}
]}

Other games

Every game draws its board as rows from the top, and a move is always the square it plays. In Connect Four that's the square your piece lands on, so the first move in the middle column is 5.3, the bottom row. A few games need a little more:

  • In Three Men's Morris, once all six pieces are down a move takes a piece from one point to another: 0.0>1.1 in a URL, [[0, 0], [1, 1]] in JSON. The answer's phase says which kind of move comes next.
  • In Wild Tic-Tac-Toe you pick your mark: 1.1=O, or [1, 1, "O"].
  • In Ultimate the board is the whole 9×9, and playIn says which small board the next move has to go in, or null when it can go anywhere.
  • Treblecross comes in nine board lengths. Its document, /api/v1/games/treblecross, lists them as starts, and start=0 picks the first.

Connect Four and Ultimate

These two aren't in a table, so the computer can't look the answer up. In Connect Four it searches ten moves ahead, and in Ultimate it plays out thousands of random games from the position and picks the move those games went through most often. Either way you get a strong move rather than a proven one, and source tells you which kind you got. Here each player has dropped a piece in the middle column:

curl 'https://tictactoe.fun/api/v1/games/connect4/best?moves=5.3,4.3'
{"game": "connect4", "moves": [[5, 3], [4, 3]], "move": [3, 3],
 "source": "search", "depth": 10}
moves=5.3,4.3, one piece each in the middle column.

depth is how many moves ahead the search looked. An Ultimate answer says mcts instead, with iterations, the number of games it played.

A new version of the engine can answer these differently, so they're cached for an hour rather than for good. Each one carries an ETag. Ask again with it in If-None-Match and you get a 304 until the engine changes.

Errors

Anything that isn't a success comes back in one shape. When a move is the problem, at says which one, counting from 0:

curl -X POST https://tictactoe.fun/api/v1/games/ttt/moves -d '{"moves": [[1, 1]], "move": [1, 1]}'
{"error": {"code": "occupied", "message": "move 1, 1.1, was refused: engine: cell occupied", "at": 1}}

Switch on code, which stays the same. message is written for people and can change. A room's socket and the MCP tools use the same codes, so you can handle a refusal one way wherever you meet it.

bad_move
A move isn't written as one, or has the wrong shape for the position. at says which move.
illegal_move
That move isn't on this board, or this position doesn't allow it.
occupied
The square is taken, or in Ultimate, it's not on a board you were sent to.
game_over
The game has ended, so there's no move to make or recommend.
unknown_game
No game has that id.
bad_body
The body isn't the JSON object the contract describes.
bad_start
start isn't one of the game's openings.
bad_argument
An MCP tool argument is outside the values it takes.
not_found
Nothing is served at that path.
no_room
No live room has that code.
bad_level
level isn't easy, medium or hard, or the seed isn't one it takes.
bad_query
The query has a parameter that endpoint doesn't read, or doesn't parse.
method_not_allowed
The path doesn't take that method. Allow lists the ones it does.
not_your_turn
In a room, it's the other player's move.
not_owner
Only the player who opened the room can start another round.
game_not_over
Another round can start only once this one has ended.
room_cap
The server has as many rooms open as it can hold. Try again after Retry-After.
budget_spent
This MCP session has spent its expensive calls. A new session starts fresh.
analysis_disabled
The server has switched off searches for this game for now. Games answered from a table still answer.
busy
Every search the server runs at once is taken. Try again after Retry-After.
session_cap
The server has as many MCP sessions open as it holds. Try again after Retry-After. Open sessions keep working.
no_solver
The rules run, but nothing here can recommend a move.
host_failure
The engine failed on the server. Nothing was wrong with your request.
server_failure
The room failed on the server. Nothing was wrong with your message.

MCP

https://tictactoe.fun/mcp is the same engine over the Model Context Protocol, on Streamable HTTP. Point an AI assistant at it and it can list the games, play one, and ask the engine for the answer. There's no key here either.

To add it to Claude Code:

claude mcp add --transport http tictactoe https://tictactoe.fun/mcp

A client that reads a JSON file, like a project's .mcp.json, takes it this way:

{
  "mcpServers": {
    "tictactoe": {"type": "http", "url": "https://tictactoe.fun/mcp"}
  }
}

On claude.ai, add the same address as a custom connector under Settings, then Connectors.

There's no sign-in, and nothing about you is kept. Privacy says what the endpoint does with a call, and Terms covers using it.

Tools

  • list_games lists every game, with its id and board.
  • get_game is one game in full: its board, and its rules as markdown.
  • new_game opens a game and draws the board. Pass first: "o" and the engine plays first, and level to open against easy, medium or hard.
  • legal_moves lists the moves you can play in a position.
  • make_move plays one more move and draws the board after it.
  • best_move is the engine's move and what proved it. It takes the same level and seed as the API.
  • analyze rates every legal move.
  • render_board draws the position as text, with the name of each square and every legal move.

None of the tools change anything on the server. The game lives in the list of moves you pass.

Each game is also a resource, game://{id}, with its rules. The prompt play_a_game sets up a whole game and has the model score itself against the engine afterwards.

A game is the moves played so far, the same as in the API. Every result hands back start and moves. Pass them to the next call as they are. A move is [row, column] from the top left, and every result lists the legal ones.

Every result carries a budget. A session gets 200 expensive calls: a Connect Four or Ultimate answer spends one, unless it's at easy, and everything else is free. An answer refused as busy spends nothing.

The server holds 1,000 sessions at once. Past that, a new session gets a 503 with the code session_cap and a Retry-After, and every open session keeps working.

A board in the chat

In Claude, ChatGPT and any other chat that supports MCP Apps, new_game opens a board right in the conversation. Click a square to play, and the computer answers at the level you pick, or the one the chat asked new_game for. The board tells the chat where the game stands after every move, so you can ask about the position or pick the game up in words. A chat without MCP Apps plays the same game from the text board.

Every move on the board is a tool call like any other, so a Connect Four or Ultimate game above easy spends from that budget.

An example

After X takes the middle, a model asks for O's best move:

{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
 "params": {"name": "best_move", "arguments": {"game": "ttt", "moves": [[1, 1]]}}}

The result's structured content:

{"game": "ttt", "start": 0, "moves": [[1, 1]], "move": [0, 0], "play": "a1",
 "source": "table", "proven": true, "budget": {"limit": 200, "remaining": 200}}