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}