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
**Pawn Shop Items**: Group ALL pawn shop items under the Village (area_8) subgraph:
- Nightingale, Mint, Tinderbox, Flute, Paintbrush, Ink
- These items originate from the Village pawn shop broker interaction
**FLAT STRUCTURE ONLY - No Nested Subgraphs**:
- Every subgraph must be top-level
- 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:
- Smell, Hearing, Taste, Touch, Sight gnome satisfaction outcomes
- These are all part of the Five Senses puzzle sequence
**Same Area = Same Color**:
- If an area (e.g., Isle of Crown) appears twice (start AND end), use the SAME color
- Don't create new colors for repeated areas
- Repeat areas indicate logical separation in game progression
**Example of CORRECT structure**:
```mermaid
subgraph area_8["<style>subgraphTitleTitle {font-size: 18px; font-weight: bold;}</style>Village - Pawn Shop Items"]
classDef area_8 fill:#F5F5F5,stroke:#616161,stroke-width:2px
class O_RECEIVE_NIGHTINGALE,O_RECEIVE_MINT,O_RECEIVE_TINDERBOX,O_RECEIVE_FLUTE,O_RECEIVE_PAINTBRUSH,O_RECEIVE_INK area_8
subgraph area_1["**ISLE OF CROWN**"]
%% All Isle of Crown Phase 1 content here
end
subgraph area_2_gnomes["<style>subgraphTitleTitle {font-size: 18px; font-weight: bold;}</style>Isle of Wonder - Five Senses Gnomes"]
classDef area_2_gnomes fill:#FFF3E0,stroke:#F57C00,stroke-width:2px
class O_GNOMES_SMELL_DONE,O_GNOMES_HEARING_DONE,O_GNOMES_TASTE_DONE,O_GNOMES_TOUCH_DONE,O_GNOMES_SIGHT_DONE area_2_gnomes
subgraph area_2["**ISLE OF WONDER**"]
%% All Isle of Wonder content here
end
subgraph area_1_return["**ISLE OF CROWN - Final**"]
%% All Isle of Crown Final Phase content here (same color as area_1)
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
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
- [ ] 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
- [ ] 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
- [ ] **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

View File

@@ -16,53 +16,61 @@ def parse_mermaid_file(filepath):
with open(filepath, 'r') as f:
content = f.read()
# Skip comments
lines = [l for l in content.split('\n') if not l.strip().startswith('%%')]
lines = content.split('\n')
for line in lines:
# Skip special directives
if line.strip().startswith('subgraph '):
# Skip comments and directives
stripped = line.strip()
if stripped.startswith('%%'):
continue
if line.strip().startswith('classDef '):
if stripped.startswith('subgraph '):
continue
if line.strip().startswith('direction '):
if stripped.startswith('classDef '):
continue
if stripped.startswith('direction '):
continue
# Find all edge patterns (any arrow type: -->, -.->, ---, etc.)
# Pattern captures: left_nodes --> right_node
# Handle multi-source: A & B & C --> D
# Find all edges
if '-->' in line or '-.->' in line or '---' in line:
# Find the arrow
arrow_match = re.search(r'(-+[>-])', line)
if arrow_match:
arrow_pos = arrow_match.start()
left_part = line[:arrow_pos]
left_part = line[:arrow_match.start()]
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)
# Get right side nodes
right_nodes = re.findall(r'[A-Z_][A-Z0-9_]*(?:\[[^\]]*\])?', right_part)
for node in left_nodes:
node = re.sub(r'\[.*', '', node)
for node_orig in left_nodes:
node = re.sub(r'\[.*', '', node_orig)
if node and re.match(r'^[A-Z_][A-Z0-9_]*$', node):
defined_on_left.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:
node = re.sub(r'\[.*', '', node)
# Extract nodes from right side
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):
referenced_on_right.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.)
standalone = re.findall(r'(?<!\w)[A-Z_][A-Z0-9_]*(?=\s|$)', line)
for node in standalone:
if node not in ['subgraph', 'direction', 'TD', 'LR', 'RL', 'BT']:
if re.match(r'^[A-Z_][A-Z0-9_]*$', node):
# Find standalone node definitions (no incoming edge, defined with label)
# Match: NODE["label"] or NODE[label] anywhere in line
standalone_with_label = re.findall(r'[A-Z_][A-Z0-9_]*(?=\[[^\]]*\])', line)
for node in standalone_with_label:
if node and re.match(r'^[A-Z_][A-Z0-9_]*$', node):
defined_standalone.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)
@@ -88,6 +96,7 @@ def main():
print("=== Parsing complete ===")
print(f"Total unique nodes: {len(all_nodes)}")
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()
@@ -138,7 +147,7 @@ def main():
for node in sorted(referenced_on_right):
if node in ['TD', 'LR', 'RL', 'BT', 'END']:
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}")
undefined.append(node)
@@ -160,6 +169,13 @@ def main():
'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_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]
@@ -174,6 +190,7 @@ def main():
print("✓ PASS: No problematic dangling nodes detected")
sys.exit(0)
else:
if dead_ends:
print()
print("Note: The following are acceptable terminal story items:")
for t in dead_ends:

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