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