code wiki / _hdl_build / nx_plotgen.nx

nx_plotgen.nx source

↩ module page · 118 lines · 5342 B

1// nx_plotgen.nx -- the SOVEREIGN PLOT GENERATOR. nx_gamebench hard GAP: nx_story_vm EXECUTES authored story 2// graphs but NOTHING GENERATED them, and the operator named "our own plot generator" explicitly. Feeds the 3// Monster Girl Island / Strive: Conquest north-star outputs. 4// 5// Emits a graph in the EXACT format nx_story_vm already consumes (ch_target / ch_req / ch_set / node_is_end, 6// SV_NCH choices per node) -- so generated plots run on the existing VM with no adapter. Generation and 7// execution stay separate organs; this one only writes DATA. 8// 9// ★THE BUG THIS KILLS -- the UNWINNABLE QUEST. Procedural narrative's defining failure is emitting a story 10// whose ending is gated behind a flag no reachable path can set: the player needs a key that does not exist. 11// Here solvability is STRUCTURAL, not tested-for afterwards: 12// - a MAIN SPINE 0 -> 1 -> ... -> ending is laid down first, and its advance choice is ALWAYS choice 0 13// with NO flag requirement, so the spine is unconditionally walkable; 14// - every spine node RAISES a flag bit as you pass it; 15// - a gated branch at spine node k may only REQUIRE bits raised by spine nodes STRICTLY BEFORE k. 16// Therefore walking the spine always satisfies every gate it meets and always reaches an ending -- the 17// generator cannot emit an unwinnable plot, and the gate proves it by actually WALKING the graph via 18// sv_choose rather than by inspecting it. 19// 20// Also: deterministic (same seed -> same plot, so a story is reproducible from its seed alone) and varied 21// (different seeds -> materially different plots, not one template with names swapped). 22// LIB ONLY -- no main() by ecosystem convention. 23// license_tier: ORIGINAL expect_exit: 0 24import "nx_syscalls.nx" 25import "nx_story_vm.nx" 26 27const PG_MAXFLAGBIT: i64 = 24 // flag bits we allocate to spine progress 28 29func pg_rng(st: *i64) -> i64 { 30 var x: i64 = st[0] 31 x = x ^ (x << 13) 32 x = x ^ (x >> 7) 33 x = x ^ (x << 17) 34 st[0] = x 35 if x < 0 { return 0 - x } 36 return x 37} 38func pg_bit(i: i64) -> i64 { var v: i64=1; var k: i64=0; while k<i { v=v*2; k=k+1 } return v } 39 40// Generate a plot of n nodes. Arrays must be sized ch_*[n*SV_NCH], is_end[n]. 41// Returns the number of gated (flag-requiring) choices emitted. 42func pg_gen(ch_target: *i64, ch_req: *i64, ch_set: *i64, is_end: *i64, 43 n: i64, seed: i64) -> i64 { 44 let st: *i64 = sys_mmap(8) as *i64 45 st[0] = seed 46 if st[0] == 0 { st[0] = 1 } 47 48 // clear: every choice absent, no endings yet 49 var i: i64 = 0 50 while i < n*SV_NCH { ch_target[i] = 0-1; ch_req[i] = 0; ch_set[i] = 0; i = i + 1 } 51 i = 0 52 while i < n { is_end[i] = 0; i = i + 1 } 53 54 // the last node is the canonical ending 55 let last: i64 = n - 1 56 is_end[last] = 1 57 58 var gated: i64 = 0 59 var k: i64 = 0 60 while k < last { 61 // ---- choice 0 = THE SPINE: unconditional advance, raises this node's flag ---- 62 let base: i64 = k*SV_NCH 63 ch_target[base] = k + 1 64 ch_req[base] = 0 // never gated -- this is what guarantees solvability 65 ch_set[base] = pg_bit(k % PG_MAXFLAGBIT) // passing node k raises bit k 66 67 // ---- optional extra choices: shortcuts / detours, possibly GATED on EARLIER spine bits ---- 68 let extra: i64 = pg_rng(st) % SV_NCH // 0..3 additional choices 69 var c: i64 = 1 70 while c <= extra { 71 if c < SV_NCH { 72 // jump forward to a later node (never backwards -> the graph stays acyclic and finite) 73 let span: i64 = last - k 74 var tgt: i64 = k + 1 + (pg_rng(st) % span) 75 if tgt > last { tgt = last } 76 ch_target[base + c] = tgt 77 // gate it on a bit raised STRICTLY EARLIER than k (so the spine always satisfies it) 78 if k >= 2 { 79 let r: i64 = pg_rng(st) % 100 80 if r < 55 { 81 let src: i64 = pg_rng(st) % k // 0..k-1, all raised before reaching k 82 ch_req[base + c] = pg_bit(src % PG_MAXFLAGBIT) 83 gated = gated + 1 84 } 85 } 86 // some choices raise an extra "discovery" bit high in the range 87 if (pg_rng(st) % 100) < 30 { 88 ch_set[base + c] = pg_bit(PG_MAXFLAGBIT + (pg_rng(st) % 6)) 89 } 90 } 91 c = c + 1 92 } 93 k = k + 1 94 } 95 return gated 96} 97 98// Walk ONLY unblocked choices, preferring the spine, and report the node reached. 99// Returns the ending node id, or -1 if the walk got stuck (which must never happen). 100func pg_walk(ch_target: *i64, ch_req: *i64, ch_set: *i64, is_end: *i64, n: i64, maxsteps: i64) -> i64 { 101 let flags: *i64 = sys_mmap(8) as *i64 102 flags[0] = 0 103 var cur: i64 = 0 104 var steps: i64 = 0 105 var go: i64 = 1 106 while go == 1 { 107 if sv_is_end(is_end, cur) == 1 { go = 0 } 108 if go == 1 { 109 if steps >= maxsteps { go = 0; cur = 0-1 } 110 if go == 1 { 111 let nxt: i64 = sv_choose(ch_target, ch_req, ch_set, cur, 0, flags) 112 if nxt < 0 { go = 0; cur = 0-1 } else { cur = nxt } 113 steps = steps + 1 114 } 115 } 116 } 117 return cur 118}