Bot-Man-Toe/agents/agent.py

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)