1110 lines
30 KiB
Python
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
|
|
```
|
|
""" |