Implement royal game of Ur
parent
90d2bea528
commit
da26f5c016
|
|
@ -115,6 +115,26 @@ class Agent:
|
|||
required_actions=[""],
|
||||
)
|
||||
|
||||
def add_royal_game_of_ur(
|
||||
self,
|
||||
on_move : ActionFunction,
|
||||
profile : Payload = {},
|
||||
) -> None:
|
||||
"""
|
||||
Convenience registration for the royal game of Ur.
|
||||
|
||||
:param on_move: Called function when the player is to make a move.
|
||||
:type on_move: Callable[[dict[str, Any]], dict[str, Any]]
|
||||
:param profile: Custom details to share about the game.
|
||||
:type profile: dict[str, Any]
|
||||
"""
|
||||
return self.add_game(
|
||||
name="royal-game-of-ur",
|
||||
actions={"": on_move},
|
||||
profile=profile,
|
||||
required_actions=[""],
|
||||
)
|
||||
|
||||
def get_action(self, game : str, action : str | None) -> ActionFunction | None:
|
||||
"""
|
||||
Get an action function for a given game, if it exists.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class AgentOfChaos(Agent):
|
|||
)
|
||||
|
||||
self.add_tic_tac_toe(on_move=play_tic_tac_toe, profile={})
|
||||
self.add_royal_game_of_ur(on_move=play_royal_game_of_ur, profile={})
|
||||
|
||||
|
||||
def play_tic_tac_toe(payload : Payload) -> Payload:
|
||||
|
|
@ -61,4 +62,7 @@ def play_tic_tac_toe(payload : Payload) -> Payload:
|
|||
for k, v in dict(payload).items()
|
||||
if k in "0123456789" and v == ""
|
||||
]
|
||||
return { "move": random.choice(options) }
|
||||
return { "move": random.choice(options) }
|
||||
|
||||
def play_royal_game_of_ur(payload : Payload) -> Payload:
|
||||
return { "move": random.choice(payload.get("valid_moves", [""])) }
|
||||
|
|
@ -42,6 +42,9 @@ class MuteAgent(Agent):
|
|||
},
|
||||
)
|
||||
|
||||
self.add_tic_tac_toe(on_move=respond_mute)
|
||||
self.add_royal_game_of_ur(on_move=respond_mute)
|
||||
|
||||
def get_action(self, game: str, action: str | None) -> ActionFunction:
|
||||
"""
|
||||
Return the mute response for any game or action.
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
from .game import FinishState, Game
|
||||
from .tic_tac_toe import TicTacToe
|
||||
from .ur import RoyalGameOfUr
|
||||
|
||||
__all__ = [
|
||||
"FinishState",
|
||||
"Game",
|
||||
"RoyalGameOfUr",
|
||||
"TicTacToe",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
"""
|
||||
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
|
||||
|
||||
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[15][ot_index],
|
||||
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[15][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 my_index 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
|
||||
Loading…
Reference in New Issue