- Core engine: simulator, game mechanics, triggers (138 tests) - Fog-of-war per-player state tracking - Meeting flow: interrupt, discussion, voting, consolidation - Prompt assembler with strategy injection tiers - LLM client with fallbacks for models without JSON/system support - Prompt templates: action, discussion, voting, reflection - Full integration in main.py orchestrator - Verified working with free OpenRouter models (Gemma)
231 lines
6.2 KiB
Markdown
231 lines
6.2 KiB
Markdown
# Source Code Documentation
|
|
|
|
## Module Overview
|
|
|
|
### `src/engine/`
|
|
|
|
#### `simulator.py`
|
|
Discrete event simulator core.
|
|
|
|
```python
|
|
class Simulator:
|
|
def schedule_at(time: float, event_type: str, data: dict) -> Event
|
|
def schedule_in(delay: float, event_type: str, data: dict) -> Event
|
|
def step() -> Event | None # Process next event
|
|
def run_until(time: float) # Run until game time
|
|
def on(event_type: str, handler: Callable) # Register handler
|
|
```
|
|
|
|
#### `game.py`
|
|
Game engine with full mechanics.
|
|
|
|
```python
|
|
class GameEngine:
|
|
def add_player(id, name, color, role) -> Player
|
|
def queue_action(player_id, action_type, params) -> int
|
|
def resolve_actions() -> list[dict] # Priority-ordered resolution
|
|
def check_win_condition() -> str | None # "impostor", "crewmate", None
|
|
def get_impostor_context(player_id) -> dict # Fellow impostors
|
|
```
|
|
|
|
#### `triggers.py`
|
|
Event-driven trigger system.
|
|
|
|
```python
|
|
class TriggerRegistry:
|
|
def register_agent(agent_id)
|
|
def subscribe(agent_id, trigger_type)
|
|
def mute(agent_id, condition: TriggerCondition)
|
|
def should_fire(agent_id, trigger_type, time) -> bool
|
|
def get_agents_for_trigger(trigger_type, time) -> list[str]
|
|
```
|
|
|
|
#### `discussion.py`
|
|
Round-table discussion orchestrator.
|
|
|
|
```python
|
|
class DiscussionOrchestrator:
|
|
def calculate_priority(player_id, name, desire) -> int
|
|
def select_speaker(bids: dict) -> str | None
|
|
def add_message(player_id, name, message, target=None)
|
|
def advance_round(all_desires_low: bool) -> bool
|
|
def get_transcript() -> list[dict]
|
|
```
|
|
|
|
#### `types.py`
|
|
Core data structures.
|
|
|
|
```python
|
|
@dataclass
|
|
class Player: id, name, color, role, position, speed, is_alive, ...
|
|
class Position: room_id, edge_id, progress
|
|
class Body: id, player_id, player_name, position, time_of_death
|
|
class Event: time, event_type, data
|
|
class Role: CREWMATE, IMPOSTOR, GHOST
|
|
class GamePhase: LOBBY, PLAYING, DISCUSSION, VOTING, ENDED
|
|
```
|
|
|
|
#### `fog_of_war.py`
|
|
Per-player knowledge tracking (fog-of-war).
|
|
|
|
```python
|
|
class FogOfWarManager:
|
|
def register_player(player_id)
|
|
def update_vision(observer_id, visible_players, room_id, timestamp)
|
|
def witness_vent(observer_id, venter_id, ...) -> None
|
|
def witness_kill(observer_id, killer_id, victim_id, ...) -> None
|
|
def announce_death(player_id, via) # Broadcast to all
|
|
def get_player_game_state(player_id, full_state) -> dict # Filtered view
|
|
|
|
class PlayerKnowledge:
|
|
known_dead: set[str]
|
|
last_seen: dict[str, PlayerSighting]
|
|
witnessed_events: list[WitnessedEvent]
|
|
```
|
|
|
|
#### `available_actions.py`
|
|
Dynamic action generator per tick.
|
|
|
|
```python
|
|
class AvailableActionsGenerator:
|
|
def get_available_actions(player_id) -> dict
|
|
def to_prompt_context(player_id) -> dict # Compact for LLM
|
|
```
|
|
|
|
Returns: `movement`, `interactions`, `kills`, `sabotages` based on role/location.
|
|
|
|
#### `trigger_messages.py`
|
|
JSON schemas for trigger reasons.
|
|
|
|
```python
|
|
class TriggerMessageBuilder:
|
|
@staticmethod player_enters_fov(...) -> TriggerMessage
|
|
@staticmethod body_in_fov(...) -> TriggerMessage
|
|
@staticmethod vent_witnessed(...) -> TriggerMessage
|
|
@staticmethod kill_witnessed(...) -> TriggerMessage
|
|
@staticmethod death(...) -> TriggerMessage
|
|
# ... 15+ trigger types
|
|
```
|
|
|
|
#### `meeting_flow.py`
|
|
Full meeting lifecycle manager.
|
|
|
|
```python
|
|
class MeetingFlowManager:
|
|
def start_meeting(called_by, reason, body_location)
|
|
def submit_interrupt_note(player_id, note)
|
|
def submit_prep_thoughts(player_id, thoughts)
|
|
def add_message(speaker_id, speaker_name, message, target)
|
|
def submit_vote(player_id, vote)
|
|
def tally_votes() -> (ejected, details)
|
|
def end_meeting(ejected, was_impostor)
|
|
```
|
|
|
|
---
|
|
|
|
### `src/map/`
|
|
|
|
#### `graph.py`
|
|
Graph-based map representation.
|
|
|
|
```python
|
|
class GameMap:
|
|
def add_room(room: Room)
|
|
def add_edge(edge: Edge)
|
|
def get_neighbors(room_id) -> list[tuple[edge_id, room_id]]
|
|
def find_path(from_room, to_room) -> list[edge_id]
|
|
def path_distance(path: list[edge_id]) -> float
|
|
def load(path: str) -> GameMap # From JSON
|
|
def save(path: str) # To JSON
|
|
|
|
@dataclass
|
|
class Room: id, name, tasks: list[Task], vent: Vent | None
|
|
class Edge: id, room_a, room_b, distance
|
|
class Task: id, name, duration
|
|
class Vent: id, connects_to: list[str]
|
|
```
|
|
|
|
---
|
|
|
|
### `src/agents/`
|
|
|
|
#### `agent.py`
|
|
Stateless LLM agent wrapper.
|
|
|
|
```python
|
|
class Agent:
|
|
def get_action(game_context: dict, trigger: Trigger) -> dict
|
|
def get_discussion_response(context: dict, transcript: list) -> dict
|
|
def get_vote(context: dict, transcript: list) -> dict
|
|
def reflect(game_summary: dict) # Post-game learning
|
|
```
|
|
|
|
#### `scratchpads.py`
|
|
File-based persistent memory.
|
|
|
|
```python
|
|
class ScratchpadManager:
|
|
def read(name: str) -> dict
|
|
def write(name: str, content: dict)
|
|
def update(name: str, updates: dict) # Merge
|
|
def clear_game_specific() # Keep only learned.json
|
|
```
|
|
|
|
#### `prompt_assembler.py`
|
|
System + user prompt builder.
|
|
|
|
```python
|
|
class PromptAssembler:
|
|
def build_system_prompt(phase, game_settings, map_name, learned) -> str
|
|
def build_action_prompt(player_state, history, vision, actions, trigger) -> str
|
|
def build_discussion_prompt(player_state, transcript, meeting_scratchpad) -> str
|
|
def build_voting_prompt(player_state, transcript, vote_counts) -> str
|
|
def build_meeting_interrupt_prompt(player_state, interrupted_action) -> str
|
|
def build_consolidation_prompt(player_state, meeting_result, scratchpad) -> str
|
|
|
|
class PromptConfig:
|
|
model_name, persona, strategy_level, meta_level, is_impostor, fellow_impostors
|
|
```
|
|
|
|
---
|
|
|
|
### `src/llm/`
|
|
|
|
#### `client.py`
|
|
OpenRouter API wrapper.
|
|
|
|
```python
|
|
class LLMClient:
|
|
def chat(messages: list[dict], json_mode=True) -> dict
|
|
def chat_stream(messages: list[dict]) -> Iterator[str]
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### `config/game_settings.yaml`
|
|
```yaml
|
|
num_impostors: 2
|
|
kill_cooldown: 25.0
|
|
vision_range: 10.0
|
|
impostor_vision_multiplier: 1.5
|
|
light_sabotage_vision_multiplier: 0.25
|
|
emergencies_per_player: 1
|
|
confirm_ejects: true
|
|
```
|
|
|
|
### `data/maps/skeld.json`
|
|
```json
|
|
{
|
|
"rooms": [
|
|
{"id": "cafeteria", "name": "Cafeteria", "tasks": [...], "vent": null},
|
|
...
|
|
],
|
|
"edges": [
|
|
{"id": "cafe_weapons", "room_a": "cafeteria", "room_b": "weapons", "distance": 5.0},
|
|
...
|
|
]
|
|
}
|
|
```
|