code wiki / (root) / nx_campaign.nx

nx_campaign.nx source

↩ module page · 212 lines · 8259 B

1// nx_campaign.nx -- WRITING arc, rung W-CAMP-1: the D&D CAMPAIGN structure model. 2// Operator intent 2026-08-04: generate dnd campaigns -- the campaign/GM mode the 3// writing-hub census lists as a GAP. A campaign script is a HEADER (setting/party/ 4// quest) + SESSION -> ENCOUNTER blocks, each encounter a small key:value record 5// (kind/time/location/npcs/goal/transit). This organ counts sessions+encounters, 6// validates the record set, emits the per-encounter GM BRIEF the generation seat 7// expands + the image-prompt seed (same image-gen ingestion as manga/scene), and 8// makes the CAMPAIGN-CONTINUITY law MECHANICAL per 9// feedback-arcs-as-dnd-campaign-multiverse-continuity: 10// - time moves monotonically forward (cg_time_check counts regressions); 11// - a location change without a transit beat is a teleport (cg_transit_check). 12// The campaign TEXT is operator/lane DATA (the seam: engine structures, seat 13// authors). DRY: markers via nx_manga mg_line_is/mg_count; fields via the 14// nx_scenario whole-key parser on encounter byte sub-ranges. Pure integer, 15// NO syscalls, caller owns buffers (the nx_note shape). 16// license_tier: ORIGINAL 17// module: nishi-core.write.campaign 18// depends: nishi-core.write.manga nishi-core.write.scenario 19// capability: WRITE_CAMPAIGN 20import "nx_manga.nx" 21import "nx_scenario.nx" 22 23// null-terminated string equality (1 equal / 0 not) 24func cg_streq(a: *u8, b: *u8) -> i64 { 25 var i: i64 = 0 26 while a[i] != (0 as u8) { 27 if a[i] != b[i] { return 0 } 28 i = i + 1 29 } 30 if b[i] != (0 as u8) { return 0 } 31 return 1 32} 33 34// offset of the first SESSION marker line (the campaign header ends there), or n 35func cg_header_end(buf: *u8, n: i64) -> i64 { 36 var i: i64 = 0 37 while i < n { 38 if mg_line_is(buf, n, i, "SESSION\x00" as *u8) == 1 { return i } 39 i = sc_next_line(buf, n, i) 40 } 41 return n 42} 43 44func cg_count_sessions(buf: *u8, n: i64) -> i64 { return mg_count(buf, n, "SESSION\x00" as *u8) } 45func cg_count_encounters(buf: *u8, n: i64) -> i64 { return mg_count(buf, n, "ENCOUNTER\x00" as *u8) } 46 47// byte offset of the idx-th ENCOUNTER marker line (0-based), or -1 48func cg_enc_start(buf: *u8, n: i64, idx: i64) -> i64 { 49 var i: i64 = 0 50 var seen: i64 = 0 51 var res: i64 = 0 - 1 52 var go: i64 = 1 53 while go == 1 { 54 if i >= n { go = 0 } 55 else { 56 if mg_line_is(buf, n, i, "ENCOUNTER\x00" as *u8) == 1 { 57 if seen == idx { res = i; go = 0 } 58 else { seen = seen + 1; i = sc_next_line(buf, n, i) } 59 } else { i = sc_next_line(buf, n, i) } 60 } 61 } 62 return res 63} 64 65// end of the encounter starting at start = next ENCOUNTER or SESSION marker, or n 66func cg_enc_end(buf: *u8, n: i64, start: i64) -> i64 { 67 var i: i64 = sc_next_line(buf, n, start) 68 var e: i64 = n 69 var go: i64 = 1 70 while go == 1 { 71 if i >= n { e = n; go = 0 } 72 else { 73 if mg_line_is(buf, n, i, "ENCOUNTER\x00" as *u8) == 1 { e = i; go = 0 } 74 else { 75 if mg_line_is(buf, n, i, "SESSION\x00" as *u8) == 1 { e = i; go = 0 } 76 else { i = sc_next_line(buf, n, i) } 77 } 78 } 79 } 80 return e 81} 82 83// completeness: count of REQUIRED keys missing. Header requires setting/party/ 84// quest (checked ONLY in the header range so an encounter field can never 85// satisfy a header key); every encounter requires kind + goal (a GM cannot run 86// an encounter without them). 0 = runnable campaign. 87func cg_validate(buf: *u8, n: i64) -> i64 { 88 var miss: i64 = 0 89 let he: i64 = cg_header_end(buf, n) 90 if sc_has(buf, he, "setting\x00" as *u8) == 0 { miss = miss + 1 } 91 if sc_has(buf, he, "party\x00" as *u8) == 0 { miss = miss + 1 } 92 if sc_has(buf, he, "quest\x00" as *u8) == 0 { miss = miss + 1 } 93 let ne: i64 = cg_count_encounters(buf, n) 94 var idx: i64 = 0 95 while idx < ne { 96 let s: i64 = cg_enc_start(buf, n, idx) 97 let e: i64 = cg_enc_end(buf, n, s) 98 let sub: *u8 = (buf as i64 + s) as *u8 99 if sc_has(sub, e - s, "kind\x00" as *u8) == 0 { miss = miss + 1 } 100 if sc_has(sub, e - s, "goal\x00" as *u8) == 0 { miss = miss + 1 } 101 idx = idx + 1 102 } 103 return miss 104} 105 106// parse the non-negative integer value of key, or -1 if missing/non-numeric 107func cg_field_int(buf: *u8, n: i64, key: *u8) -> i64 { 108 let off: i64 = sc_find_field(buf, n, key) 109 if off < 0 { return 0 - 1 } 110 var v: i64 = 0 111 var i: i64 = off 112 var any: i64 = 0 113 var go: i64 = 1 114 while go == 1 { 115 if i >= n { go = 0 } 116 else { 117 let c: i64 = buf[i] as i64 118 if c >= 48 { 119 if c <= 57 { v = v * 10 + (c - 48); any = 1; i = i + 1 } else { go = 0 } 120 } else { go = 0 } 121 } 122 } 123 if any == 0 { return 0 - 1 } 124 return v 125} 126 127// CONTINUITY tooth 1: time is monotonically non-decreasing across the campaign. 128// Encounters without a numeric time are skipped (no false positives). Returns 129// the count of regressions (0 = timeline sound). 130func cg_time_check(buf: *u8, n: i64) -> i64 { 131 var viol: i64 = 0 132 var prev: i64 = 0 - 1 133 let ne: i64 = cg_count_encounters(buf, n) 134 var idx: i64 = 0 135 while idx < ne { 136 let s: i64 = cg_enc_start(buf, n, idx) 137 let e: i64 = cg_enc_end(buf, n, s) 138 let sub: *u8 = (buf as i64 + s) as *u8 139 let t: i64 = cg_field_int(sub, e - s, "time\x00" as *u8) 140 if t >= 0 { 141 if prev >= 0 { if t < prev { viol = viol + 1 } } 142 prev = t 143 } 144 idx = idx + 1 145 } 146 return viol 147} 148 149// CONTINUITY tooth 2: a location change between consecutive encounters without a 150// transit beat on the later one is a TELEPORT. Caller owns two scratch buffers 151// (sa/sb, each cap bytes). Returns the count of teleports (0 = travel sound). 152func cg_transit_check(buf: *u8, n: i64, sa: *u8, sb: *u8, cap: i64) -> i64 { 153 var viol: i64 = 0 154 let ne: i64 = cg_count_encounters(buf, n) 155 var idx: i64 = 1 156 while idx < ne { 157 let sp: i64 = cg_enc_start(buf, n, idx - 1) 158 let ep: i64 = cg_enc_end(buf, n, sp) 159 let subp: *u8 = (buf as i64 + sp) as *u8 160 let s: i64 = cg_enc_start(buf, n, idx) 161 let e: i64 = cg_enc_end(buf, n, s) 162 let sub: *u8 = (buf as i64 + s) as *u8 163 let ra: i64 = sc_copy_field(subp, ep - sp, "location\x00" as *u8, sa, cap) 164 let rb: i64 = sc_copy_field(sub, e - s, "location\x00" as *u8, sb, cap) 165 if ra > 0 { 166 if rb > 0 { 167 if cg_streq(sa, sb) == 0 { 168 if sc_has(sub, e - s, "transit\x00" as *u8) == 0 { viol = viol + 1 } 169 } 170 } 171 } 172 idx = idx + 1 173 } 174 return viol 175} 176 177// emit the per-encounter GM BRIEF the generation seat expands: 178// GM: <kind> @ <location> | npcs: <npcs> | goal: <goal> 179func cg_emit_brief(buf: *u8, n: i64, idx: i64, out: *u8, cap: i64) -> i64 { 180 let s: i64 = cg_enc_start(buf, n, idx) 181 if s < 0 { out[0] = 0 as u8; return 0 } 182 let e: i64 = cg_enc_end(buf, n, s) 183 let sub: *u8 = (buf as i64 + s) as *u8 184 let sn2: i64 = e - s 185 var p: i64 = 0 186 p = sc_app_lit(out, p, cap, "GM: \x00" as *u8) 187 p = sc_app_field(sub, sn2, "kind\x00" as *u8, out, p, cap) 188 p = sc_app_lit(out, p, cap, " @ \x00" as *u8) 189 p = sc_app_field(sub, sn2, "location\x00" as *u8, out, p, cap) 190 p = sc_app_lit(out, p, cap, " | npcs: \x00" as *u8) 191 p = sc_app_field(sub, sn2, "npcs\x00" as *u8, out, p, cap) 192 p = sc_app_lit(out, p, cap, " | goal: \x00" as *u8) 193 p = sc_app_field(sub, sn2, "goal\x00" as *u8, out, p, cap) 194 out[p] = 0 as u8 195 return p 196} 197 198// emit the per-encounter image-prompt seed (feeds the same image-gen path): 199// <location>, <npcs> 200func cg_emit_img(buf: *u8, n: i64, idx: i64, out: *u8, cap: i64) -> i64 { 201 let s: i64 = cg_enc_start(buf, n, idx) 202 if s < 0 { out[0] = 0 as u8; return 0 } 203 let e: i64 = cg_enc_end(buf, n, s) 204 let sub: *u8 = (buf as i64 + s) as *u8 205 let sn2: i64 = e - s 206 var p: i64 = 0 207 p = sc_app_field(sub, sn2, "location\x00" as *u8, out, p, cap) 208 p = sc_app_lit(out, p, cap, ", \x00" as *u8) 209 p = sc_app_field(sub, sn2, "npcs\x00" as *u8, out, p, cap) 210 out[p] = 0 as u8 211 return p 212}