Bot-Man-Toe/agents/chatgpt.py

1110 lines
30 KiB
Python

# -----------------------------------------------------------------------------
# This file was generated by an AI language model.
# Model: ChatGPT (OpenAI) GPT-5.5
# Generated: 2026-07-21T00:00:00Z
#
# As with any AI-generated code, it should be reviewed and tested before use.
# -----------------------------------------------------------------------------
from __future__ import annotations
from .agent import Agent, Payload
class ChatGPTAgent(Agent):
"""
A reasonably strong general-purpose agent.
Tic-tac-toe:
- Perfect play using minimax.
Royal Game of Ur:
- Rule-based heuristic prioritizing:
* finishing tokens
* capturing
* star squares
* advancing furthest pieces
"""
def __init__(self) -> None:
super().__init__(
name="ChatGPT",
author="OpenAI",
version="1.0.0",
profile={
"model": "GPT-5.5",
"games": [
"tic-tac-toe",
"royal-game-of-ur",
],
},
)
self.add_tic_tac_toe(
on_move=self.play_tic_tac_toe,
profile={"strategy": "minimax"},
)
self.add_royal_game_of_ur(
on_move=self.play_royal_game_of_ur,
profile={"strategy": "heuristic"},
)
# ------------------------------------------------------------------
# Tic-Tac-Toe
# ------------------------------------------------------------------
WIN_LINES = (
(1, 2, 3),
(4, 5, 6),
(7, 8, 9),
(1, 4, 7),
(2, 5, 8),
(3, 6, 9),
(1, 5, 9),
(3, 5, 7),
)
@classmethod
def _winner(cls, board):
for a, b, c in cls.WIN_LINES:
if board[a] != "" and board[a] == board[b] == board[c]:
return board[a]
return None
@classmethod
def _minimax(cls, board, player, me):
winner = cls._winner(board)
opponent = "O" if me == "X" else "X"
if winner == me:
return 1, None
if winner == opponent:
return -1, None
empty = [i for i in range(1, 10) if board[i] == ""]
if not empty:
return 0, None
maximizing = player == me
best_score = -2 if maximizing else 2
best_move = None
next_player = "O" if player == "X" else "X"
for move in empty:
board[move] = player
score, _ = cls._minimax(board, next_player, me)
board[move] = ""
if maximizing:
if score > best_score:
best_score = score
best_move = move
else:
if score < best_score:
best_score = score
best_move = move
return best_score, best_move
@classmethod
def play_tic_tac_toe(cls, payload: Payload) -> Payload:
board = {
i: payload.get(str(i), "")
for i in range(1, 10)
}
me = payload["your_token"]
_, move = cls._minimax(
board,
me,
me,
)
if move is None:
for i in range(1, 10):
if board[i] == "":
move = i
break
return {"move": move}
# ------------------------------------------------------------------
# Royal Game of Ur
# ------------------------------------------------------------------
@staticmethod
def play_royal_game_of_ur(payload: Payload) -> Payload:
valid_moves = payload.get("valid_moves", [])
if not valid_moves:
return {"move": 0}
safe = set(payload["safe_fields"])
stars = set(payload["star_fields"])
best_move = valid_moves[0]
best_score = -10**9
for start in valid_moves:
dest = start + payload["roll"]
score = 0
# Finishing a token is excellent.
if dest == 15:
score += 10000
# Capturing an enemy.
if (
dest not in safe
and payload.get(str(dest), "") == "ENEMY"
):
score += 5000
# Extra turn.
if dest in stars:
score += 2000
# Prefer advancing already-progressed tokens.
score += dest * 25
# Prefer moving pieces already on the board.
if start != 0:
score += 100
# Small preference for leaving the start square.
if start == 0:
score += 10
if score > best_score:
best_score = score
best_move = start
return {"move": best_move}
"""
This code was generated using ChatGPT. The prompt can be reviewed below.
===============================================================================
Please write an agent that can play tic-tac-toe and the royal game of ur. Your output will be written to `agents/chatgpt.py`.
At the top of the file, please write a small comment that explains that your code was written by you, an AI agent. Please specify your name, version and a timestamp of when the code was generated top give some insights in the quality of your code generation.
For context, here's a few files that are available in the repository:
At `README.md`:
```md
# Bot-Man-Toe
Write a script that plays tic-tac-toe, and see how well it performs against
other programs!
- [🚀 Write your own agent](agents/README.md)
- [🖊 Compare how well your agent performs](elo_tracker/README.md)
- [🌐 Publish your agent to the internet](pyserver/README.md)
## Get started
1. Clone this repository.
2. Run `python client.py` in the terminal and let two random agents play
against each other.
3. Copy the example agent and [create your own](agents/README.md).
4. Play against the AgentOfChaos while you improve your strategy.
5. Publish your agent. _(optional)_
6. Compare it against others with the Elo tracker. _(optional)_
## Links
- [📜 API specification](pyserver/spec.md)
- [🏆 Online ELO tracker](https://elo.noordstar.me/)
```
At `agents/README.md`:
```md
# 🚀 Write your own agent!
An **agent** is a Python class that knows how to play one or more games.
Don't worry about writing a perfect strategy. Start with something that works,
print the incoming game state, and improve it one step at a time.
## Quick start
1. Copy [example.py](example.py).
2. Rename the file and the `ExampleAgent` class.
3. Update the agent's name, author and version.
4. Implement `play_tic_tac_toe()`.
5. Import your agent in `client.py` and let it play a game.
That's enough to get started.
## Understanding the game
Whenever your agent has to make a move, it receives the current game state as
a Python dictionary.
Start by printing it:
```py
print(payload)
```
Run a few games and watch how the payload changes after every move. Once you
understand what you're receiving, you can start writing your own strategy!
Your function should return:
```py
{"move": 7}
```
where the number is the square you want to play.
## Testing your agent
Open [client.py](../client.py) and replace one of the players with your own agent.
For example:
```py
from agents.my_agent import MyAgent
from agents.chaos import AgentOfChaos
players = [
MyAgent(),
AgentOfChaos(),
]
```
The repository includes `AgentOfChaos`, a very simple opponent that plays
random moves. It's useful for testing your own agent while you're developing it.
> ⚠️ Don't try to build the perfect player immediately! Agents are easy to
> improve while you test against other agents. Plus, imperfect agents are
> typically the most interesting.
Run the client, inspect the output, tweak your algorithm, and repeat. You don't
need to understand the rest of the project before you can start experimenting.
## What's next?
Once you're happy with your agent, you can:
- [🖊 Compare your agent against other agents with the ELO tracker](../elo_tracker/README.md)
- [🌐 Publish your agent so other people can play against it](../pyserver/README.md)
```
At `agents/example.py`:
```py
\"""
This module contains an example agent that you can use to create your own!
Please copy this file, rename it, and then build it the way you see fit.
\"""
from __future__ import annotations
import random
from .agent import Agent, Payload
class ExampleAgent(Agent):
\"""
Describe here what your agent does and how it behaves! This will help
others understand how your agent works.
\"""
def __init__(self) -> None:
\"""
Create a custom instance of your agent. This function allows you
to define which games your agent can play.
You may add parameters to the function if your agent requires more
information to be able to operate.
\"""
super().__init__(
# Give your bot a name to display in leaderboards
name="MY super smart agent",
# Your name, to give you credit
author="Unknown programmer",
# Update the version to indicate the agent behaves differently.
# This will later allow you to compare different versions of your
# agent against one another.
version="1.0.0",
# Add extra custom information about the agent to this dictionary.
profile={},
)
# Indicate that you're willing to play tic-tac-toe
# Remove this if you don't want your bot to participate there.
self.add_tic_tac_toe(on_move=self.play_tic_tac_toe, profile={})
@staticmethod
def play_tic_tac_toe(payload : Payload) -> Payload:
\"""
Play a game of tic-tac-toe.
You receive a payload that looks like this:
{
"1": "X", "2": "", "3": "O",
"4": "X", "5": "O", "6": "",
"7": "", "8": "", "9": "",
"your_token": "X"
}
And you're expected to return a response of which field you'd like to
place your piece in. For example, if you wish to place your token in
field 7, your response should look like this:
{ "move": 7 }
The board is arranged as follows:
1 | 2 | 3
---+---+---
4 | 5 | 6
---+---+---
7 | 8 | 9
:param payload: The incoming JSON that contains the game state.
:type payload: dict[str, Any]
:return: The move you wish to take.
:rtype: dict[str, Any]
\"""
# Try printing the payload to see what it looks like!
print(payload)
options = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, ]
# 1. Try filtering out the impossible moves!
# If an X or O was already placed at a field, remove it from the options
#
# 2. Try finding two in a row! If possible, you can try to place the third
# item on the board and get 3 in a row.
#
# 3. Perhaps you can block the opponent from getting 3 in a row?
#
# Now, pick any of the remaining options.
# This is just a simple implementation. Naturally, you're welcome to try
# your own algorithm.
return { "move": random.choice(options) }
```
At `pyclient/games/tic_tac_toe.py`:
```py
\"""
This module hosts the game of tic-tac-toe, the traditional game where
you're trying to place 3 items in a row in a 3x3-grid.
\"""
from __future__ import annotations
from .game import FinishState, Game
from dataclasses import dataclass
from enum import Enum, auto
from typing import Any, Optional
class Field(Enum):
X = auto()
O = auto()
empty = auto()
def __str__(self):
\"""
Convert the field to a string.
\"""
match self:
case Field.X:
return "X"
case Field.O:
return "O"
case Field.empty:
return ""
@dataclass(frozen=True)
class TicTacToe(Game):
field_1 : Field
field_2 : Field
field_3 : Field
field_4 : Field
field_5 : Field
field_6 : Field
field_7 : Field
field_8 : Field
field_9 : Field
def __write(self, index : int, value : Field) -> "TicTacToe":
\"""
Create a new copy where one field has been written with any value.
:param index: The field number to replace. (1-9)
:type index: int
:param value: The field value to write.
:type value: Field
:return: A new copy where the value was written.
:rtype: TicTacToe
:raises ValueError: The field index has already been written to.
:raises KeyError: The index is an invalid number.
\"""
current_value = self.to_dict().get(str(index), None)
if current_value is None:
raise KeyError(
f"Index to write to should be 1-9, not {index}"
)
elif current_value != str(Field.empty):
raise ValueError(
f"Field should be empty, not {current_value}"
)
match index:
case 1:
return TicTacToe(
field_1=value,
field_2=self.field_2,
field_3=self.field_3,
field_4=self.field_4,
field_5=self.field_5,
field_6=self.field_6,
field_7=self.field_7,
field_8=self.field_8,
field_9=self.field_9,
)
case 2:
return TicTacToe(
field_1=self.field_1,
field_2=value,
field_3=self.field_3,
field_4=self.field_4,
field_5=self.field_5,
field_6=self.field_6,
field_7=self.field_7,
field_8=self.field_8,
field_9=self.field_9,
)
case 3:
return TicTacToe(
field_1=self.field_1,
field_2=self.field_2,
field_3=value,
field_4=self.field_4,
field_5=self.field_5,
field_6=self.field_6,
field_7=self.field_7,
field_8=self.field_8,
field_9=self.field_9,
)
case 4:
return TicTacToe(
field_1=self.field_1,
field_2=self.field_2,
field_3=self.field_3,
field_4=value,
field_5=self.field_5,
field_6=self.field_6,
field_7=self.field_7,
field_8=self.field_8,
field_9=self.field_9,
)
case 5:
return TicTacToe(
field_1=self.field_1,
field_2=self.field_2,
field_3=self.field_3,
field_4=self.field_4,
field_5=value,
field_6=self.field_6,
field_7=self.field_7,
field_8=self.field_8,
field_9=self.field_9,
)
case 6:
return TicTacToe(
field_1=self.field_1,
field_2=self.field_2,
field_3=self.field_3,
field_4=self.field_4,
field_5=self.field_5,
field_6=value,
field_7=self.field_7,
field_8=self.field_8,
field_9=self.field_9,
)
case 7:
return TicTacToe(
field_1=self.field_1,
field_2=self.field_2,
field_3=self.field_3,
field_4=self.field_4,
field_5=self.field_5,
field_6=self.field_6,
field_7=value,
field_8=self.field_8,
field_9=self.field_9,
)
case 8:
return TicTacToe(
field_1=self.field_1,
field_2=self.field_2,
field_3=self.field_3,
field_4=self.field_4,
field_5=self.field_5,
field_6=self.field_6,
field_7=self.field_7,
field_8=value,
field_9=self.field_9,
)
case 9:
return TicTacToe(
field_1=self.field_1,
field_2=self.field_2,
field_3=self.field_3,
field_4=self.field_4,
field_5=self.field_5,
field_6=self.field_6,
field_7=self.field_7,
field_8=self.field_8,
field_9=value,
)
case _:
raise KeyError(
f"Index to write to should be 1-9, not {index}"
)
def action_name(self) -> str:
return ""
def as_seen_by(self, player : int) -> dict[str, Any]:
\"""
Return the view of the game from the perspective of a given player.
:param player: The perspective on the board.
:type player: int
\"""
out = self.to_dict()
out["your_token"] = str(Field.X) if player == 1 else str(Field.O)
return out
def count_o(self) -> int:
\"""
Count the number of O's on the board.
\"""
return list(self.to_dict().values()).count(str(Field.O))
def count_x(self) -> int:
\"""
Count the number of X's on the board.
\"""
return list(self.to_dict().values()).count(str(Field.X))
@classmethod
def empty(cls):
return cls(
field_1=Field.empty, field_2=Field.empty, field_3=Field.empty,
field_4=Field.empty, field_5=Field.empty, field_6=Field.empty,
field_7=Field.empty, field_8=Field.empty, field_9=Field.empty,
)
def game_name(self) -> str:
return "tic-tac-toe"
def move_default(self) -> "TicTacToe":
\"""
Have a player take a "default" move. They'll take this move
whenever their response is invalid, or when they take too long
to decide, or when they're no longer accessible.
:return: New state of the board.
:rtype: TicTacToe
:raises ValueError: When no more moves exist. By this point, someone should've already won.
\"""
symbol = Field.X if self.player_to_move() == 1 else Field.O
for i in range(9):
try:
out = self.__write(i+1, symbol)
except ValueError:
# Field already occupied! Move to the next option.
pass
else:
return out
else:
# All moves seem invalid!
raise ValueError(
"No legal moves exist anymore on this tic-tac-toe board."
)
def move(self, payload : Optional[dict[str, Any]] = None) -> "TicTacToe":
\"""
Have a player make a move. Based on this information, update the
game.
:param payload: Dictionary containing the player's response.
:type payload: Optional[Dict[str, Any]]
:return: New state of the board.
:rtype: TicTacToe
:raises ValueError: When no more moves exist. By this point, someone should've already won.
\"""
symbol = Field.X if self.player_to_move() == 1 else Field.O
# Extract field to place symbol at
move = 1
if isinstance(payload, dict):
move = payload.get("move", None)
if not isinstance(move, int):
move = None
else:
move = move if 1 <= move <= 9 else None
if move is not None:
try:
out = self.__write(move, symbol)
except ValueError:
# Invalid move! We'll try any other move.
pass
else:
return out
# The player chose an invalid move.
return self.move_default()
def player_to_move(self) -> int:
\"""
Return which player needs to move.
\"""
return 1 if self.count_x() <= self.count_o() else 2
def to_dict(self) -> dict[str, Any]:
return {
"1": str(self.field_1),
"2": str(self.field_2),
"3": str(self.field_3),
"4": str(self.field_4),
"5": str(self.field_5),
"6": str(self.field_6),
"7": str(self.field_7),
"8": str(self.field_8),
"9": str(self.field_9),
}
def winner(self) -> dict[int, FinishState] | None:
\"""
Returns whether the board indicates that there's a winner.
:return: The winning player, zero in case of a tie, or None if there's no winner yet.
:rtype: dict[int, FinishState] | None
\"""
win_lines = [
[ 1, 2, 3, ],
[ 4, 5, 6, ],
[ 7, 8, 9, ],
[ 1, 4, 7, ],
[ 2, 5, 8, ],
[ 3, 6, 9, ],
[ 1, 5, 9, ],
[ 3, 5, 7, ],
]
d = self.to_dict()
out = { 1 : FinishState.loss, 2 : FinishState.loss, }
for player, symbol in [ ( 1, str(Field.X) ), ( 2, str(Field.O) ) ]:
for win_line in win_lines:
if all(d[str(w)] == symbol for w in win_line):
out[player] = FinishState.win
return out
else:
# Check for draw
if all(item != "" for item in d.values()):
return { 1 : FinishState.draw, 2 : FinishState.draw, }
return None
```
At `pyclient/games/ur.py`:
```py
\"""
The Royal game of Ur is supposedly one of the world's oldest known board
games. Players race 7 horses to the finish line while trying to sabotage
each other.
\"""
from __future__ import annotations
from typing import Any, Generator
import random
from .game import FinishState, Game
from dataclasses import dataclass
# On these fields, you can safely place your token without getting it removed
# if the opponent plays a token on their respective spot.
SAFE_FIELDS = set([ 0, 1, 2, 3, 4, 13, 14, 15 ])
# Whenever you land a token on this piece, you may play another turn.
STAR_FIELDS = set([ 4, 8, 14 ])
# You may place multiple pieces on these fields.
STACK_FIELDS = set([ 0, 15 ])
# Length of the board
BOARD_LENGTH = 1 + 4 + (4 + 2 + 2) + 2 + 1
@dataclass(frozen=True)
class RoyalGameOfUr(Game):
\"""
The Royal Game of Ur is a game where players roll dice to get to the
finish line.
\"""
board : list[tuple[int, int]]
mover : int
roll : int
def __updated_board(self, start : int) -> list[tuple[int, int]]:
new_board = []
assert self.roll > 0
assert 0 <= start < BOARD_LENGTH
assert start + self.roll < BOARD_LENGTH
elim = False
for i, (a, b) in enumerate(self.board):
if i == start:
if self.player_to_move() == 1:
new_board.append(( a - 1, b ))
else:
new_board.append(( a, b - 1 ))
elif i == start + self.roll:
if self.player_to_move() == 1:
if b > 0 and i not in SAFE_FIELDS:
elim = True
b = 0
new_board.append(( a + 1, b ))
else:
if a > 0 and i not in SAFE_FIELDS:
elim = True
a = 0
new_board.append(( a, b + 1 ))
else:
new_board.append(( a, b ))
if elim:
if self.player_to_move() == 1:
new_board[0] = ( new_board[0][0], new_board[0][1] + 1)
else:
new_board[0] = ( new_board[0][0] + 1, new_board[0][1] )
return new_board
def __valid_moves(self) -> Generator[int, None, None]:
for i in range(BOARD_LENGTH - self.roll):
dest = i + self.roll
# Skip star fields if they're already occupied
if dest in STAR_FIELDS and dest not in SAFE_FIELDS and self.board[dest] != ( 0, 0 ):
continue
if self.board[i][self.mover] > 0:
if dest in STACK_FIELDS or self.board[dest][self.mover] == 0:
yield i
def action_name(self) -> str:
return "" # Players can only move in this game
def as_seen_by(self, player: int) -> dict[str, Any]:
my_index = player - 1
ot_index = 1 - my_index
d : dict[str, Any] = dict(
enemies_at_start=self.board[0][ot_index],
enemies_at_finish=self.board[BOARD_LENGTH-1][ot_index],
roll=self.roll,
safe_fields=list(SAFE_FIELDS),
stack_fields=list(STACK_FIELDS),
star_fields=list(STAR_FIELDS),
tokens_at_start=self.board[0][my_index],
tokens_at_finish=self.board[BOARD_LENGTH-1][my_index],
valid_moves=list(self.__valid_moves()),
)
for i in range(BOARD_LENGTH):
d[str(i)] = (
"YOU" if self.board[i][my_index] > 0 else (
"ENEMY" if i not in SAFE_FIELDS and self.board[i][ot_index] > 0 else (
""
)))
for i in SAFE_FIELDS:
d[f"{i}_enemy"] = (
"ENEMY" if self.board[i][ot_index] > 0 else ""
)
return d
@classmethod
def empty(cls) -> RoyalGameOfUr:
\"""
Create a new game.
:return: Start state of the board
:rtype: RoyalGameOfUr
\"""
times, roll = cls.roll_repeatedly()
return cls(
board=[ (7, 7) ] + ((BOARD_LENGTH - 1) * [ (0, 0 ) ]),
mover=(1 + times) % 2,
roll=roll,
)
def game_name(self) -> str:
return "royal-game-of-ur"
def move_default(self) -> Game:
\"""
Fallback move option for when a user isn't available or cannot
decide. In this case, the player attempts to move whatever is the
furthest in the back.
\"""
player = self.player_to_move()
for i in self.__valid_moves():
new_board = self.__updated_board(start=i)
new_mover = self.mover
if i + self.roll not in STAR_FIELDS:
new_mover += 1
return RoyalGameOfUr(
board=new_board, mover=new_mover % 2, roll=self.roll_once(),
).skip_if_possible()
else:
raise ValueError(
"Player has no valid moves even though those should've been erased."
)
def move(self, payload : dict[str, Any] | None = None):
if not isinstance(payload, dict):
return self.move_default()
key = str(payload.get("move", ""))
for i in self.__valid_moves():
if str(i) == key:
new_board = self.__updated_board(start=i)
new_mover = self.mover
if i + self.roll not in STAR_FIELDS:
new_mover += 1
return RoyalGameOfUr(
board=new_board, mover=new_mover % 2, roll=self.roll_once(),
).skip_if_possible()
else:
return self.move_default()
def player_to_move(self) -> int:
return self.mover + 1
@staticmethod
def roll_once() -> int:
tot = 0
for _ in range(4):
tot += random.randint(0, 1)
return tot
@staticmethod
def roll_repeatedly() -> tuple[int, int]:
\"""
Roll until you don't roll a zero.
\"""
n, roll = 0, 0
while roll == 0:
n += 1
roll = RoyalGameOfUr.roll_once()
return n, roll
def skip_if_possible(self) -> RoyalGameOfUr:
\"""
Skip this turn. Allow the next player to make a move.
Players are not allowed to skip if they can make a valid move.
\"""
# Exit if at least one valid move exists
if self.roll > 0:
for _ in self.__valid_moves():
return self
# Exit if game has already finished
if self.winner() is not None:
return self
return RoyalGameOfUr(
board=self.board,
mover=(self.mover + 1) % 2,
roll=self.roll_once()
).skip_if_possible()
def to_dict(self) -> dict[str, Any]:
return self.as_seen_by(player=1)
def winner(self) -> dict[int, FinishState] | None:
a, b = self.board[-1]
if a == 7:
return {
1 : FinishState.win,
2 : FinishState.loss,
}
elif b == 7:
return {
1 : FinishState.loss,
2 : FinishState.win,
}
else:
return None
```
"""