# ----------------------------------------------------------------------------- # 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 ``` """