code wiki / _hdl_build / nx_game_breed.nx
nx_game_breed.nx source
↩ module page · 249 lines · 13231 B
1// nx_game_breed.nx -- the BREEDERS mechanic (ws=game-interact rung 3; operator-named first target;
2// the Palworld capture loop reshaped: weaken -> WIN -> DOMINATE -> the defeated one SUBMITS and joins
3// you -> BREED -> unique offspring). Composes the certified parts and adds ONLY the missing verbs:
4// nx_creature_battle = the fight (deterministic, typed, transcripted)
5// nx_game_companion = the ownership substrate (unique genes, investment, persistence)
6// nx_rpgstats = stat derivation (saturating, order-independent)
7// DESIGN LAWS HONORED:
8// - DOMINANCE IS EARNED, NOT ROLLED: the margin is computed from the battle's real outcome (winner's
9// remaining HP + level advantage). A scraped-by win does not dominate; a crushing one does.
10// - SUBMISSION IS THE LOSER'S CHOICE, DATA-DRIVEN: each species carries a submit_bar (permil) -- a
11// proud species must be beaten decisively. Rows are DATA; tuning never touches logic.
12// - THE ONE YOU DOMINATED IS THE ONE YOU OWN: capture copies the EXACT gene vector (species+4 IVs),
13// never re-rolls -- the endowment law from nx_game_companion carried through.
14// - INHERITANCE IS REAL: every offspring gene comes from a parent's same slot +/- a bounded mutation
15// (BRD_MUT), provable in-gate. No inheritance = no breeding game, just a slot machine.
16// - SCENE BEATS ARE DATA: the engine emits (tick,beat,who,with) rows -- WIN/DOMINATE/SUBMIT/MATE/
17// BIRTH/DEFEAT. The presentation layer (VN/renderer) turns beats into scenes; adult content lives
18// in content packs, never in mechanics. (Beat ring idiom shared with nx_game_agent events; extract
19// to a common lib on the third consumer per migrate-on-touch.)
20// All integer, deterministic. LIB, no main. license_tier: ORIGINAL
21import "nx_syscalls.nx"
22import "nx_game_companion.nx"
23import "nx_creature_battle.nx"
24// WIRING, NOT NEW CODE (2026-07-31). gx_breed_modern and GX_REC are declared in nx_game_genetics.nx and
25// were unreachable from here. This file is the chokepoint for nx_game_breed_gate, nx_breeder_arena and
26// nx_breeder_arena_gate, so the one line closes all four -- migrate the CHOKEPOINT LIB, not the leaf.
27import "nx_game_genetics.nx"
28
29// ---- species battle+temperament rows: DATA, stride 8 ----
30// [btype, hp_base, atk_base, def_base, spd_base, submit_bar_permil, pow_primary, pow_normal]
31const BRD_SP_STRIDE: i64 = 8
32func brd_species_default(tbl: *i64) -> i64 {
33 // 0 Sylph (Grass) gentle: submits at 350
34 tbl[0]=3; tbl[1]=34; tbl[2]=10; tbl[3]=8; tbl[4]=12; tbl[5]=350; tbl[6]=40; tbl[7]=30
35 // 1 Flamia (Fire) fierce: 450
36 tbl[8]=1; tbl[9]=30; tbl[10]=13; tbl[11]=7; tbl[12]=11; tbl[13]=450; tbl[14]=45; tbl[15]=30
37 // 2 Undine (Water) calm: 400
38 tbl[16]=2; tbl[17]=36; tbl[18]=9; tbl[19]=10; tbl[20]=9; tbl[21]=400; tbl[22]=40; tbl[23]=30
39 // 3 Voltra (Electric) proud: 500
40 tbl[24]=4; tbl[25]=28; tbl[26]=12; tbl[27]=7; tbl[28]=14; tbl[29]=500; tbl[30]=45; tbl[31]=25
41 // 4 Terra (Rock) stoic: 300
42 tbl[32]=5; tbl[33]=40; tbl[34]=11; tbl[35]=12; tbl[36]=6; tbl[37]=300; tbl[38]=40; tbl[39]=35
43 // 5 Luna (Normal) even: 400
44 tbl[40]=0; tbl[41]=32; tbl[42]=10; tbl[43]=9; tbl[44]=10; tbl[45]=400; tbl[46]=40; tbl[47]=40
45 return 6
46}
47func brd_sp_type(tbl: *i64, s: i64) -> i64 { return tbl[s*BRD_SP_STRIDE + 0] }
48func brd_sp_submitbar(tbl: *i64, s: i64) -> i64 { return tbl[s*BRD_SP_STRIDE + 5] }
49
50// ---- scene-beat ring: [0]=count, rows stride 4 = [tick, beat, actor_vec, partner_vec] ----
51const BRD_BEAT_STRIDE: i64 = 4
52const BEAT_WIN: i64 = 1
53const BEAT_DEFEAT: i64 = 2 // the player lost (losable, by design)
54const BEAT_DOMINATE: i64 = 3 // margin cleared the bar
55const BEAT_SUBMIT: i64 = 4 // the beaten one yields and joins
56const BEAT_RESIST: i64 = 5 // won but NOT dominantly; she breaks away
57const BEAT_MATE: i64 = 6
58const BEAT_BIRTH: i64 = 7
59func brd_beats(bt: *i64) -> i64 { return bt[0] }
60func brd_beat_push(bt: *i64, cap: i64, tick: i64, kind: i64, a: i64, b: i64) -> i64 {
61 let n: i64 = bt[0]
62 if n >= cap { return 0-1 }
63 bt[1 + n*BRD_BEAT_STRIDE + 0] = tick
64 bt[1 + n*BRD_BEAT_STRIDE + 1] = kind
65 bt[1 + n*BRD_BEAT_STRIDE + 2] = a
66 bt[1 + n*BRD_BEAT_STRIDE + 3] = b
67 bt[0] = n + 1
68 return n
69}
70func brd_beat_kind(bt: *i64, i: i64) -> i64 { return bt[1 + i*BRD_BEAT_STRIDE + 1] }
71
72// ---- bridge: a roster companion -> a battle creature (i64[24], cb_ layout) ----
73// stats derive from genes via nx_rpgstats: hp from IV0 (constitution role), atk IV1, def IV2, spd IV3.
74func brd_to_creature(ros: *i64, h: i64, tbl: *i64, c: *i64, name: *u8) -> i64 {
75 let sp: i64 = en_get(ros, h, C_SPEC)
76 let lvl: i64 = en_get(ros, h, C_LVL)
77 let r: i64 = sp * BRD_SP_STRIDE
78 let hp: i64 = rs_derived_hp(en_get(ros, h, C_IV0), lvl, tbl[r+1], 1, 2)
79 let atk: i64 = rs_sadd(rs_sadd(tbl[r+2], en_get(ros, h, C_IV1)), lvl)
80 let def: i64 = rs_sadd(rs_sadd(tbl[r+3], en_get(ros, h, C_IV2)), lvl)
81 let spd: i64 = rs_sadd(rs_sadd(tbl[r+4], en_get(ros, h, C_IV3)), lvl)
82 cb_set(c, tbl[r+0], lvl, hp, atk, def, spd, name)
83 cb_addmove(c, tbl[r+0], tbl[r+6], 90)
84 cb_addmove(c, 0, tbl[r+7], 100)
85 return 0
86}
87
88// ---- dominance margin from the battle's REAL outcome (never rolled) ----
89// margin = winner remaining-HP permil + 50 per level of advantage (advantage clamped 0..5)
90func brd_margin(win_hp: i64, win_maxhp: i64, win_lvl: i64, lose_lvl: i64) -> i64 {
91 var hpp: i64 = 0
92 if win_maxhp > 0 { hpp = win_hp * 1000 / win_maxhp }
93 let adv: i64 = rs_clamp(win_lvl - lose_lvl, 0, 5)
94 return hpp + adv * 50
95}
96
97// ---- the duel: A (player's) vs B (the wild). Returns winner 0=A 1=B.
98// out[0]=margin out[1]=submitted(0/1) out[2]=winner_hp out[3]=turns(from transcript len proxy: 0)
99func brd_duel(rosA: *i64, hA: i64, rosB: *i64, hB: i64, tbl: *i64, seed: i64,
100 buf: *u8, bt: *i64, btcap: i64, tick: i64, out: *i64) -> i64 {
101 let cA: *i64 = sys_mmap(24*8) as *i64
102 let cB: *i64 = sys_mmap(24*8) as *i64
103 brd_to_creature(rosA, hA, tbl, cA, "Yours" as *u8)
104 brd_to_creature(rosB, hB, tbl, cB, "Wild" as *u8)
105 let win: i64 = cb_battle(cA, cB, seed, buf)
106 var wc: *i64 = cA
107 var lc: *i64 = cB
108 var lros: *i64 = rosB
109 var lh: i64 = hB
110 if win == 1 { wc = cB; lc = cA; lros = rosA; lh = hA }
111 let m: i64 = brd_margin(wc[3], wc[2], wc[1], lc[1])
112 out[0] = m
113 out[2] = wc[3]
114 var submitted: i64 = 0
115 if win == 0 {
116 brd_beat_push(bt, btcap, tick, BEAT_WIN, comp_vec(rosA, hA), comp_vec(rosB, hB))
117 let bar: i64 = brd_sp_submitbar(tbl, en_get(lros, lh, C_SPEC))
118 if m >= bar {
119 brd_beat_push(bt, btcap, tick, BEAT_DOMINATE, comp_vec(rosA, hA), comp_vec(rosB, hB))
120 brd_beat_push(bt, btcap, tick, BEAT_SUBMIT, comp_vec(rosB, hB), comp_vec(rosA, hA))
121 submitted = 1
122 }
123 if m < bar { brd_beat_push(bt, btcap, tick, BEAT_RESIST, comp_vec(rosB, hB), comp_vec(rosA, hA)) }
124 }
125 if win == 1 { brd_beat_push(bt, btcap, tick, BEAT_DEFEAT, comp_vec(rosA, hA), comp_vec(rosB, hB)) }
126 out[1] = submitted
127 return win
128}
129
130// ---- capture: the submitted one JOINS -- exact genes, never re-rolled ----
131// returns the new handle in dst (or -1 full). Removes her from the wild roster.
132func brd_capture(dst: *i64, src: *i64, h: i64) -> i64 {
133 let nh: i64 = en_spawn(dst)
134 if nh == EN_NULL { return 0-1 }
135 en_set(dst, nh, C_SPEC, en_get(src, h, C_SPEC))
136 en_set(dst, nh, C_IV0, en_get(src, h, C_IV0))
137 en_set(dst, nh, C_IV1, en_get(src, h, C_IV1))
138 en_set(dst, nh, C_IV2, en_get(src, h, C_IV2))
139 en_set(dst, nh, C_IV3, en_get(src, h, C_IV3))
140 en_set(dst, nh, C_XP, en_get(src, h, C_XP))
141 en_set(dst, nh, C_LVL, en_get(src, h, C_LVL))
142 en_set(dst, nh, C_NICK, en_count(dst) - 1)
143 // her GENOME and her BOND state come with her -- capture transfers a creature, it does not mint a
144 // new one. (The taming path that captured her is what sets trust afterward; capture itself is honest.)
145 en_set(dst, nh, C_GENO, en_get(src, h, C_GENO))
146 en_set(dst, nh, C_SEXG, en_get(src, h, C_SEXG))
147 en_set(dst, nh, C_POLY, en_get(src, h, C_POLY))
148 en_set(dst, nh, C_EPI, en_get(src, h, C_EPI))
149 en_set(dst, nh, C_TRUST, en_get(src, h, C_TRUST))
150 en_set(dst, nh, C_WILL, en_get(src, h, C_WILL))
151 en_set(dst, nh, C_DISP, en_get(src, h, C_DISP))
152 en_destroy(src, h)
153 return nh
154}
155
156// ---- breeding: two parents -> one offspring, REAL inheritance ----
157const BRD_MUT: i64 = 2 // max mutation distance per gene (the provable bound)
158func brd_rng(s: *i64) -> i64 {
159 var x: i64 = s[0]
160 x = x ^ (x << 13); x = x ^ (x >> 7); x = x ^ (x << 17)
161 s[0] = x
162 if x < 0 { return 0 - x }
163 return x
164}
165// child gene: pick a parent's same-slot gene, mutate within +/-BRD_MUT, clamp to the IV range
166func brd_gene(ga: i64, gb: i64, s: *i64) -> i64 {
167 var g: i64 = ga
168 if (brd_rng(s) & 1) == 1 { g = gb }
169 let mu: i64 = (brd_rng(s) % (BRD_MUT*2 + 1)) - BRD_MUT
170 return rs_clamp(g + mu, 0, 31)
171}
172// species: same -> same; cross -> the DOMINANT parent's line carries (domA=1 means A dominated)
173func brd_cross_species(spA: i64, spB: i64, domA: i64) -> i64 {
174 if spA == spB { return spA }
175 if domA == 1 { return spA }
176 return spB
177}
178// breed A x B into the roster. Emits MATE + BIRTH beats. Returns the child's handle or -1.
179// ★TWO INHERITANCE LAYERS, both real biology (2026-07-27):
180// QUANTITATIVE (the IV stat vector) -- blended-with-mutation, the polygenic model: many small effects
181// summing to a continuous trait. This is why stats drift toward the parental mean.
182// MENDELIAN (the genome word) -- discrete alleles segregating per gx_breed_modern: independent
183// assortment, linkage+recombination on the HORN/TAIL pair, ZW sex determination, epigenetic marks
184// inherited-but-fading, rare permanent mutation. This is why VISIBLE traits jump in 3:1 ratios.
185// A real organism has both, so the game teaches both. info out: [0]=recombinations [1]=mutated
186// [2]=mutated_trait [3]=child sex.
187func brd_breed_info(ros: *i64, hA: i64, hB: i64, domA: i64, seed: i64,
188 bt: *i64, btcap: i64, tick: i64, info: *i64) -> i64 {
189 if en_valid(ros, hA) == 0 { return 0-1 }
190 if en_valid(ros, hB) == 0 { return 0-1 }
191 let s: *i64 = sys_mmap(16) as *i64
192 s[0] = seed * 2654435761 + comp_vec(ros, hA) * 40503 + comp_vec(ros, hB) * 69061 + 1
193 let ch: i64 = en_spawn(ros)
194 if ch == EN_NULL { return 0-1 }
195 brd_beat_push(bt, btcap, tick, BEAT_MATE, comp_vec(ros, hA), comp_vec(ros, hB))
196 en_set(ros, ch, C_SPEC, brd_cross_species(en_get(ros, hA, C_SPEC), en_get(ros, hB, C_SPEC), domA))
197 en_set(ros, ch, C_IV0, brd_gene(en_get(ros, hA, C_IV0), en_get(ros, hB, C_IV0), s))
198 en_set(ros, ch, C_IV1, brd_gene(en_get(ros, hA, C_IV1), en_get(ros, hB, C_IV1), s))
199 en_set(ros, ch, C_IV2, brd_gene(en_get(ros, hA, C_IV2), en_get(ros, hB, C_IV2), s))
200 en_set(ros, ch, C_IV3, brd_gene(en_get(ros, hA, C_IV3), en_get(ros, hB, C_IV3), s))
201 en_set(ros, ch, C_XP, 0)
202 en_set(ros, ch, C_LVL, 1)
203 en_set(ros, ch, C_NICK, en_count(ros) - 1)
204 // ---- the Mendelian half: pack both parents into gx records and run the modern cross ----
205 let pA: *i64 = sys_mmap(GX_REC * 8) as *i64
206 let pB: *i64 = sys_mmap(GX_REC * 8) as *i64
207 let kid: *i64 = sys_mmap(GX_REC * 8) as *i64
208 pA[0] = en_get(ros, hA, C_GENO); pA[1] = en_get(ros, hA, C_SEXG)
209 pA[2] = en_get(ros, hA, C_POLY); pA[3] = en_get(ros, hA, C_EPI)
210 pB[0] = en_get(ros, hB, C_GENO); pB[1] = en_get(ros, hB, C_SEXG)
211 pB[2] = en_get(ros, hB, C_POLY); pB[3] = en_get(ros, hB, C_EPI)
212 gx_breed_modern(pA, pB, s, kid, info)
213 en_set(ros, ch, C_GENO, kid[0])
214 en_set(ros, ch, C_SEXG, kid[1])
215 en_set(ros, ch, C_POLY, kid[2])
216 en_set(ros, ch, C_EPI, kid[3])
217 // a child born to companions you already hold starts BONDED, not wild -- she was raised by you.
218 en_set(ros, ch, C_TRUST, 600)
219 en_set(ros, ch, C_WILL, 300)
220 en_set(ros, ch, C_DISP, 700)
221 brd_beat_push(bt, btcap, tick, BEAT_BIRTH, comp_vec(ros, ch), 0)
222 return ch
223}
224// thin wrapper: the original 8-arg signature every existing caller uses stays valid (add a verb, never
225// widen -- NishiLang has no call-arity check on old call sites that were compiled against the old shape).
226func brd_breed(ros: *i64, hA: i64, hB: i64, domA: i64, seed: i64,
227 bt: *i64, btcap: i64, tick: i64) -> i64 {
228 let info: *i64 = sys_mmap(8 * 8) as *i64
229 return brd_breed_info(ros, hA, hB, domA, seed, bt, btcap, tick, info)
230}
231
232// inheritance audit: 1 if EVERY gene of child is within BRD_MUT of the same slot of SOME parent
233func brd_inherit_ok(ros: *i64, ch: i64, hA: i64, hB: i64) -> i64 {
234 var k: i64 = 0
235 while k < 4 {
236 let comp: i64 = C_IV0 + k
237 let cg: i64 = en_get(ros, ch, comp)
238 var da: i64 = cg - en_get(ros, hA, comp)
239 if da < 0 { da = 0 - da }
240 var db: i64 = cg - en_get(ros, hB, comp)
241 if db < 0 { db = 0 - db }
242 var near: i64 = 0
243 if da <= BRD_MUT { near = 1 }
244 if db <= BRD_MUT { near = 1 }
245 if near == 0 { return 0 }
246 k = k + 1
247 }
248 return 1
249}