Flatten KQVI chart structure, update skills with layout guidelines
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -16,55 +16,63 @@ 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):
|
||||
defined_standalone.add(node)
|
||||
all_nodes.add(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)
|
||||
|
||||
return defined_on_left, referenced_on_right, defined_standalone, all_nodes
|
||||
|
||||
@@ -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,11 +190,12 @@ def main():
|
||||
print("✓ PASS: No problematic dangling nodes detected")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print()
|
||||
print("Note: The following are acceptable terminal story items:")
|
||||
for t in dead_ends:
|
||||
if t in acceptable_terminals:
|
||||
print(f" (acceptable) {t}")
|
||||
if dead_ends:
|
||||
print()
|
||||
print("Note: The following are acceptable terminal story items:")
|
||||
for t in dead_ends:
|
||||
if t in acceptable_terminals:
|
||||
print(f" (acceptable) {t}")
|
||||
print("✗ FAIL: Dangling nodes detected")
|
||||
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 |
Reference in New Issue
Block a user