File size: 12,565 Bytes
1070765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
"""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,
            },
        }