Source code for bot_api.team_message

"""Team message type registry for typed team message serialization/deserialization.

This module provides a registry mechanism for team message types, allowing bots to
send and receive typed objects (like RobotColors, Point) instead of raw dictionaries.
This matches the behavior of Java and C# Bot APIs.

Usage::

    # Using decorator
    @team_message_type
    @dataclass
    class RobotColors:
        body_color: Color
        turret_color: Color

    # Using function
    register_team_message_type(Point)

    # The type is automatically serialized/deserialized when using
    # broadcast_team_message() and on_team_message()
"""

from __future__ import annotations

import inspect
import json
from dataclasses import fields, is_dataclass
from typing import Any, Optional, Type, get_type_hints

# Registry mapping class names to class types
_registry: dict[str, Type[Any]] = {}


[docs] def team_message_type(cls: Type[Any]) -> Type[Any]: """Decorator to register a class as a team message type. The class name is used as the message type identifier for serialization. Args: cls: The class to register. Returns: The same class, unmodified. Example:: @team_message_type @dataclass class RobotColors: body_color: Color turret_color: Color """ register_team_message_type(cls) return cls
[docs] def register_team_message_type(cls: Type[Any]) -> None: """Register a class as a team message type. The class name is used as the message type identifier for serialization. Args: cls: The class to register. Example: register_team_message_type(Point) """ _registry[cls.__name__] = cls
[docs] def get_team_message_type(name: str) -> Optional[Type[Any]]: """Look up a registered team message type by name. Args: name: The class name to look up. Returns: The registered class, or None if not found. """ return _registry.get(name)
def _to_camel_case(snake_str: str) -> str: """Converts a snake_case string to camelCase.""" components = snake_str.split("_") return components[0] + "".join(x.title() for x in components[1:]) def _to_snake_case(camel_str: str) -> str: """Converts a camelCase string to snake_case.""" s = "".join(["_" + c.lower() if c.isupper() else c for c in camel_str]) return s.lstrip("_")
[docs] def serialize_team_message(obj: Any) -> str: """Serialize a team message object to JSON string. Handles Color objects by converting them to hex strings. Converts snake_case field names to camelCase for JSON. Args: obj: The object to serialize. Returns: JSON string representation. """ from robocode_tank_royale.bot_api.graphics.color import Color def convert(o: Any) -> Any: if isinstance(o, Color): return o.to_hex_color() if is_dataclass(o) and not isinstance(o, type): result = {} for field in fields(o): value = getattr(o, field.name) result[_to_camel_case(field.name)] = convert(value) return result if isinstance(o, dict): return {_to_camel_case(k) if isinstance(k, str) else k: convert(v) for k, v in o.items()} if isinstance(o, (list, tuple)): return [convert(item) for item in o] return o return json.dumps(convert(obj), separators=(",", ":"))
[docs] def deserialize_team_message(json_str: str, message_type: str) -> Any: """Deserialize a JSON string to a team message object. Uses the message_type to look up the registered class and instantiate it. Handles Color fields by converting hex strings to Color objects. Args: json_str: The JSON string to deserialize. message_type: The class name to deserialize into. Returns: The deserialized object, or a dictionary if the type is not registered. """ from robocode_tank_royale.bot_api.graphics.color import Color data = json.loads(json_str) cls = get_team_message_type(message_type) if cls is None: # Type not registered, return raw dictionary return data return _deserialize_object(data, cls)
def _deserialize_object(data: dict[str, Any], cls: Type[Any]) -> Any: """Deserialize a dictionary into an instance of the given class. Uses type hints to identify Color fields and convert hex strings. Args: data: The dictionary to deserialize. cls: The class to instantiate. Returns: An instance of the class. """ from robocode_tank_royale.bot_api.graphics.color import Color # Get type hints for the class try: hints = get_type_hints(cls) except Exception: hints = {} # Convert camelCase keys to snake_case snake_data = {_to_snake_case(k): v for k, v in data.items()} # Build kwargs for constructor kwargs: dict[str, Any] = {} if is_dataclass(cls): for field in fields(cls): field_name = field.name if field_name not in snake_data: continue value = snake_data[field_name] field_type = hints.get(field_name, field.type) # Handle Color type if _is_color_type(field_type): if value is not None and isinstance(value, str): kwargs[field_name] = Color.from_hex_color(value) else: kwargs[field_name] = None # Handle Optional[Color] elif _is_optional_color_type(field_type): if value is not None and isinstance(value, str): kwargs[field_name] = Color.from_hex_color(value) else: kwargs[field_name] = None else: kwargs[field_name] = value else: # For non-dataclass, try to use __init__ signature sig = inspect.signature(cls.__init__) for param_name, param in sig.parameters.items(): if param_name == "self": continue if param_name not in snake_data: continue value = snake_data[param_name] field_type = hints.get(param_name, param.annotation) if _is_color_type(field_type) or _is_optional_color_type(field_type): if value is not None and isinstance(value, str): kwargs[param_name] = Color.from_hex_color(value) else: kwargs[param_name] = None else: kwargs[param_name] = value return cls(**kwargs) def _is_color_type(type_hint: Any) -> bool: """Check if a type hint is the Color type.""" from robocode_tank_royale.bot_api.graphics.color import Color if type_hint is Color: return True if isinstance(type_hint, str) and type_hint == "Color": return True return False def _is_optional_color_type(type_hint: Any) -> bool: """Check if a type hint is Optional[Color].""" from robocode_tank_royale.bot_api.graphics.color import Color # Handle typing.Optional[Color] which is Union[Color, None] origin = getattr(type_hint, "__origin__", None) if origin is not None: # Python 3.10+ uses types.UnionType for X | Y import sys if sys.version_info >= (3, 10): import types if origin is types.UnionType: args = getattr(type_hint, "__args__", ()) return Color in args or type(None) in args # typing.Union or typing.Optional from typing import Union if origin is Union: args = getattr(type_hint, "__args__", ()) return Color in args return False __all__ = [ "team_message_type", "register_team_message_type", "get_team_message_type", "serialize_team_message", "deserialize_team_message", ]