Wargame Engine → LangGraph Mapping
Status: ACTIVE (reference) Agent: opencode/ext-agent (sandshrew) Timestamp UTC: 2026-05-11T22:15:00Z Claim: synthesis | 2026-05-11T22:10:00Z Session: Mapping Wargame Engine features to LangGraph architecture. Scope updated: 3 units.
Wargame Engine Features That Map Directly
1. Node-Tree Viewport → LangGraph Nodes
Wargame: "A tree of nodes is used as a viewport, with each node usually representing either a view of sprites, or the sprites themselves."
LangGraph mapping: The viewport node tree mirrors the LangGraph graph topology. Each Wargame node is the visual skin of a LangGraph node. The viewport node doesn't own state — it reads LangGraph state and renders accordingly.
# Wargame viewport node = visual skin
# LangGraph graph node = state + logic
# When unit enters node_14 in LangGraph:
viewport.get_node("node_14").set_color("active") # highlight on screen
viewport.get_node("node_14").set_sprite("unit/rif") # show unit sprite
viewport.get_node("node_14").set_label("Rif — Design") # show unit name
# When unit leaves:
viewport.get_node("node_14").set_color("completed") # dim or recolor
viewport.get_node("node_14").clear_sprite() # remove unit sprite
Opportunity: 1:1 mapping. 30 LangGraph nodes = 30 viewport nodes. No translation layer needed.
2. Scene System → Game Phase Views
Wargame: Discrete scenes that can be switched. controller.add_scene('planning', scene) → controller.run('planning').
LangGraph mapping: Each game phase gets its own scene. The underlying LangGraph state is the same across all scenes — only the rendering changes.
| Scene | What It Shows | LangGraph State Source |
|---|---|---|
grid |
30-node grid with units | nodes, unit.positions |
unit_detail |
Single unit config/trail/access | unit.config, unit.trail_context |
node_output |
Node's output summary sections | nodes[node].output_summary |
move_config |
Pre-move config menu | unit.config, unit.trail_context |
route_curate |
Cross-node curation interface | nodes[source].output_summary, nodes[target].config |
interrupt_prompt |
interrupt() dialog | interrupt return value |
Opportunity: Scene switching is a rendering-layer concern. LangGraph doesn't know it happened. The same state powers every scene. 3 units = 3 independent detail views.
3. Messaging System → Input → Invoke Bridge
Wargame: "A messaging system is used to implement the nodes communicating with themselves, which allows for total separation of systems."
LangGraph mapping: The messaging system is the event bridge between player input and LangGraph invocations. A viewport node receives a "clicked" message → dispatches to input handler → builds invoke payload → fires graph.invoke() → state updates → viewport re-renders from new state.
# Wargame messaging flow
class InputHandler:
def on_viewport_click(self, node_id):
# Player clicked a tile on the grid
if has_unit(node_id):
action = self.show_unit_menu(node_id)
else:
action = self.show_node_menu(node_id)
# Dispatch to LangGraph
result = graph.invoke(action, config)
# Wargame re-renders from updated state
self.controller.refresh_from_state(result)
Opportunity: Total separation. The viewport doesn't know about LangGraph. LangGraph doesn't know about the viewport. The messaging system carries JSON both ways.
4. GUI System → Interrupt Prompts
Wargame: Built-in GUI elements — windows, images, containers.
LangGraph mapping: Every interrupt() call returns structured data. The GUI system renders it as dialogs, menus, text inputs. The player's response flows back through Command(resume=...).
# LangGraph node calls interrupt
response = interrupt({
"type": "menu",
"title": "Design Node — Review Output",
"options": ["accept", "correct", "pull_more_context"],
"chain_of_thought": "...",
"current_output": "..."
})
# Wargame GUI renders it:
# ┌──────────────────────────────────────┐
# │ Design Node — Review Output │
# │ "Recommend Config X..." │
# │ [Accept] [Correct] [Pull Context] │
# └──────────────────────────────────────┘
# Player selects → GUI returns "accept" → LangGraph resumes
Opportunity: The interrupt is a structured dict. The GUI renders it. The 3 units can have 3 simultaneous interrupt prompts (different threads, different GUIs).
5. Tween System → Unit Movement Animations
Wargame: "A tween system for sprite animations."
LangGraph mapping: When a unit's position changes in state, the tween animates the sprite from old position to new position. LangGraph state updates are instant. The tween smooths the visual transition.
# State: unit.position = "node_14" → "node_15"
# Viewport: tween sprite from node_14 pixel coords to node_15 pixel coords
viewport.animate_sprite(
unit_id="rif",
from_pos=hex_to_pixel("node_14"),
to_pos=hex_to_pixel("node_15"),
duration=0.3 # seconds
)
Opportunity: Purely visual. LangGraph doesn't know about animations. State changes are atomic. The renderer smooths them.
6. Save/Load + Replay → Complements Checkpointer
Wargame: "Automatic game saving, loading and replays."
LangGraph mapping: Wargame saves/loads the visual state (viewport node positions, sprites, scene). LangGraph checkpointer saves/loads the game state (node statuses, unit configs, access lists, output summaries). Combined: full persistence. Replay replays both.
| Layer | Persistence Mechanism | What It Saves |
|---|---|---|
| LangGraph | SqliteSaver checkpointer | Game state (nodes, units, outputs, prompts, history) |
| Wargame | Built-in save/load | Visual state (sprite positions, animations, scene) |
| Combined | Both triggered on same event | Full game snapshot |
Opportunity: No conflict. They save different things. The checkpointer is the source of truth for game logic. Wargame's save is the source of truth for visual continuity.
7. Config Loaders → Graph Initialization
Wargame: "Builtin loaders for images, sounds and config files."
LangGraph mapping: Wargame's config loader reads node definitions, phase labels, initial agent configs from files. These are written to LangGraph state at graph initialization.
# config/nodes.yaml (loaded by Wargame)
nodes:
node_01:
phase: planning
config:
prompt_template: "Define scope for: {task}"
default_model: deepseek-v4
node_14:
phase: design
config:
prompt_template: "Design solution for: {task}"
default_model: kimi-k2.6
# Loaded at graph init → written to state["nodes"]
for node_id, node_data in wargame_config.nodes.items():
state["nodes"][node_id] = node_data
Opportunity: Config files = graph definition. Edit YAML, restart graph, new config takes effect. No code changes.
8. Terminal → Live State Debugging
Wargame: "A terminal to interact with any program whilst the code is running."
LangGraph mapping: While the game runs on RG, the Wargame terminal provides a REPL into LangGraph state. Inspect state["nodes"], check state["unit"]["trail_context"], manually invoke a node. Debug without stopping the game.
# In Wargame terminal while game is running:
>>> state = graph.get_state(config)
>>> state["nodes"]["node_14"]["output_summary"]["sections"][0]
{"header": "Recommendation", "body": "Use SqliteSaver...", "status": "final"}
>>> state["unit"]["trail_context"]
["node_01/curated", "node_08/curated"]
Opportunity: Live debugging of LangGraph state from the same runtime. No SSH needed. The terminal is attached to the game process.
9. Logging → Full Traceability
Wargame: "A logging system to help pinpoint errors."
LangGraph mapping: Every invoke, state change, agent call, and interrupt logged through Wargame's system. Combined with LangGraph's built-in metadata (langgraph_step, langgraph_node, langgraph_triggers).
# Combined log output
[Turn 3 | Step 17] node_14 activated. Triggered by: unit_move.
[Turn 3 | Step 17] Agent call: deepseek-v4/opencode. Prompt: 240 tokens.
[Turn 3 | Step 17] Agent response: 180 tokens. Latency: 1.2s.
[Turn 3 | Step 17] interrupt: "Review output." Options: [accept, correct, pull]
[Turn 3 | Step 17] Player: accept. Node_14 → completed.
Opportunity: Debugging surface that spans both engine and game logic. Trace every decision, every latency spike, every player action.
3 Units: What Changes
Moving from 1 unit to 3:
| Aspect | 1 Unit | 3 Units |
|---|---|---|
| State | state["unit"] = single dict |
state["units"]["rif"], state["units"]["echo"], state["units"]["sherpa"] |
| Parallelism | Single invoke() per turn | 3 units can move simultaneously via Send fan-out |
| Access lists | One unit's access | Each unit has independent access. Can share context between them. |
| Renderer | One highlighted tile | 3 highlighted tiles, 3 sprites, 3 detail views |
| Interrupt | One prompt per turn | Up to 3 prompts (one per unit that needs human input) |
The Wargame viewport handles multiple units trivially — each unit is a sprite on a node. 3 units = 3 sprites. The scene system switches between grid view and per-unit detail views. The messaging system dispatches clicks to the correct unit.
Opportunity Summary
| Wargame Feature | LangGraph Equivalent | Value |
|---|---|---|
| Node-tree viewport | 30 graph nodes | 1:1 visual mapping, no translation |
| Scene system | Game phase views | Same state, different render modes |
| Messaging system | Input → invoke bridge | Total decoupling. JSON both ways. |
| GUI system | interrupt() prompts | Structured dict → rendered dialog |
| Tween system | Unit movement animations | Smooth visual transitions on state change |
| Save/load + replay | Checkpointer + visual persistence | Full game snapshot, complementary |
| Config loaders | Graph initialization | YAML-defined nodes, no code changes |
| Terminal | Live state debugging | REPL into LangGraph from game process |
| Logging | Full traceability | Every invoke, agent call, player action |