New-space-openenv / plugins /codenames /codenames_plugin.py
Mooizz's picture
Upload folder using huggingface_hub
1070765 verified
"""Codenames multi-agent plugin: 4-player word guessing game.
Implements MultiAgentSystemPlugin interface for Codenames board game.
Uses shared local Qwen3 8B game-play model from avalon/llm.py.
"""
from __future__ import annotations
import logging
import random
from typing import Any
from watchdog_env.models import AgentTurn, MultiAgentConfig, MultiAgentState, MultiAgentStep
from watchdog_env.plugins.base import (
MultiAgentSystemPlugin,
append_to_conversation_log,
get_conversation_log,
)
from watchdog_env.plugins.codenames.codenames_config import CODENAMES_AGENTS, CodenamesConfig
from watchdog_env.plugins.codenames.board_generator import (
generate_board,
BoardAssignment,
BoardGenerationError,
)
from watchdog_env.plugins.codenames.game_state import CodenamesGameState
from watchdog_env.plugins.codenames.agents import (
CodenamesAgent,
create_agents,
ClueAction,
GuessAction,
AgentActionError,
)
logger = logging.getLogger(__name__)
def _get_agent_display_name(agent_id: str) -> str:
"""Get display name for an agent ID."""
display_names = {
"red_spymaster": "Red Spymaster",
"red_operative": "Red Operative",
"blue_spymaster": "Blue Spymaster",
"blue_operative": "Blue Operative",
}
return display_names.get(agent_id, agent_id)
class CodenamesPlugin(MultiAgentSystemPlugin):
"""Multi-agent Codenames plugin with 4 players (2 teams).
Game flow:
1. Red Spymaster gives clue
2. Red Operative guesses (can make multiple guesses)
3. Blue Spymaster gives clue
4. Blue Operative guesses
5. Repeat until one team wins or max turns reached
Win conditions:
- Find all your team's words
- Opponent hits the assassin
Lose conditions:
- Hit the assassin
- Opponent finds all their words first
"""
def __init__(self) -> None:
self._state = MultiAgentState()
self._game_state: CodenamesGameState | None = None
self._agents: dict[str, CodenamesAgent] = {}
self._config: CodenamesConfig | None = None
def get_game_id(self) -> str:
return "codenames"
def get_display_name(self) -> str:
return "Codenames (4-player word game)"
def get_default_config(self, level: int) -> CodenamesConfig:
"""Default config for the given difficulty level."""
return CodenamesConfig(complexity_level=level)
def list_agent_ids(self) -> list[str]:
return list(CODENAMES_AGENTS)
def reset(
self,
seed: int | None = None,
config: MultiAgentConfig | None = None,
) -> None:
"""Initialize a new game with the given seed and config."""
if seed is not None:
random.seed(seed)
# Parse config
cfg = config if isinstance(config, CodenamesConfig) else CodenamesConfig()
cfg.validate()
self._config = cfg
# Generate board (uses WATCHDOG_LLM_BACKEND for LLM selection)
board = generate_board(
seed=seed,
complexity_level=cfg.complexity_level,
red_words=cfg.red_words,
blue_words=cfg.blue_words,
neutral_words=cfg.neutral_words,
assassin_words=cfg.assassin_words,
)
# Initialize game state
self._game_state = CodenamesGameState(
board=board,
current_team=cfg.starting_team,
current_phase="clue",
max_turns=cfg.max_turns,
)
# Create agents (uses WATCHDOG_LLM_BACKEND for LLM selection)
self._agents = create_agents()
# Initialize plugin state with conversation_log (matching Cicero pattern)
self._state = MultiAgentState(
step_index=0,
turns_so_far=[],
config=cfg,
done=False,
conversation_log=[],
metadata={
"game_id": "codenames",
"board_words": board.words,
"starting_team": cfg.starting_team,
},
)
def get_state(self) -> MultiAgentState:
return self._state
def get_game_state(self) -> CodenamesGameState | None:
"""Get the internal Codenames game state (for testing/debugging)."""
return self._game_state
def generate_step(self, seed: int | None, step_index: int) -> MultiAgentStep:
"""Generate one step of the game.
Each step is one agent's action:
- Spymaster giving a clue
- Operative making a guess (or passing)
Multiple guess steps may occur in sequence for the same operative.
"""
if seed is not None:
random.seed(seed)
if self._game_state is None or self._config is None:
# Return empty step if not initialized
return MultiAgentStep(
turns=[],
done=True,
step_index=step_index,
game_id=self.get_game_id(),
)
game = self._game_state
# Check if game is already over
if game.game_over:
return self._finalize_step(step_index, done=True, message="Game already over")
# Get current agent
current_agent_id = game.get_current_agent_id()
agent = self._agents.get(current_agent_id)
if agent is None:
return self._finalize_step(step_index, done=True, message=f"Unknown agent: {current_agent_id}")
# Get agent's action
action = agent.get_action(game)
turns: list[AgentTurn] = []
display_name = _get_agent_display_name(current_agent_id)
if game.current_phase == "clue":
# Spymaster giving clue
if isinstance(action, ClueAction):
game.process_clue(action.clue_word, action.clue_number, action.reasoning)
action_text = f"CLUE: \"{action.clue_word}\" {action.clue_number}"
if action.reasoning:
action_text += f" (Reasoning: {action.reasoning})"
turn = AgentTurn(
agent_id=current_agent_id,
action_text=action_text,
step_index=step_index,
phase="clue",
display_name=display_name,
metadata={
"clue_word": action.clue_word,
"clue_number": action.clue_number,
"reasoning": action.reasoning,
"team": game.current_team,
"role": "Spymaster",
"phase": "clue",
},
)
turns.append(turn)
# Add to conversation log (matching Cicero pattern)
append_to_conversation_log(
self._state,
speaker_id=current_agent_id,
speaker_display=display_name,
message=action_text,
phase="clue",
team=game.current_team,
)
else: # guess phase
# Operative making guess
if isinstance(action, GuessAction):
if action.pass_turn:
# Operative passes
game.pass_turn()
action_text = f"PASS: Ending turn. (Reasoning: {action.reasoning})"
turn = AgentTurn(
agent_id=current_agent_id,
action_text=action_text,
step_index=step_index,
phase="guess_pass",
display_name=display_name,
metadata={
"pass": True,
"reasoning": action.reasoning,
"team": agent.team,
"role": "Operative",
"phase": "guess_pass",
},
)
turns.append(turn)
append_to_conversation_log(
self._state,
speaker_id=current_agent_id,
speaker_display=display_name,
message=action_text,
phase="guess_pass",
team=agent.team,
)
else:
# Process the guess
continue_guessing, result_message = game.process_guess(
action.guessed_word, action.reasoning
)
action_text = f"GUESS: \"{action.guessed_word}\" - {result_message}"
if action.reasoning:
action_text += f" (Reasoning: {action.reasoning})"
turn = AgentTurn(
agent_id=current_agent_id,
action_text=action_text,
step_index=step_index,
phase="guess",
display_name=display_name,
metadata={
"guessed_word": action.guessed_word,
"result": result_message,
"reasoning": action.reasoning,
"team": agent.team,
"continue_guessing": continue_guessing,
"role": "Operative",
"phase": "guess",
},
)
turns.append(turn)
append_to_conversation_log(
self._state,
speaker_id=current_agent_id,
speaker_display=display_name,
message=action_text,
phase="guess",
team=agent.team,
guessed_word=action.guessed_word,
result=result_message,
)
# If wrong guess or max guesses reached, end turn
if not continue_guessing and not game.game_over:
game.end_turn()
# Update state
self._state.step_index = step_index + 1
self._state.turns_so_far.extend(turns)
self._state.done = game.game_over
if game.game_over:
self._state.metadata["winner"] = game.winner
self._state.metadata["game_over_reason"] = game.game_over_reason
return MultiAgentStep(
turns=turns,
done=game.game_over,
step_index=step_index,
game_id=self.get_game_id(),
state=self._create_state_snapshot(),
)
def _finalize_step(self, step_index: int, done: bool, message: str = "") -> MultiAgentStep:
"""Create a final step when game ends or error occurs."""
self._state.step_index = step_index + 1
self._state.done = done
return MultiAgentStep(
turns=[],
done=done,
step_index=step_index,
game_id=self.get_game_id(),
state=self._create_state_snapshot(),
)
def _create_state_snapshot(self) -> MultiAgentState:
"""Create a snapshot of current state for the step output."""
return MultiAgentState(
step_index=self._state.step_index,
turns_so_far=list(self._state.turns_so_far),
config=self._state.config,
done=self._state.done,
metadata=dict(self._state.metadata),
conversation_log=list(self._state.conversation_log),
)
def get_full_game_state(self) -> dict[str, Any]:
"""Get the complete serialized game state for recording."""
if self._game_state is None:
return {}
return {
"game_state": self._game_state.to_dict(),
"plugin_state": {
"step_index": self._state.step_index,
"done": self._state.done,
"turns_count": len(self._state.turns_so_far),
"metadata": self._state.metadata,
},
}