155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
"""
|
|
The agent module hosts the base class of all agents. It dictates how
|
|
agents are expected to behave.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Callable
|
|
|
|
# Communication type that the players communicate in
|
|
Payload = dict[str, Any]
|
|
|
|
# Function that the agent is expected to communicate with.
|
|
# The ActionFunction takes a Payload as input and returns a Payload of its own.
|
|
ActionFunction = Callable[[Payload], Payload]
|
|
|
|
GameInfo = tuple[Payload, dict[str, ActionFunction]]
|
|
|
|
class Agent:
|
|
"""
|
|
Base class for all agents to communicate in.
|
|
"""
|
|
|
|
# Local variables
|
|
author : str
|
|
name : str
|
|
profile : Payload
|
|
registered_games : dict[str, tuple[Payload, dict[str, ActionFunction]]]
|
|
version : str | None
|
|
|
|
def __init__(
|
|
self,
|
|
name : str,
|
|
author : str,
|
|
version : str | None = None,
|
|
profile : Payload = {},
|
|
) -> None:
|
|
"""
|
|
Create an instance of the agent.
|
|
|
|
Note that the base class expects quite a lot of variables for the
|
|
initialization, whereas the subclasses are expected to require
|
|
no inputs for initialization. It is the subclasses' responsibility
|
|
to override the current function by inserting its own values.
|
|
|
|
:param name: String value giving the player a name.
|
|
:type name: str
|
|
:param author: String value representing the player's designer or programmer.
|
|
:type author: str
|
|
:param version: Version of the player, in case an update changes its behavior.
|
|
:type version: str | None
|
|
:param profile: Custom profile pertaining to the player.
|
|
:type profile: dict[str, Any]
|
|
"""
|
|
|
|
# Register basic (required) variables
|
|
self.author = author
|
|
self.name = name
|
|
self.profile = profile
|
|
self.version = version
|
|
|
|
# Have the subclass register games
|
|
self.registered_games = {}
|
|
|
|
def add_game(
|
|
self,
|
|
name : str,
|
|
actions : dict[str, ActionFunction],
|
|
profile : Payload,
|
|
required_actions : list[str] = [],
|
|
) -> None:
|
|
"""
|
|
Add a new game to the player. Replaces any prior definition for the
|
|
game.
|
|
|
|
:param name: Game name
|
|
:type name: str
|
|
:param actions: Dictionary containing all actions the player is willing to take.
|
|
:type actions: dict[str, Callable[[dict[str, Any]], dict[str, Any]]]
|
|
:param profile: Custom profile describing some details about a game.
|
|
:type profile: dict[str, Any]
|
|
:param required_actions: Optional completeness check. If provided,
|
|
all required action names must exist in `actions` and map to
|
|
callables.
|
|
:type required_actions: list[str]
|
|
:raises AssertionError: Some of the required actions aren't present.
|
|
"""
|
|
|
|
# Verify that all required actions exist
|
|
for ra in required_actions:
|
|
if ra not in actions.keys():
|
|
raise AssertionError(
|
|
f"Missing required action `{ra}` in action mapping."
|
|
)
|
|
|
|
self.registered_games[name] = ( profile, actions )
|
|
|
|
def add_tic_tac_toe(
|
|
self,
|
|
on_move : ActionFunction,
|
|
profile : Payload = {},
|
|
) -> None:
|
|
"""
|
|
Convenience registration for the tic-tac-toe game.
|
|
|
|
: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="tic-tac-toe",
|
|
actions={"": on_move},
|
|
profile=profile,
|
|
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.
|
|
|
|
:param game: The name of the game.
|
|
:type game: str
|
|
:param action: The name of the action, or None if the game only has one action.
|
|
:type action: str | None
|
|
:return: The action with which the agent would like to act, if at all.
|
|
:rtype: Callable[[dict[str, Any]], dict[str, Any]] | None
|
|
"""
|
|
_, actions = self.registered_games.get(game, ({}, {}))
|
|
|
|
if action is None:
|
|
return list(actions.values())[0] if len(actions) == 1 else None
|
|
else:
|
|
return actions.get(action, None)
|