code wiki / (root) / nx_world_state.nx

nx_world_state.nx source

↩ module page · 328 lines · 13697 B

1// nx_world_state.nx -- CANVAS-style carry-forward visual memory + 2// per-scene continuity verifier. 3// 4// Direct port of the world-class architecture from: 5// 6// * CANVAS (arxiv 2604.13452) -- Continuity-Aware Narratives via 7// Visual Agentic Storyboarding. Continuity plan P = (C, L, O) 8// and visual memory M = (Mc, Ml, Mf). 9// * Narrative Graph Prompting (ICCV 2025 AISTORY workshop) -- 10// typed hyper-nodes with unique consistency seeds per entity. 11// * OpenAI Agents DungeonMaster pattern -- engine owns the world 12// state, deterministic rules live in code (here in NishiLang). 13// 14// The substrate's existing nx_arc treated each stage as an 15// independent draw from pools. That's WRONG for the D&D-campaign- 16// in-a-multiverse arc model (see feedback-arcs-as-dnd-campaign- 17// multiverse-continuity): renders are scenes in a chained narrative, 18// not random rolls. This module owns the carry-forward. 19// 20// Scope of v1: characters + locations + monotonic day_clock + 21// universe_id (for multiverse branching). Objects/inventory queued 22// for v2 (CANVAS's O mapping) once we see how it's used in 23// nx_batch_audit_sidecar. 24// 25// genealogy_id: canvas_2026 + narrative_graph_prompting_iccv2025 + 26// openai_agents_dungeonmaster_pattern 27// lineage_id: world_state_v1 28// 29// nx_safety_envelope: 30// intended_use: "World-state container -- terrain + biome + 31// event-trace registers consumed by per-kind 32// generators" 33// sil_target: SIL1 34// asil_target: QM 35// dal_target: NONE 36// evidence: [sealed_state_kind_enum, 37// fixed_capacity_pools, 38// no_FP_in_state_management] 39// hazard_register: [bug-tape-state-overflow-on-cap-exhaustion, 40// bug-tape-event-trace-truncation] 41// residual_risk: "Pool capacities are static; large world 42// partitioning queued via nx_world_shard." 43// verdict: NOT_YET_EVALUATED 44 45import "nx_syscalls.nx" 46import "nx_tier.nx" 47import "nx_directors_note.nx" 48 49// ===== Sealed-enum: ContinuityVerdict ============================= 50// 51// What kind of break the candidate next-scene would cause if 52// committed. CONSISTENT is the only ship-without-flag verdict. 53 54const NX_CONT_CONSISTENT: nx_int = 0 55const NX_CONT_VIOLATES_OUTFIT: nx_int = 1 // outfit changed without story beat 56const NX_CONT_VIOLATES_LOCATION_TELEPORT: nx_int = 2 // location changed without transit 57const NX_CONT_VIOLATES_TIME_REVERSAL: nx_int = 3 // clock went backwards 58const NX_CONT_VIOLATES_CHARACTER_ABSENT: nx_int = 4 // character not registered 59const NX_CONT_VIOLATES_UNIVERSE_MISMATCH: nx_int = 5 // wrong universe_id 60const NX_CONT_N_VERDICTS: nx_int = 6 61 62func nx_cont_verdict_is_valid(v: nx_int) -> nx_int { 63 if v < 0 { return 0 } 64 if v >= NX_CONT_N_VERDICTS { return 0 } 65 return 1 66} 67 68// ===== Character carry-forward fields ============================= 69// 70// Flat-array per character. Matches the substrate's flat-buffer 71// convention (nx_canon_proportions, nx_arc). 72 73const NX_WS_CHAR_F_ID: nx_int = 0 74const NX_WS_CHAR_F_IDENTITY_SEED: nx_int = 1 // per-NGP consistency seed 75const NX_WS_CHAR_F_ARCHETYPE_ID: nx_int = 2 76const NX_WS_CHAR_F_OUTFIT_ID: nx_int = 3 77const NX_WS_CHAR_F_LOCATION_ID: nx_int = 4 78const NX_WS_CHAR_F_AFFECT_AXIS: nx_int = 5 79const NX_WS_CHAR_F_LAST_SEEN_MIN: nx_int = 6 // day_clock at last scene 80const NX_WS_CHAR_FIELDS: nx_int = 7 81 82// ===== Location carry-forward fields ============================== 83 84const NX_WS_LOC_F_ID: nx_int = 0 85const NX_WS_LOC_F_SETTING_SEED: nx_int = 1 86const NX_WS_LOC_F_TIME_FIRST_SEEN: nx_int = 2 87const NX_WS_LOC_FIELDS: nx_int = 3 88 89// ===== Story-beat permission flags ================================ 90// 91// Outfit + location changes require an EXPLICIT story beat per the 92// D&D-continuity cardinal. The pending-beat slot lets the caller 93// authorise the next legitimate change before committing it. 94 95const NX_WS_BEAT_NONE: nx_int = 0 96const NX_WS_BEAT_OUTFIT_CHANGE_OK: nx_int = 1 97const NX_WS_BEAT_LOCATION_CHANGE_OK: nx_int = 2 98const NX_WS_BEAT_BOTH_OK: nx_int = 3 99const NX_WS_BEAT_N_KINDS: nx_int = 4 100 101// ===== WorldState struct =========================================== 102 103struct WorldState { 104 universe_id: nx_int, 105 parent_universe_id: nx_int, // -1 if root universe 106 day_clock_minutes: nx_int, // monotonic 0..1439 (or beyond for multi-day) 107 character_count: nx_int, 108 character_cap: nx_int, 109 characters: *i64, // character_cap * NX_WS_CHAR_FIELDS 110 location_count: nx_int, 111 location_cap: nx_int, 112 locations: *i64, // location_cap * NX_WS_LOC_FIELDS 113 pending_beat: nx_int, // NX_WS_BEAT_* 114 scene_count: nx_int // monotonic scene-number counter 115} 116 117const NX_WS_BYTES: nx_int = 88 // 11 fields * 8 118 119// ===== Builder ==================================================== 120 121func nx_world_alloc(universe_id: nx_int, parent_universe_id: nx_int, 122 character_cap: nx_int, location_cap: nx_int) -> *WorldState { 123 let ws: *WorldState = (sys_mmap(NX_WS_BYTES)) as *WorldState 124 ws.universe_id = universe_id 125 ws.parent_universe_id = parent_universe_id 126 ws.day_clock_minutes = 0 127 ws.character_count = 0 128 ws.character_cap = character_cap 129 let char_bytes: nx_int = character_cap * NX_WS_CHAR_FIELDS * NX_SIZEOF_NX_INT 130 ws.characters = (sys_mmap(char_bytes)) as *i64 131 ws.location_count = 0 132 ws.location_cap = location_cap 133 let loc_bytes: nx_int = location_cap * NX_WS_LOC_FIELDS * NX_SIZEOF_NX_INT 134 ws.locations = (sys_mmap(loc_bytes)) as *i64 135 ws.pending_beat = NX_WS_BEAT_NONE 136 ws.scene_count = 0 137 return ws 138} 139 140// ===== Character registration ====================================== 141// 142// Returns the slot index of the registered character, or -1 if the 143// table is full. Identity-seed and archetype-id are set at register; 144// they don't change across scenes (that's NGP's stable-seed rule). 145 146func nx_world_register_character(ws: *WorldState, id: nx_int, 147 identity_seed: nx_int, 148 archetype_id: nx_int) -> nx_int { 149 if ws.character_count >= ws.character_cap { return 0 - 1 } 150 let slot: nx_int = ws.character_count 151 let base: nx_int = slot * NX_WS_CHAR_FIELDS 152 ws.characters[base + NX_WS_CHAR_F_ID] = id 153 ws.characters[base + NX_WS_CHAR_F_IDENTITY_SEED] = identity_seed 154 ws.characters[base + NX_WS_CHAR_F_ARCHETYPE_ID] = archetype_id 155 ws.characters[base + NX_WS_CHAR_F_OUTFIT_ID] = 0 // unset 156 ws.characters[base + NX_WS_CHAR_F_LOCATION_ID] = 0 // unset 157 ws.characters[base + NX_WS_CHAR_F_AFFECT_AXIS] = NX_AF_NEUTRAL 158 ws.characters[base + NX_WS_CHAR_F_LAST_SEEN_MIN] = 0 159 ws.character_count = ws.character_count + 1 160 return slot 161} 162 163// ===== Location registration ====================================== 164 165func nx_world_register_location(ws: *WorldState, id: nx_int, 166 setting_seed: nx_int) -> nx_int { 167 if ws.location_count >= ws.location_cap { return 0 - 1 } 168 let slot: nx_int = ws.location_count 169 let base: nx_int = slot * NX_WS_LOC_FIELDS 170 ws.locations[base + NX_WS_LOC_F_ID] = id 171 ws.locations[base + NX_WS_LOC_F_SETTING_SEED] = setting_seed 172 ws.locations[base + NX_WS_LOC_F_TIME_FIRST_SEEN] = ws.day_clock_minutes 173 ws.location_count = ws.location_count + 1 174 return slot 175} 176 177// ===== Lookup helpers ============================================= 178 179func nx_world_find_character_slot(ws: *WorldState, char_id: nx_int) -> nx_int { 180 var i: nx_int = 0 181 while i < ws.character_count { 182 let base: nx_int = i * NX_WS_CHAR_FIELDS 183 if ws.characters[base + NX_WS_CHAR_F_ID] == char_id { return i } 184 i = i + 1 185 } 186 return 0 - 1 187} 188 189func nx_world_get_char_field(ws: *WorldState, char_id: nx_int, 190 field: nx_int) -> nx_int { 191 let slot: nx_int = nx_world_find_character_slot(ws, char_id) 192 if slot < 0 { return 0 - 1 } 193 return ws.characters[slot * NX_WS_CHAR_FIELDS + field] 194} 195 196// ===== Story-beat authorisation =================================== 197// 198// Caller signals the kind of legitimate change about to be committed. 199// Once consumed by nx_continuity_check the beat resets to NONE so 200// subsequent stages must re-authorise. 201 202func nx_world_arm_beat(ws: *WorldState, kind: nx_int) -> nx_int { 203 if kind < 0 { return 0 - 1 } 204 if kind >= NX_WS_BEAT_N_KINDS { return 0 - 1 } 205 ws.pending_beat = kind 206 return 0 207} 208 209// ===== Beat-permission helpers (hoisted above continuity_check 210// because nxc2's resolver is single-pass) =========================== 211 212func _ws_beat_permits_outfit(beat: nx_int) -> nx_int { 213 if beat == NX_WS_BEAT_OUTFIT_CHANGE_OK { return 1 } 214 if beat == NX_WS_BEAT_BOTH_OK { return 1 } 215 return 0 216} 217 218func _ws_beat_permits_location(beat: nx_int) -> nx_int { 219 if beat == NX_WS_BEAT_LOCATION_CHANGE_OK { return 1 } 220 if beat == NX_WS_BEAT_BOTH_OK { return 1 } 221 return 0 222} 223 224// ===== Continuity verifier ======================================== 225// 226// Given the world's CURRENT state and a candidate DirectorsNote for 227// the NEXT scene, decide whether committing the note would cause a 228// continuity violation. Per CANVAS: the verifier sits between 229// "candidate generation" and "memory update". 230// 231// Sealed-enum return; CONSISTENT = ship. 232 233func nx_continuity_check(ws: *WorldState, dn: *DirectorsNote) -> nx_int { 234 // Character must be registered 235 let cslot: nx_int = nx_world_find_character_slot(ws, dn.character_id) 236 if cslot < 0 { return NX_CONT_VIOLATES_CHARACTER_ABSENT } 237 let cbase: nx_int = cslot * NX_WS_CHAR_FIELDS 238 239 // Outfit may change only if an OUTFIT or BOTH beat is armed. 240 let cur_outfit: nx_int = ws.characters[cbase + NX_WS_CHAR_F_OUTFIT_ID] 241 if cur_outfit != 0 { 242 if dn.outfit_key_id != cur_outfit { 243 let beat_outfit_ok: nx_int = _ws_beat_permits_outfit(ws.pending_beat) 244 if beat_outfit_ok == 0 { return NX_CONT_VIOLATES_OUTFIT } 245 } 246 } 247 248 // Location may change only if a LOCATION or BOTH beat is armed. 249 let cur_loc: nx_int = ws.characters[cbase + NX_WS_CHAR_F_LOCATION_ID] 250 if cur_loc != 0 { 251 if dn.location_id != cur_loc { 252 let beat_loc_ok: nx_int = _ws_beat_permits_location(ws.pending_beat) 253 if beat_loc_ok == 0 { return NX_CONT_VIOLATES_LOCATION_TELEPORT } 254 } 255 } 256 257 // Day clock must not reverse vs the character's last_seen_min. 258 let last_seen: nx_int = ws.characters[cbase + NX_WS_CHAR_F_LAST_SEEN_MIN] 259 if ws.day_clock_minutes < last_seen { return NX_CONT_VIOLATES_TIME_REVERSAL } 260 261 return NX_CONT_CONSISTENT 262} 263 264// ===== Commit the scene =========================================== 265// 266// AFTER a candidate passes nx_continuity_check, the caller commits 267// it: state advances, pending_beat consumed, scene_count++. This is 268// CANVAS's "update memory before proceeding to the next shot" step. 269 270func nx_world_commit_scene(ws: *WorldState, dn: *DirectorsNote, 271 new_day_clock: nx_int) -> nx_int { 272 if new_day_clock < ws.day_clock_minutes { return NX_CONT_VIOLATES_TIME_REVERSAL } 273 274 let cslot: nx_int = nx_world_find_character_slot(ws, dn.character_id) 275 if cslot < 0 { return NX_CONT_VIOLATES_CHARACTER_ABSENT } 276 let cbase: nx_int = cslot * NX_WS_CHAR_FIELDS 277 278 ws.characters[cbase + NX_WS_CHAR_F_OUTFIT_ID] = dn.outfit_key_id 279 ws.characters[cbase + NX_WS_CHAR_F_LOCATION_ID] = dn.location_id 280 ws.characters[cbase + NX_WS_CHAR_F_AFFECT_AXIS] = dn.affect_axis 281 ws.characters[cbase + NX_WS_CHAR_F_LAST_SEEN_MIN] = new_day_clock 282 283 ws.day_clock_minutes = new_day_clock 284 ws.pending_beat = NX_WS_BEAT_NONE 285 ws.scene_count = ws.scene_count + 1 286 return NX_CONT_CONSISTENT 287} 288 289// ===== Multiverse branching ======================================= 290// 291// Spawn a NEW universe forked from the current one. Copies character 292// + location registry but resets pending_beat + scene_count so the 293// branch is internally independent. The parent universe_id chain 294// lets the caller (or batch sidecar) walk back to the divergence 295// point for explanation traces. 296 297func nx_world_branch(parent: *WorldState, new_universe_id: nx_int) -> *WorldState { 298 let child: *WorldState = nx_world_alloc(new_universe_id, 299 parent.universe_id, 300 parent.character_cap, 301 parent.location_cap) 302 child.day_clock_minutes = parent.day_clock_minutes 303 var i: nx_int = 0 304 while i < parent.character_count { 305 let pbase: nx_int = i * NX_WS_CHAR_FIELDS 306 let cbase: nx_int = i * NX_WS_CHAR_FIELDS 307 var f: nx_int = 0 308 while f < NX_WS_CHAR_FIELDS { 309 child.characters[cbase + f] = parent.characters[pbase + f] 310 f = f + 1 311 } 312 i = i + 1 313 } 314 child.character_count = parent.character_count 315 var j: nx_int = 0 316 while j < parent.location_count { 317 let pbase2: nx_int = j * NX_WS_LOC_FIELDS 318 let cbase2: nx_int = j * NX_WS_LOC_FIELDS 319 var ff: nx_int = 0 320 while ff < NX_WS_LOC_FIELDS { 321 child.locations[cbase2 + ff] = parent.locations[pbase2 + ff] 322 ff = ff + 1 323 } 324 j = j + 1 325 } 326 child.location_count = parent.location_count 327 return child 328}