nx_actor_role_game_logic.nx source
↩ module page · 171 lines · 6116 B
1// nx_actor_role_game_logic.nx -- game-logic actor role.
2//
3// Wraps [[nx_tictactoe]] (other-agent / sibling-arc game primitive) as
4// a cooperative actor. Each step plays ONE ply: the configured AI
5// picks a move for the current side, applies it, evaluates outcome,
6// fans out a GAME_ACTION message. Actor COMPLETED when game ends
7// (WIN / DRAW).
8//
9// V1 ships tic-tac-toe; the same adapter shape will wrap nx_checkers
10// + future game primitives in subsequent versions. Lighter transitive
11// import set than the image grader -- composes cleanly with the LLM +
12// audio adapters in a quad-companion smoke per
13// [[feedback-nxc2-module-const-ceiling]] guidance.
14
15import "nx_syscalls.nx"
16import "nx_tier.nx"
17import "nx_prng.nx"
18import "nx_tictactoe.nx"
19import "nx_actor.nx"
20import "nx_message.nx"
21
22// ===== Sealed enum: NxGameLogicVerdict ============================
23
24const NX_GL_V_STEPPED: nx_int = 0
25const NX_GL_V_COMPLETED: nx_int = 1
26const NX_GL_V_FAILED_PICK: nx_int = 2
27const NX_GL_V_FAILED_APPLY: nx_int = 3
28const NX_GL_V_INVALID: nx_int = 4
29const NX_GL_V_NULL: nx_int = 5
30const NX_GL_V_N: nx_int = 6
31
32// ===== Struct: NxGameLogicCtx =====================================
33
34struct NxGameLogicCtx {
35 state: *i64, // tictactoe board state
36 difficulty_x: nx_int, // NX_TTT_AI_EASY/MEDIUM/PERFECT
37 difficulty_o: nx_int,
38 prng_state: *i64,
39 n_plies_emitted: nx_int,
40 last_move_idx: nx_int,
41 last_side: nx_int, // who moved last (X or O)
42 last_outcome: nx_int, // ONGOING/WIN/DRAW after last apply
43 last_verdict: nx_int,
44}
45
46const NX_GL_CTX_BYTES: nx_int = 72 // 9 fields * 8
47
48// ===== Validators =================================================
49
50func nx_gl_v_is_valid(v: nx_int) -> nx_int {
51 if v < 0 { return 0 }
52 if v >= NX_GL_V_N { return 0 }
53 return 1
54}
55
56func nx_gl_difficulty_is_valid(d: nx_int) -> nx_int {
57 if d == NX_TTT_AI_EASY { return 1 }
58 if d == NX_TTT_AI_MEDIUM { return 1 }
59 if d == NX_TTT_AI_PERFECT { return 1 }
60 return 0
61}
62
63// ===== Constructor ================================================
64
65func nx_gl_actor_new(first_mover: nx_int,
66 difficulty_x: nx_int,
67 difficulty_o: nx_int,
68 prng_seed: i64) -> *NxGameLogicCtx {
69 if first_mover != NX_TTT_X {
70 if first_mover != NX_TTT_O { return 0 as *NxGameLogicCtx }
71 }
72 if nx_gl_difficulty_is_valid(difficulty_x) == 0 { return 0 as *NxGameLogicCtx }
73 if nx_gl_difficulty_is_valid(difficulty_o) == 0 { return 0 as *NxGameLogicCtx }
74 let raw: *u8 = sys_mmap(NX_GL_CTX_BYTES)
75 let ctx: *NxGameLogicCtx = raw as *NxGameLogicCtx
76 ctx.state = nx_ttt_new(first_mover)
77 ctx.difficulty_x = difficulty_x
78 ctx.difficulty_o = difficulty_o
79 ctx.prng_state = sys_mmap(16) as *i64
80 nx_prng_init(ctx.prng_state, prng_seed)
81 ctx.n_plies_emitted = 0
82 ctx.last_move_idx = -1
83 ctx.last_side = 0
84 ctx.last_outcome = NX_TTT_ONGOING
85 ctx.last_verdict = NX_GL_V_STEPPED
86 return ctx
87}
88
89// ===== Pick + apply + emit (one ply) ==============================
90
91func nx_gl_actor_step(ctx: *NxGameLogicCtx,
92 sched: *NxActorScheduler,
93 bus: *NxMessageBus,
94 actor_id: nx_int,
95 now_us: nx_size) -> nx_int {
96 if (ctx as i64) == 0 { return NX_GL_V_NULL }
97 // If game already terminal, idempotent COMPLETED.
98 if ctx.last_outcome != NX_TTT_ONGOING {
99 if (sched as i64) != 0 {
100 let a_done: *NxActor = nx_ac_find(sched, actor_id)
101 if (a_done as i64) != 0 {
102 if a_done.state != NX_AC_STATE_COMPLETED {
103 nx_ac_complete(sched, actor_id)
104 }
105 }
106 }
107 return NX_GL_V_COMPLETED
108 }
109 let side: nx_int = nx_ttt_turn(ctx.state)
110 var difficulty: nx_int = ctx.difficulty_x
111 if side == NX_TTT_O { difficulty = ctx.difficulty_o }
112 let mv: nx_int = nx_ttt_pick(ctx.state, side, difficulty, ctx.prng_state)
113 if mv < 0 { return NX_GL_V_FAILED_PICK }
114 if mv >= 9 { return NX_GL_V_FAILED_PICK }
115 if nx_ttt_is_legal(ctx.state, mv) == 0 { return NX_GL_V_FAILED_PICK }
116 let applied: nx_int = nx_ttt_try_apply(ctx.state, mv)
117 if applied == 0 { return NX_GL_V_FAILED_APPLY }
118 nx_ttt_evaluate(ctx.state)
119 ctx.last_move_idx = mv
120 ctx.last_side = side
121 ctx.last_outcome = nx_ttt_outcome(ctx.state)
122 ctx.n_plies_emitted = ctx.n_plies_emitted + 1
123
124 // EMIT fanout
125 if (bus as i64) != 0 {
126 // payload: pack (side<<4 | move_idx) so listener can decode both
127 let payload: nx_size = ((side * 16) + mv) as nx_size
128 nx_ms_send_fanout(bus, actor_id, NX_MS_KIND_GAME_ACTION, payload, 8, now_us)
129 }
130
131 if (sched as i64) != 0 { nx_ac_step(sched, actor_id, 1000, now_us) }
132
133 if ctx.last_outcome != NX_TTT_ONGOING {
134 if (sched as i64) != 0 { nx_ac_complete(sched, actor_id) }
135 return NX_GL_V_COMPLETED
136 }
137 return NX_GL_V_STEPPED
138}
139
140// ===== Accessors ==================================================
141
142func nx_gl_actor_outcome(ctx: *NxGameLogicCtx) -> nx_int {
143 if (ctx as i64) == 0 { return NX_TTT_ONGOING }
144 return ctx.last_outcome
145}
146
147func nx_gl_actor_winner(ctx: *NxGameLogicCtx) -> nx_int {
148 if (ctx as i64) == 0 { return 0 }
149 return nx_ttt_winner(ctx.state)
150}
151
152func nx_gl_actor_plies(ctx: *NxGameLogicCtx) -> nx_int {
153 if (ctx as i64) == 0 { return 0 }
154 return ctx.n_plies_emitted
155}
156
157func nx_gl_actor_last_move(ctx: *NxGameLogicCtx) -> nx_int {
158 if (ctx as i64) == 0 { return -1 }
159 return ctx.last_move_idx
160}
161
162func nx_gl_actor_last_side(ctx: *NxGameLogicCtx) -> nx_int {
163 if (ctx as i64) == 0 { return 0 }
164 return ctx.last_side
165}
166
167func nx_gl_actor_is_done(ctx: *NxGameLogicCtx) -> nx_int {
168 if (ctx as i64) == 0 { return 0 }
169 if ctx.last_outcome != NX_TTT_ONGOING { return 1 }
170 return 0
171}