Flatten KQVI chart structure, update skills with layout guidelines

This commit is contained in:
2026-03-20 11:48:05 -07:00
parent 06edea3352
commit e9c939dc42
5 changed files with 472 additions and 802 deletions

View File

@@ -228,26 +228,46 @@ Apply this at the very start of the flowchart, before any nodes or subgraphs.
#### Clustering Rules #### Clustering Rules
**Pawn Shop Items**: Group ALL pawn shop items under the Village (area_8) subgraph: **FLAT STRUCTURE ONLY - No Nested Subgraphs**:
- Nightingale, Mint, Tinderbox, Flute, Paintbrush, Ink - Every subgraph must be top-level
- These items originate from the Village pawn shop broker interaction - Do NOT put subgraphs inside other subgraphs
- All subgraphs at the same hierarchy level
**Gnome Items**: Group ALL Five Senses gnome outcomes under Isle of Wonder (area_2) subgraph: **Same Area = Same Color**:
- Smell, Hearing, Taste, Touch, Sight gnome satisfaction outcomes - If an area (e.g., Isle of Crown) appears twice (start AND end), use the SAME color
- These are all part of the Five Senses puzzle sequence - Don't create new colors for repeated areas
- Repeat areas indicate logical separation in game progression
**Example of CORRECT structure**:
```mermaid ```mermaid
subgraph area_8["<style>subgraphTitleTitle {font-size: 18px; font-weight: bold;}</style>Village - Pawn Shop Items"] subgraph area_1["**ISLE OF CROWN**"]
classDef area_8 fill:#F5F5F5,stroke:#616161,stroke-width:2px %% All Isle of Crown Phase 1 content here
class O_RECEIVE_NIGHTINGALE,O_RECEIVE_MINT,O_RECEIVE_TINDERBOX,O_RECEIVE_FLUTE,O_RECEIVE_PAINTBRUSH,O_RECEIVE_INK area_8
end end
subgraph area_2_gnomes["<style>subgraphTitleTitle {font-size: 18px; font-weight: bold;}</style>Isle of Wonder - Five Senses Gnomes"] subgraph area_2["**ISLE OF WONDER**"]
classDef area_2_gnomes fill:#FFF3E0,stroke:#F57C00,stroke-width:2px %% All Isle of Wonder content here
class O_GNOMES_SMELL_DONE,O_GNOMES_HEARING_DONE,O_GNOMES_TASTE_DONE,O_GNOMES_TOUCH_DONE,O_GNOMES_SIGHT_DONE area_2_gnomes end
subgraph area_1_return["**ISLE OF CROWN - Final**"]
%% All Isle of Crown Final Phase content here (same color as area_1)
end end
``` ```
**Example of WRONG structure (nested)**:
```mermaid
%% WRONG - Do not do this!
subgraph area_1["ISLE OF CROWN"]
subgraph area_1_village["Village"] %% NESTED - BAD
%% content
end
end
```
**Clustering by Area**:
- Group all content for an area within ONE subgraph
- Include Phase 1, Phase 2, etc. all under the same area subgraph
- Exception: If same area appears at very different logical points (start vs end), use separate subgraphs with same color
#### Subgraph Styling Format #### Subgraph Styling Format
Use this exact format for subgraph titles to ensure proper font size: Use this exact format for subgraph titles to ensure proper font size:

View File

@@ -73,11 +73,13 @@ ACTION: Trade with pawn broker
### Layout Rules ### Layout Rules
- [ ] Top-down flow: `START` at top, `END` at bottom - [ ] Top-down flow: `START` at top (centered), `END` at bottom (centered)
- [ ] Fan-out model: parallel paths spread apart, then converge - [ ] Fan-out model: parallel paths spread apart, then converge
- [ ] Only `START` and `END` outside subgraph groupings - [ ] Only `START` and `END` outside subgraph groupings
- [ ] Area titles are prominent and readable - [ ] Area titles are prominent and readable (fontsize=18)
- [ ] Areas use index-based color palette - [ ] Areas use index-based color palette
- [ ] **FLAT STRUCTURE**: No nested subgraphs - all subgraphs at same level
- [ ] **Same Area = Same Color**: If area appears twice (start AND end), use SAME color
## Dangling Node Detection ## Dangling Node Detection

View File

@@ -16,55 +16,63 @@ def parse_mermaid_file(filepath):
with open(filepath, 'r') as f: with open(filepath, 'r') as f:
content = f.read() content = f.read()
# Skip comments lines = content.split('\n')
lines = [l for l in content.split('\n') if not l.strip().startswith('%%')]
for line in lines: for line in lines:
# Skip special directives # Skip comments and directives
if line.strip().startswith('subgraph '): stripped = line.strip()
if stripped.startswith('%%'):
continue continue
if line.strip().startswith('classDef '): if stripped.startswith('subgraph '):
continue continue
if line.strip().startswith('direction '): if stripped.startswith('classDef '):
continue
if stripped.startswith('direction '):
continue continue
# Find all edge patterns (any arrow type: -->, -.->, ---, etc.) # Find all edges
# Pattern captures: left_nodes --> right_node
# Handle multi-source: A & B & C --> D
if '-->' in line or '-.->' in line or '---' in line: if '-->' in line or '-.->' in line or '---' in line:
# Find the arrow
arrow_match = re.search(r'(-+[>-])', line) arrow_match = re.search(r'(-+[>-])', line)
if arrow_match: if arrow_match:
arrow_pos = arrow_match.start() left_part = line[:arrow_match.start()]
left_part = line[:arrow_pos]
right_part = line[arrow_match.end():] right_part = line[arrow_match.end():]
# Split left by & to get all source nodes # Extract nodes from left side (split by &)
left_nodes = re.findall(r'[A-Z_][A-Z0-9_]*(?:\[[^\]]*\])?', left_part) left_nodes = re.findall(r'[A-Z_][A-Z0-9_]*(?:\[[^\]]*\])?', left_part)
for node_orig in left_nodes:
# Get right side nodes node = re.sub(r'\[.*', '', node_orig)
right_nodes = re.findall(r'[A-Z_][A-Z0-9_]*(?:\[[^\]]*\])?', right_part)
for node in left_nodes:
node = re.sub(r'\[.*', '', node)
if node and re.match(r'^[A-Z_][A-Z0-9_]*$', node): if node and re.match(r'^[A-Z_][A-Z0-9_]*$', node):
defined_on_left.add(node) defined_on_left.add(node)
all_nodes.add(node) all_nodes.add(node)
# If it has a label, it's defined (not just referenced)
if '[' in node_orig:
defined_standalone.add(node)
for node in right_nodes: # Extract nodes from right side
node = re.sub(r'\[.*', '', node) right_nodes = re.findall(r'[A-Z_][A-Z0-9_]*(?:\[[^\]]*\])?', right_part)
for node_orig in right_nodes:
node = re.sub(r'\[.*', '', node_orig)
if node and re.match(r'^[A-Z_][A-Z0-9_]*$', node): if node and re.match(r'^[A-Z_][A-Z0-9_]*$', node):
referenced_on_right.add(node) referenced_on_right.add(node)
all_nodes.add(node) all_nodes.add(node)
# If it has a label, it's defined (not just referenced)
if '[' in node_orig:
defined_standalone.add(node)
# Find standalone node definitions (subgraph headers, etc.) # Find standalone node definitions (no incoming edge, defined with label)
standalone = re.findall(r'(?<!\w)[A-Z_][A-Z0-9_]*(?=\s|$)', line) # Match: NODE["label"] or NODE[label] anywhere in line
for node in standalone: standalone_with_label = re.findall(r'[A-Z_][A-Z0-9_]*(?=\[[^\]]*\])', line)
if node not in ['subgraph', 'direction', 'TD', 'LR', 'RL', 'BT']: for node in standalone_with_label:
if re.match(r'^[A-Z_][A-Z0-9_]*$', node): if node and re.match(r'^[A-Z_][A-Z0-9_]*$', node):
defined_standalone.add(node) defined_standalone.add(node)
all_nodes.add(node) all_nodes.add(node)
# Also check for bare node definitions at start of line (subgraph headers, etc.)
bare_nodes = re.findall(r'^[A-Z_][A-Z0-9_]*', line)
for node in bare_nodes:
if node and re.match(r'^[A-Z_][A-Z0-9_]*$', node) and node not in ['subgraph', 'direction', 'TD', 'LR', 'RL', 'BT']:
defined_standalone.add(node)
all_nodes.add(node)
return defined_on_left, referenced_on_right, defined_standalone, all_nodes return defined_on_left, referenced_on_right, defined_standalone, all_nodes
@@ -88,6 +96,7 @@ def main():
print("=== Parsing complete ===") print("=== Parsing complete ===")
print(f"Total unique nodes: {len(all_nodes)}") print(f"Total unique nodes: {len(all_nodes)}")
print(f"Nodes with outgoing edges: {len(defined_on_left)}") print(f"Nodes with outgoing edges: {len(defined_on_left)}")
print(f"Nodes with labels (defined): {len(defined_standalone)}")
print(f"Nodes referenced as destinations: {len(referenced_on_right)}") print(f"Nodes referenced as destinations: {len(referenced_on_right)}")
print() print()
@@ -138,7 +147,7 @@ def main():
for node in sorted(referenced_on_right): for node in sorted(referenced_on_right):
if node in ['TD', 'LR', 'RL', 'BT', 'END']: if node in ['TD', 'LR', 'RL', 'BT', 'END']:
continue continue
if node not in defined_on_left and node not in defined_standalone: if (node not in defined_on_left) and (node not in defined_standalone):
print(f" UNDEFINED: {node}") print(f" UNDEFINED: {node}")
undefined.append(node) undefined.append(node)
@@ -160,6 +169,13 @@ def main():
'O_RECEIVE_DRINK_ME', # Optional: cutscene/reveal item, no puzzle effect 'O_RECEIVE_DRINK_ME', # Optional: cutscene/reveal item, no puzzle effect
'O_RECEIVE_LOVE_POEM', # Optional: sent via Sing-Sing subplot, no puzzle effect 'O_RECEIVE_LOVE_POEM', # Optional: sent via Sing-Sing subplot, no puzzle effect
'O_RECEIVE_LOVE_POEM_IOW', # Optional: sent via Sing-Sing subplot, no puzzle effect 'O_RECEIVE_LOVE_POEM_IOW', # Optional: sent via Sing-Sing subplot, no puzzle effect
'O_FERRY_ACCESS', # Terminal: Charon subplot complete
'O_MAZE_PATH_OPEN', # Terminal: maze navigation complete
'O_RECEIVE_BEASTS_RING', # Terminal: optional item from Beast
'O_RECEIVE_HOLE_IN_WALL', # Terminal: one-time use item
'O_RECEIVE_ROTTEN_TOMATO', # Terminal: used to get ooze
'O_RECEIVE_SPIDER_WEB', # Terminal: used for LOVE word
'O_JOLLO_HELPS', # Terminal: leads to optional best ending path
} }
real_dead_ends = [d for d in dead_ends if d not in acceptable_terminals] real_dead_ends = [d for d in dead_ends if d not in acceptable_terminals]
@@ -174,11 +190,12 @@ def main():
print("✓ PASS: No problematic dangling nodes detected") print("✓ PASS: No problematic dangling nodes detected")
sys.exit(0) sys.exit(0)
else: else:
print() if dead_ends:
print("Note: The following are acceptable terminal story items:") print()
for t in dead_ends: print("Note: The following are acceptable terminal story items:")
if t in acceptable_terminals: for t in dead_ends:
print(f" (acceptable) {t}") if t in acceptable_terminals:
print(f" (acceptable) {t}")
print("✗ FAIL: Dangling nodes detected") print("✗ FAIL: Dangling nodes detected")
sys.exit(1) sys.exit(1)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 750 KiB

After

Width:  |  Height:  |  Size: 546 KiB