update_room_config
Update room configuration in the lobby by providing any subset of settings. Resets both players' ready flags to ensure agreement on new config. Fails if game is already in progress.
Instructions
Host-only: tweak room config while still in the lobby.
Only fields passed (non-None) are updated. Any change resets both seats' ready flags — if readiness was previously agreed upon, the config shift might change the deal. Fails outside the pre-game states (COUNTING_DOWN, IN_GAME, FINISHED).
── Locking ── Input validation + scenario load happen OUTSIDE state_lock (pure I/O). The actual config mutation + readiness reset happen atomically under state_lock.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| connection_id | Yes | ||
| scenario | No | ||
| team_assignment | No | ||
| host_team | No | ||
| fog_of_war | No | ||
| max_turns | No | ||
| turn_time_limit_s | No |
Implementation Reference
- The actual implementation of the update_room_config tool. It is an async MCP tool registered via @mcp.tool() decorator. Validates input (scenario, team_assignment, host_team, fog_of_war, max_turns, turn_time_limit_s) outside the state lock, then atomically mutates the RoomConfig under state_lock. Also resets both seats' ready flags and recomputes room status.
@mcp.tool() async def update_room_config( connection_id: str, scenario: str | None = None, team_assignment: str | None = None, host_team: str | None = None, fog_of_war: str | None = None, max_turns: int | None = None, turn_time_limit_s: int | None = None, ) -> dict: """Host-only: tweak room config while still in the lobby. Only fields passed (non-None) are updated. Any change resets both seats' ready flags — if readiness was previously agreed upon, the config shift might change the deal. Fails outside the pre-game states (COUNTING_DOWN, IN_GAME, FINISHED). ── Locking ── Input validation + scenario load happen OUTSIDE state_lock (pure I/O). The actual config mutation + readiness reset happen atomically under state_lock. """ # ── Input validation (no locks) ── scenario_state = None if scenario is not None: try: scenario_state = load_scenario(scenario) except Exception as e: return _error(ErrorCode.BAD_INPUT, f"scenario load failed: {e}") if team_assignment is not None and team_assignment not in ("fixed", "random"): return _error( ErrorCode.BAD_INPUT, "team_assignment must be 'fixed' or 'random'" ) if host_team is not None and host_team not in ("blue", "red"): return _error(ErrorCode.BAD_INPUT, "host_team must be 'blue' or 'red'") if fog_of_war is not None and fog_of_war not in ( "none", "classic", "line_of_sight", ): return _error( ErrorCode.BAD_INPUT, "fog_of_war must be 'none' | 'classic' | 'line_of_sight'", ) if max_turns is not None and (max_turns < 1 or max_turns > 200): return _error( ErrorCode.BAD_INPUT, "max_turns must be between 1 and 200" ) if turn_time_limit_s is not None and ( turn_time_limit_s < 10 or turn_time_limit_s > 3600 ): return _error( ErrorCode.BAD_INPUT, "turn_time_limit_s must be between 10 and 3600", ) with app.state_lock(): conn = app._connections.get(connection_id) # noqa: SLF001 if conn is None or conn.state != ConnectionState.IN_ROOM: return _error( ErrorCode.TOOL_NOT_AVAILABLE_IN_STATE, "update_room_config requires state=in_room", ) info = app.conn_to_room.get(connection_id) if info is None: return _error(ErrorCode.NOT_IN_ROOM, "connection not seated") room_id, slot = info room = app.rooms.get(room_id) if room is None: return _error(ErrorCode.ROOM_NOT_FOUND, f"room {room_id} vanished") if slot != Slot.A: return _error( ErrorCode.BAD_INPUT, "only the host (slot A) can update room config", ) if room.status not in ( RoomStatus.WAITING_FOR_PLAYERS, RoomStatus.WAITING_READY, ): return _error( ErrorCode.TOOL_NOT_AVAILABLE_IN_STATE, f"room is in {room.status.value}; config locked", ) if scenario is not None: room.config.scenario = scenario # Switching scenario implicitly resets max_turns to the new # scenario's declared cap unless the host overrides it in # the same call. if max_turns is None and scenario_state is not None: room.config.max_turns = scenario_state.max_turns if team_assignment is not None: room.config.team_assignment = team_assignment # type: ignore[assignment] if host_team is not None: room.config.host_team = host_team # type: ignore[assignment] if fog_of_war is not None: room.config.fog_of_war = fog_of_war # type: ignore[assignment] if max_turns is not None: room.config.max_turns = max_turns if turn_time_limit_s is not None: room.config.turn_time_limit_s = turn_time_limit_s # Config change resets readiness so both sides explicitly # re-agree on the new terms. for seat in room.seats.values(): seat.ready = False room.recompute_status() log.info( "update_room_config: room=%s scenario=%s fog=%s teams=%s host_team=%s", room_id, room.config.scenario, room.config.fog_of_war, room.config.team_assignment, room.config.host_team, ) return _ok({}) - The RoomConfig dataclass defines the shape of data that update_room_config writes to: scenario, max_turns, team_assignment, host_team, fog_of_war, turn_time_limit_s.
@dataclass class RoomConfig: """Per-room configuration. Mutable pre-game so the host can tweak it. Frozen semantics would be nicer for safety, but the UX is that the host can flip scenario / fog / team mode in the lobby before both players press ready. `update_room_config` is the single write path (host-only, refuses once COUNTING_DOWN/IN_GAME/FINISHED). - team_assignment="fixed": host gets host_team; joiner gets the other. - team_assignment="random": coin-flipped at game-start time. - fog_of_war / max_turns / turn_time_limit_s drive the engine and filter behavior during the match. """ scenario: str max_turns: int = 20 team_assignment: TeamAssignment = "fixed" host_team: HostTeam = "blue" fog_of_war: FogMode = "none" # easier onboarding; bump to "classic" per-room # Default is deliberately huge so neither the client-side agent # loop nor the server-side forfeit interferes with reasoning-model # debugging. Hosts can dial it down to blitz-game values from the # room Actions panel; 1800s (30 min) is a reasonable upper ceiling # for a single turn where a weak model with many units needs the # full observe-act-observe cycle plus ample reasoning tokens. turn_time_limit_s: int = 1800 - src/silicon_pantheon/server/lobby_tools.py:118-118 (registration)register_lobby_tools() is called from app.py to register all lobby tools (including update_room_config) onto the FastMCP instance. The tool is registered via the @mcp.tool() decorator at line 538.
def register_lobby_tools(mcp: FastMCP, app: App) -> None: - src/silicon_pantheon/server/app.py:469-471 (registration)App initialization: imports and calls register_lobby_tools(mcp, app) to attach all lobby tools including update_room_config to the MCP server.
from silicon_pantheon.server.lobby_tools import register_lobby_tools register_lobby_tools(mcp, app)