From 7b0f147a89e2a0502489d65c89d2278599a8090d Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Tue, 23 Jun 2026 23:24:32 +0200 Subject: [PATCH] Create generalized Agent class --- agents/__init__.py | 14 +++++ agents/agent.py | 134 +++++++++++++++++++++++++++++++++++++++++++++ pyserver/server.py | 2 - 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 agents/__init__.py create mode 100644 agents/agent.py diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000..cc664a8 --- /dev/null +++ b/agents/__init__.py @@ -0,0 +1,14 @@ +""" + Agents are players that can participate in a game. THey contain a unified + API that allows other systems to apply to them for communication. + For example: + + 1. The PyClient can locally run a game with agents as participants. + 2. A PyServer can host a web server and asks a specific agent to respond. +""" + +from .agent import Agent + +__all__ = [ + "Agent" +] diff --git a/agents/agent.py b/agents/agent.py new file mode 100644 index 0000000..71585ce --- /dev/null +++ b/agents/agent.py @@ -0,0 +1,134 @@ +""" + 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 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) diff --git a/pyserver/server.py b/pyserver/server.py index 26fc088..40b9b26 100644 --- a/pyserver/server.py +++ b/pyserver/server.py @@ -19,7 +19,6 @@ class PyServer: name: str, import_name: str = __name__, profile: Optional[PayloadType] = None, - subpath : str = "", ) -> None: """ Create a PyServer. @@ -30,7 +29,6 @@ class PyServer: :type import_name: str :param profile: Additional root-level discovery metadata. :type profile: Optional[Dict[str, Any]] - :type subpath: str :raises ValueError: The input contains invalid information. """ self.name = name