Mermaid visualization generators for OGS patterns.
Inspired by gds-viz, but adapted for game-theory-specific PatternIR.
Views:
1. Structural - block topology with composition operators
2. Architecture by Role - games grouped by game_type
3. Architecture by Domain - games grouped by tags
4. Game Hierarchy - nested composition tree
5. Flow Topology - covariant flow graph
6. Terminal Conditions - state transitions
structural_to_mermaid(pattern)
View 1: Structural - compiled game graph with composition topology.
Shows all games as nodes with their types, and all flows as edges.
Role-based styling: decision games are rectangles, functions are stadiums.
Source code in packages/gds-games/ogs/viz.py
| def structural_to_mermaid(pattern: PatternIR) -> str:
"""View 1: Structural - compiled game graph with composition topology.
Shows all games as nodes with their types, and all flows as edges.
Role-based styling: decision games are rectangles, functions are stadiums.
"""
lines = ["%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60}}}%%"]
lines.append("flowchart TD")
# Define nodes with shapes based on game type
for game in pattern.games:
node_id = _sanitize_id(game.name)
if game.game_type == GameType.DECISION:
# Rectangle for decision games
lines.append(f' {node_id}["{game.name}"]')
elif game.game_type == GameType.FUNCTION_COVARIANT:
# Stadium for covariant functions
lines.append(f' {node_id}(["{game.name}"])')
elif game.game_type == GameType.FUNCTION_CONTRAVARIANT:
# Cylinder for contravariant functions
lines.append(f" {node_id}[({game.name})]")
else:
# Default rectangle
lines.append(f' {node_id}["{game.name}"]')
# Add flows as edges
for flow in pattern.flows:
source_id = _sanitize_id(flow.source)
target_id = _sanitize_id(flow.target)
# Style edges based on flow type
if (
flow.is_feedback
or flow.is_corecursive
or flow.direction == FlowDirection.CONTRAVARIANT
):
lines.append(f' {source_id} -.->|"{flow.label}"| {target_id}')
else:
lines.append(f' {source_id} -->|"{flow.label}"| {target_id}')
return "\n".join(lines)
|
architecture_by_role_to_mermaid(pattern)
View 2: Architecture by Role - games grouped by game_type.
Groups games by their GameType (decision, function_covariant, etc.).
Source code in packages/gds-games/ogs/viz.py
| def architecture_by_role_to_mermaid(pattern: PatternIR) -> str:
"""View 2: Architecture by Role - games grouped by game_type.
Groups games by their GameType (decision, function_covariant, etc.).
"""
lines = ["%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 80}}}%%"]
lines.append("flowchart TD")
# Group games by type
by_type: dict[GameType, list] = {}
for game in pattern.games:
by_type.setdefault(game.game_type, []).append(game)
# Create subgraphs for each type
for game_type, games in sorted(by_type.items(), key=lambda x: x[0].value):
type_name = game_type.value.replace("_", " ").title()
lines.append(f" subgraph {game_type.value} [{type_name}]")
for game in games:
node_id = _sanitize_id(game.name)
lines.append(f' {node_id}["{game.name}"]')
lines.append(" end")
# Add flows between subgraphs
for flow in pattern.flows:
source_id = _sanitize_id(flow.source)
target_id = _sanitize_id(flow.target)
lines.append(f' {source_id} -->|"{flow.label}"| {target_id}')
return "\n".join(lines)
|
architecture_by_domain_to_mermaid(pattern, tag_key='domain')
View 3: Architecture by Domain - games grouped by tag.
Groups games by a tag key (default: "domain"). Games without
the tag go to "ungrouped".
Source code in packages/gds-games/ogs/viz.py
| def architecture_by_domain_to_mermaid(
pattern: PatternIR, tag_key: str = "domain"
) -> str:
"""View 3: Architecture by Domain - games grouped by tag.
Groups games by a tag key (default: "domain"). Games without
the tag go to "ungrouped".
"""
lines = ["%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 80}}}%%"]
lines.append("flowchart TD")
# Group games by tag value
by_domain: dict[str, list] = {}
ungrouped = []
for game in pattern.games:
tag_value = game.tags.get(tag_key) if game.tags else None
if tag_value:
by_domain.setdefault(tag_value, []).append(game)
else:
ungrouped.append(game)
# Create subgraphs for each domain
# (prefix with "dom_" to avoid ID collisions with game nodes)
for domain, games in sorted(by_domain.items()):
safe_domain = "dom_" + _sanitize_id(domain)
lines.append(f' subgraph {safe_domain} ["{domain}"]')
for game in games:
node_id = _sanitize_id(game.name)
lines.append(f' {node_id}["{game.name}"]')
lines.append(" end")
# Ungrouped games
if ungrouped:
lines.append(' subgraph ungrouped ["Ungrouped"]')
for game in ungrouped:
node_id = _sanitize_id(game.name)
lines.append(f' {node_id}["{game.name}"]')
lines.append(" end")
# Add flows
for flow in pattern.flows:
source_id = _sanitize_id(flow.source)
target_id = _sanitize_id(flow.target)
lines.append(f' {source_id} -->|"{flow.label}"| {target_id}')
return "\n".join(lines)
|
hierarchy_to_mermaid(pattern)
View 4: Game Hierarchy - nested composition tree.
Shows the hierarchical composition structure
(sequential, parallel, feedback, corecursive).
Source code in packages/gds-games/ogs/viz.py
| def hierarchy_to_mermaid(pattern: PatternIR) -> str:
"""View 4: Game Hierarchy - nested composition tree.
Shows the hierarchical composition structure
(sequential, parallel, feedback, corecursive).
"""
lines = ["%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50}}}%%"]
lines.append("flowchart TD")
if not pattern.hierarchy:
return "\n".join([*lines, " No hierarchy information available"])
def render_node(node, parent_id: str | None = None, depth: int = 0) -> list[str]:
node_lines = []
node_id = _sanitize_id(node.id)
if node.composition_type:
# Composite node
type_label = (
node.composition_type.value if node.composition_type else "group"
)
label = f"{node.name} ({type_label})"
if node.exit_condition:
label += f"<br/>exit: {node.exit_condition[:30]}..."
node_lines.append(f' {node_id}["{label}"]')
if parent_id:
node_lines.append(f" {parent_id} --> {node_id}")
for child in node.children:
node_lines.extend(render_node(child, node_id, depth + 1))
else:
# Leaf node (atomic game)
game_name = node.block_name or node.name
node_lines.append(f' {node_id}["{game_name}"]')
if parent_id:
node_lines.append(f" {parent_id} --> {node_id}")
return node_lines
lines.extend(render_node(pattern.hierarchy))
return "\n".join(lines)
|
flow_topology_to_mermaid(pattern)
View 5: Flow Topology - covariant flow graph.
Shows only covariant (forward) flows, useful for understanding data flow.
Source code in packages/gds-games/ogs/viz.py
| def flow_topology_to_mermaid(pattern: PatternIR) -> str:
"""View 5: Flow Topology - covariant flow graph.
Shows only covariant (forward) flows, useful for understanding data flow.
"""
lines = ["%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60}}}%%"]
lines.append("flowchart LR")
# Only covariant flows
covariant_flows = [
f for f in pattern.flows if f.direction == FlowDirection.COVARIANT
]
# Collect all nodes that appear in covariant flows
node_names = set()
for flow in covariant_flows:
node_names.add(flow.source)
node_names.add(flow.target)
# Define nodes
for name in sorted(node_names):
node_id = _sanitize_id(name)
lines.append(f' {node_id}["{name}"]')
# Add covariant flows only
for flow in covariant_flows:
source_id = _sanitize_id(flow.source)
target_id = _sanitize_id(flow.target)
style = "-.->" if flow.is_corecursive else "-->"
lines.append(f' {source_id} {style}|"{flow.label}"| {target_id}')
return "\n".join(lines)
|
terminal_conditions_to_mermaid(pattern)
View 6: Terminal Conditions - state transitions.
Shows terminal conditions as state transitions.
Source code in packages/gds-games/ogs/viz.py
| def terminal_conditions_to_mermaid(pattern: PatternIR) -> str:
"""View 6: Terminal Conditions - state transitions.
Shows terminal conditions as state transitions.
"""
lines = ["%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60}}}%%"]
lines.append("stateDiagram-v2")
lines.append(" [*] --> Running")
if not pattern.terminal_conditions:
lines.append(" Running --> [*]")
return "\n".join(lines)
# Add terminal condition states
for tc in pattern.terminal_conditions:
tc_id = _sanitize_id(tc.name)
lines.append(f" Running --> {tc_id} : {tc.outcome}")
lines.append(f" {tc_id} : {tc.name}")
if tc.description:
lines.append(f" note right of {tc_id}")
lines.append(f" {tc.description[:60]}")
lines.append(" end note")
return "\n".join(lines)
|
generate_all_views(pattern)
Generate all 6 views and return as a dictionary.
Source code in packages/gds-games/ogs/viz.py
| def generate_all_views(pattern: PatternIR) -> dict[str, str]:
"""Generate all 6 views and return as a dictionary."""
return {
"structural": structural_to_mermaid(pattern),
"architecture_by_role": architecture_by_role_to_mermaid(pattern),
"architecture_by_domain": architecture_by_domain_to_mermaid(pattern),
"hierarchy": hierarchy_to_mermaid(pattern),
"flow_topology": flow_topology_to_mermaid(pattern),
"terminal_conditions": terminal_conditions_to_mermaid(pattern),
}
|