1use crate::config::{CurriculumConfig, ObservationVisibility};
2use crate::db::CardDb;
3use crate::legal::{ActionDesc, Decision, DecisionKind};
4use crate::state::{
5 AttackType, GameState, ModifierKind, Phase, StageStatus, TerminalResult, REVEAL_HISTORY_LEN,
6};
7
8pub const OBS_ENCODING_VERSION: u32 = 1;
9pub const ACTION_ENCODING_VERSION: u32 = 1;
10pub const POLICY_VERSION: u32 = 1;
11pub const SPEC_HASH: u64 = ((OBS_ENCODING_VERSION as u64) << 32)
12 | ((ACTION_ENCODING_VERSION as u64) << 16)
13 | (POLICY_VERSION as u64);
14
15pub const MAX_HAND: usize = 50;
16pub const MAX_DECK: usize = 50;
17pub const MAX_STAGE: usize = 5;
18pub const MAX_ABILITIES_PER_CARD: usize = 4;
19pub const ATTACK_SLOT_COUNT: usize = 3;
20pub const MAX_LEVEL: usize = 4;
21pub const TOP_CLOCK: usize = 7;
22pub const TOP_WAITING_ROOM: usize = 5;
23pub const TOP_STOCK: usize = 5;
24pub const TOP_RESOLUTION: usize = 5;
25
26pub const MULLIGAN_CONFIRM_ID: usize = 0;
27pub const MULLIGAN_SELECT_BASE: usize = MULLIGAN_CONFIRM_ID + 1;
28pub const MULLIGAN_SELECT_COUNT: usize = MAX_HAND;
29
30pub const PASS_ACTION_ID: usize = MULLIGAN_SELECT_BASE + MULLIGAN_SELECT_COUNT;
31pub const CLOCK_HAND_BASE: usize = PASS_ACTION_ID + 1;
32pub const CLOCK_HAND_COUNT: usize = MAX_HAND;
33
34pub const MAIN_PLAY_CHAR_BASE: usize = CLOCK_HAND_BASE + CLOCK_HAND_COUNT;
35pub const MAIN_PLAY_CHAR_COUNT: usize = MAX_HAND * MAX_STAGE;
36pub const MAIN_PLAY_EVENT_BASE: usize = MAIN_PLAY_CHAR_BASE + MAIN_PLAY_CHAR_COUNT;
37pub const MAIN_PLAY_EVENT_COUNT: usize = MAX_HAND;
38pub const MAIN_MOVE_BASE: usize = MAIN_PLAY_EVENT_BASE + MAIN_PLAY_EVENT_COUNT;
39pub const MAIN_MOVE_COUNT: usize = MAX_STAGE * (MAX_STAGE - 1);
40
41pub const CLIMAX_PLAY_BASE: usize = MAIN_MOVE_BASE + MAIN_MOVE_COUNT;
42pub const CLIMAX_PLAY_COUNT: usize = MAX_HAND;
43
44pub const ATTACK_BASE: usize = CLIMAX_PLAY_BASE + CLIMAX_PLAY_COUNT;
45pub const ATTACK_COUNT: usize = ATTACK_SLOT_COUNT * 3;
46
47pub const LEVEL_UP_BASE: usize = ATTACK_BASE + ATTACK_COUNT;
48pub const LEVEL_UP_COUNT: usize = 7;
49
50pub const ENCORE_PAY_BASE: usize = LEVEL_UP_BASE + LEVEL_UP_COUNT;
51pub const ENCORE_PAY_COUNT: usize = MAX_STAGE;
52pub const ENCORE_DECLINE_BASE: usize = ENCORE_PAY_BASE + ENCORE_PAY_COUNT;
53pub const ENCORE_DECLINE_COUNT: usize = MAX_STAGE;
54
55pub const TRIGGER_ORDER_BASE: usize = ENCORE_DECLINE_BASE + ENCORE_DECLINE_COUNT;
56pub const TRIGGER_ORDER_COUNT: usize = 10;
57
58pub const CHOICE_BASE: usize = TRIGGER_ORDER_BASE + TRIGGER_ORDER_COUNT;
59pub const CHOICE_COUNT: usize = 16;
60pub const CHOICE_PREV_ID: usize = CHOICE_BASE + CHOICE_COUNT;
61pub const CHOICE_NEXT_ID: usize = CHOICE_PREV_ID + 1;
62
63pub const CONCEDE_ID: usize = CHOICE_NEXT_ID + 1;
64pub const ACTION_SPACE_SIZE: usize = CONCEDE_ID + 1;
65
66pub const OBS_HEADER_LEN: usize = 16;
67pub const OBS_REASON_LEN: usize = 8;
68pub const OBS_REASON_IN_MAIN: usize = 0;
69pub const OBS_REASON_IN_CLIMAX: usize = 1;
70pub const OBS_REASON_IN_ATTACK: usize = 2;
71pub const OBS_REASON_IN_COUNTER_WINDOW: usize = 3;
72pub const OBS_REASON_NO_STOCK: usize = 4;
73pub const OBS_REASON_NO_COLOR: usize = 5;
74pub const OBS_REASON_NO_HAND: usize = 6;
75pub const OBS_REASON_NO_TARGETS: usize = 7;
76pub const OBS_REVEAL_LEN: usize = REVEAL_HISTORY_LEN;
77pub const OBS_CONTEXT_LEN: usize = 4;
78pub const OBS_CONTEXT_PRIORITY_WINDOW: usize = 0;
79pub const OBS_CONTEXT_CHOICE_ACTIVE: usize = 1;
80pub const OBS_CONTEXT_STACK_NONEMPTY: usize = 2;
81pub const OBS_CONTEXT_ENCORE_PENDING: usize = 3;
82pub const PER_PLAYER_COUNTS: usize = 9;
83pub const PER_STAGE_SLOT: usize = 5;
84pub const PER_PLAYER_STAGE: usize = MAX_STAGE * PER_STAGE_SLOT;
85pub const PER_PLAYER_CLIMAX_TOP: usize = 1;
86pub const PER_PLAYER_LEVEL: usize = MAX_LEVEL;
87pub const PER_PLAYER_CLOCK_TOP: usize = TOP_CLOCK;
88pub const PER_PLAYER_WAITING_TOP: usize = TOP_WAITING_ROOM;
89pub const PER_PLAYER_RESOLUTION_TOP: usize = TOP_RESOLUTION;
90pub const PER_PLAYER_STOCK_TOP: usize = TOP_STOCK;
91pub const PER_PLAYER_HAND: usize = MAX_HAND;
92pub const PER_PLAYER_DECK: usize = MAX_DECK;
93pub const PER_PLAYER_BLOCK_LEN: usize = PER_PLAYER_COUNTS
94 + PER_PLAYER_STAGE
95 + PER_PLAYER_CLIMAX_TOP
96 + PER_PLAYER_LEVEL
97 + PER_PLAYER_CLOCK_TOP
98 + PER_PLAYER_WAITING_TOP
99 + PER_PLAYER_RESOLUTION_TOP
100 + PER_PLAYER_STOCK_TOP
101 + PER_PLAYER_HAND
102 + PER_PLAYER_DECK;
103pub const OBS_REASON_BASE: usize = OBS_HEADER_LEN + 2 * PER_PLAYER_BLOCK_LEN;
104pub const OBS_REVEAL_BASE: usize = OBS_REASON_BASE + OBS_REASON_LEN;
105pub const OBS_CONTEXT_BASE: usize = OBS_REVEAL_BASE + OBS_REVEAL_LEN;
106pub const OBS_LEN: usize = OBS_CONTEXT_BASE + OBS_CONTEXT_LEN;
107
108#[allow(clippy::too_many_arguments)]
109pub fn encode_observation(
110 state: &GameState,
111 db: &CardDb,
112 curriculum: &CurriculumConfig,
113 perspective: u8,
114 decision: Option<&Decision>,
115 last_action: Option<&ActionDesc>,
116 last_action_player: Option<u8>,
117 visibility: ObservationVisibility,
118 out: &mut [i32],
119) {
120 let mut slot_powers = [[0i32; MAX_STAGE]; 2];
121 compute_slot_powers_from_state(state, db, &mut slot_powers);
122 encode_observation_with_slot_power(
123 state,
124 db,
125 curriculum,
126 perspective,
127 decision,
128 last_action,
129 last_action_player,
130 visibility,
131 &slot_powers,
132 out,
133 );
134}
135
136#[allow(clippy::too_many_arguments)]
137pub(crate) fn encode_observation_with_slot_power(
138 state: &GameState,
139 db: &CardDb,
140 curriculum: &CurriculumConfig,
141 perspective: u8,
142 decision: Option<&Decision>,
143 last_action: Option<&ActionDesc>,
144 last_action_player: Option<u8>,
145 visibility: ObservationVisibility,
146 slot_powers: &[[i32; MAX_STAGE]; 2],
147 out: &mut [i32],
148) {
149 assert!(out.len() >= OBS_LEN);
150 out.fill(0);
151 let p0 = perspective as usize;
152 let p1 = 1 - p0;
153 out[0] = state.turn.active_player as i32;
154 out[1] = phase_to_i32(state.turn.phase);
155 out[2] = decision_kind_to_i32(decision.map(|d| d.kind));
156 out[3] = decision.map(|d| d.player as i32).unwrap_or(-1);
157 out[4] = terminal_to_i32(state.terminal);
158 let (last_kind, last_p1, last_p2) =
159 last_action_to_fields(last_action, last_action_player, perspective, visibility);
160 out[5] = last_kind;
161 out[6] = last_p1;
162 out[7] = last_p2;
163 if let Some(ctx) = &state.turn.attack {
164 out[8] = ctx.attacker_slot as i32;
165 out[9] = ctx.defender_slot.map(|s| s as i32).unwrap_or(-1);
166 out[10] = attack_type_to_i32(ctx.attack_type);
167 out[11] = ctx.damage;
168 out[12] = ctx.counter_power;
169 } else {
170 out[8] = -1;
171 out[9] = -1;
172 out[10] = -1;
173 out[11] = 0;
174 out[12] = 0;
175 }
176 out[13] = decision
177 .and_then(|d| d.focus_slot.map(|s| s as i32))
178 .unwrap_or(-1);
179 let choice_page = decision
180 .filter(|d| d.kind == DecisionKind::Choice)
181 .and(state.turn.choice.as_ref())
182 .map(|choice| (choice.page_start as i32, choice.total_candidates as i32));
183 if let Some((page_start, total)) = choice_page {
184 out[14] = page_start;
185 out[15] = total;
186 } else {
187 out[14] = -1;
188 out[15] = -1;
189 }
190
191 let mut offset = OBS_HEADER_LEN;
192 for (idx, player_index) in [p0, p1].iter().enumerate() {
193 let p = &state.players[*player_index];
194 out[offset] = p.level.len() as i32;
195 out[offset + 1] = p.clock.len() as i32;
196 out[offset + 2] = p.deck.len() as i32;
197 out[offset + 3] = p.hand.len() as i32;
198 out[offset + 4] = p.stock.len() as i32;
199 out[offset + 5] = p.waiting_room.len() as i32;
200 let memory_visible =
201 if visibility == ObservationVisibility::Public && !curriculum.memory_is_public {
202 *player_index == perspective as usize
203 } else {
204 true
205 };
206 out[offset + 6] = if memory_visible {
207 p.memory.len() as i32
208 } else {
209 0
210 };
211 out[offset + 7] = p.climax.len() as i32;
212 out[offset + 8] = p.resolution.len() as i32;
213 offset += PER_PLAYER_COUNTS;
214
215 for (slot, slot_state) in p.stage.iter().enumerate() {
216 let card_id = slot_state.card.map(|c| c.id).unwrap_or(0) as i32;
217 let status = if slot_state.card.is_some() {
218 status_to_i32(slot_state.status)
219 } else {
220 0
221 };
222 let has_attacked = if slot_state.has_attacked { 1 } else { 0 };
223 let (power, soul) = if let Some(card) = slot_state.card.and_then(|inst| db.get(inst.id))
224 {
225 let power = slot_powers[*player_index][slot];
226 let soul = card.soul as i32;
227 (power, soul)
228 } else {
229 (0, 0)
230 };
231 let base = offset + slot * PER_STAGE_SLOT;
232 out[base] = card_id;
233 out[base + 1] = status;
234 out[base + 2] = has_attacked;
235 out[base + 3] = power;
236 out[base + 4] = soul;
237 }
238 offset += PER_PLAYER_STAGE;
239
240 out[offset] = p.climax.last().map(|c| c.id).unwrap_or(0) as i32;
241 offset += PER_PLAYER_CLIMAX_TOP;
242
243 for i in 0..MAX_LEVEL {
244 out[offset + i] = p.level.get(i).map(|c| c.id).unwrap_or(0) as i32;
245 }
246 offset += PER_PLAYER_LEVEL;
247
248 for i in 0..TOP_CLOCK {
249 let idx = p.clock.len().saturating_sub(1 + i);
250 let value = if idx < p.clock.len() {
251 p.clock[idx].id as i32
252 } else {
253 0
254 };
255 out[offset + i] = value;
256 }
257 offset += PER_PLAYER_CLOCK_TOP;
258
259 for i in 0..TOP_WAITING_ROOM {
260 let idx = p.waiting_room.len().saturating_sub(1 + i);
261 let value = if idx < p.waiting_room.len() {
262 p.waiting_room[idx].id as i32
263 } else {
264 0
265 };
266 out[offset + i] = value;
267 }
268 offset += PER_PLAYER_WAITING_TOP;
269
270 for i in 0..TOP_RESOLUTION {
271 let idx = p.resolution.len().saturating_sub(1 + i);
272 let value = if idx < p.resolution.len() {
273 p.resolution[idx].id as i32
274 } else {
275 0
276 };
277 out[offset + i] = value;
278 }
279 offset += PER_PLAYER_RESOLUTION_TOP;
280
281 for i in 0..TOP_STOCK {
282 let value = if visibility == ObservationVisibility::Full {
283 let idx = p.stock.len().saturating_sub(1 + i);
284 if idx < p.stock.len() {
285 p.stock[idx].id as i32
286 } else {
287 0
288 }
289 } else {
290 -1
291 };
292 out[offset + i] = value;
293 }
294 offset += PER_PLAYER_STOCK_TOP;
295
296 for i in 0..MAX_HAND {
297 let value = if visibility == ObservationVisibility::Full || idx == 0 {
298 p.hand.get(i).map(|c| c.id).unwrap_or(0) as i32
299 } else {
300 -1
301 };
302 out[offset + i] = value;
303 }
304 offset += MAX_HAND;
305
306 for i in 0..MAX_DECK {
307 let value = if visibility == ObservationVisibility::Full {
308 if i < p.deck.len() {
309 let deck_idx = p.deck.len() - 1 - i;
310 p.deck[deck_idx].id as i32
311 } else {
312 0
313 }
314 } else {
315 -1
316 };
317 out[offset + i] = value;
318 }
319 offset += MAX_DECK;
320 }
321
322 let reason_bits = compute_reason_bits(state, db, curriculum, perspective, decision);
323 let reason_base = OBS_REASON_BASE;
324 out[reason_base..reason_base + OBS_REASON_LEN].copy_from_slice(&reason_bits);
325
326 let reveal_base = OBS_REVEAL_BASE;
327 let reveal_slice = &mut out[reveal_base..reveal_base + OBS_REVEAL_LEN];
328 state.reveal_history[p0].write_chronological(reveal_slice);
329
330 let context_base = OBS_CONTEXT_BASE;
331 let context_bits = compute_context_bits(state);
332 out[context_base..context_base + OBS_CONTEXT_LEN].copy_from_slice(&context_bits);
333}
334
335fn compute_slot_powers_from_state(state: &GameState, db: &CardDb, out: &mut [[i32; MAX_STAGE]; 2]) {
336 let mut slot_card_ids = [[0u32; MAX_STAGE]; 2];
337 for (player, p) in state.players.iter().enumerate() {
338 for (slot, slot_state) in p.stage.iter().enumerate() {
339 slot_card_ids[player][slot] = slot_state.card.map(|c| c.id).unwrap_or(0);
340 }
341 }
342 let mut slot_power_mods = [[0i32; MAX_STAGE]; 2];
343 for modifier in &state.modifiers {
344 if modifier.kind != ModifierKind::Power {
345 continue;
346 }
347 let p = modifier.target_player as usize;
348 let s = modifier.target_slot as usize;
349 if p >= 2 || s >= MAX_STAGE {
350 continue;
351 }
352 if slot_card_ids[p][s] != modifier.target_card {
353 continue;
354 }
355 slot_power_mods[p][s] = slot_power_mods[p][s].saturating_add(modifier.magnitude);
356 }
357 for (player, p) in state.players.iter().enumerate() {
358 for (slot, slot_state) in p.stage.iter().enumerate() {
359 let power = if let Some(card) = slot_state.card.and_then(|inst| db.get(inst.id)) {
360 card.power
361 + slot_state.power_mod_turn
362 + slot_state.power_mod_battle
363 + slot_power_mods[player][slot]
364 } else {
365 0
366 };
367 out[player][slot] = power;
368 }
369 }
370}
371
372fn compute_reason_bits(
373 state: &GameState,
374 db: &CardDb,
375 curriculum: &CurriculumConfig,
376 perspective: u8,
377 decision: Option<&Decision>,
378) -> [i32; OBS_REASON_LEN] {
379 let mut out = [0i32; OBS_REASON_LEN];
380 let decision = match decision {
381 Some(decision) if decision.player == perspective => decision,
382 _ => return out,
383 };
384 let in_main = decision.kind == DecisionKind::Main;
385 let in_climax = decision.kind == DecisionKind::Climax;
386 let in_attack = decision.kind == DecisionKind::AttackDeclaration;
387 let in_counter_window = state
388 .turn
389 .priority
390 .as_ref()
391 .map(|p| p.window == crate::state::TimingWindow::CounterWindow)
392 .unwrap_or(false);
393 out[OBS_REASON_IN_MAIN] = i32::from(in_main);
394 out[OBS_REASON_IN_CLIMAX] = i32::from(in_climax);
395 out[OBS_REASON_IN_ATTACK] = i32::from(in_attack);
396 out[OBS_REASON_IN_COUNTER_WINDOW] = i32::from(in_counter_window);
397
398 let p = &state.players[perspective as usize];
399 let mut any_candidate = false;
400 let mut stock_blocked = false;
401 let mut color_blocked = false;
402 if in_main || in_climax {
403 for card_inst in &p.hand {
404 let Some(card) = db.get(card_inst.id) else {
405 continue;
406 };
407 if !card_set_allowed(card, curriculum) {
408 continue;
409 }
410 if in_main {
411 match card.card_type {
412 crate::db::CardType::Character => {
413 if !curriculum.allow_character {
414 continue;
415 }
416 }
417 crate::db::CardType::Event => {
418 if !curriculum.allow_event {
419 continue;
420 }
421 }
422 _ => continue,
423 }
424 } else if in_climax {
425 if card.card_type != crate::db::CardType::Climax || !curriculum.allow_climax {
426 continue;
427 }
428 if !curriculum.enable_climax_phase {
429 continue;
430 }
431 }
432 if !meets_level_requirement(card, p.level.len()) {
433 continue;
434 }
435 any_candidate = true;
436 if !meets_cost_requirement(card, p, curriculum) {
437 stock_blocked = true;
438 }
439 if !meets_color_requirement(card, p, db, curriculum) {
440 color_blocked = true;
441 }
442 }
443 }
444 if in_main || in_climax {
445 out[OBS_REASON_NO_HAND] = i32::from(!any_candidate);
446 out[OBS_REASON_NO_STOCK] = i32::from(stock_blocked);
447 out[OBS_REASON_NO_COLOR] = i32::from(color_blocked);
448 }
449
450 let no_targets = decision.kind == DecisionKind::Choice
451 && state
452 .turn
453 .choice
454 .as_ref()
455 .map(|choice| {
456 choice
457 .options
458 .iter()
459 .all(|opt| opt.zone == crate::state::ChoiceZone::Skip)
460 })
461 .unwrap_or(true);
462 out[OBS_REASON_NO_TARGETS] = i32::from(no_targets);
463
464 out
465}
466
467fn compute_context_bits(state: &GameState) -> [i32; OBS_CONTEXT_LEN] {
468 let mut out = [0i32; OBS_CONTEXT_LEN];
469 out[OBS_CONTEXT_PRIORITY_WINDOW] = i32::from(state.turn.priority.is_some());
470 out[OBS_CONTEXT_CHOICE_ACTIVE] = i32::from(state.turn.choice.is_some());
471 out[OBS_CONTEXT_STACK_NONEMPTY] = i32::from(!state.turn.stack.is_empty());
472 out[OBS_CONTEXT_ENCORE_PENDING] = i32::from(!state.turn.encore_queue.is_empty());
473 out
474}
475
476fn card_set_allowed(card: &crate::db::CardStatic, curriculum: &CurriculumConfig) -> bool {
477 if let Some(set) = curriculum.allowed_card_sets_cache.as_ref() {
478 match &card.card_set {
479 Some(set_id) => set.contains(set_id),
480 None => false,
481 }
482 } else if curriculum.allowed_card_sets.is_empty() {
483 true
484 } else {
485 card.card_set
486 .as_ref()
487 .map(|s| curriculum.allowed_card_sets.iter().any(|a| a == s))
488 .unwrap_or(false)
489 }
490}
491
492fn meets_level_requirement(card: &crate::db::CardStatic, level_count: usize) -> bool {
493 card.level as usize <= level_count
494}
495
496fn meets_cost_requirement(
497 card: &crate::db::CardStatic,
498 player: &crate::state::PlayerState,
499 curriculum: &CurriculumConfig,
500) -> bool {
501 if !curriculum.enforce_cost_requirement {
502 return true;
503 }
504 player.stock.len() >= card.cost as usize
505}
506
507fn meets_color_requirement(
508 card: &crate::db::CardStatic,
509 player: &crate::state::PlayerState,
510 db: &CardDb,
511 curriculum: &CurriculumConfig,
512) -> bool {
513 if !curriculum.enforce_color_requirement {
514 return true;
515 }
516 if card.level == 0 || card.color == crate::db::CardColor::Colorless {
517 return true;
518 }
519 for card_id in player.level.iter().chain(player.clock.iter()) {
520 if let Some(c) = db.get(card_id.id) {
521 if c.color == card.color {
522 return true;
523 }
524 }
525 }
526 false
527}
528
529fn phase_to_i32(phase: Phase) -> i32 {
530 match phase {
531 Phase::Mulligan => 0,
532 Phase::Stand => 1,
533 Phase::Draw => 2,
534 Phase::Clock => 3,
535 Phase::Main => 4,
536 Phase::Climax => 5,
537 Phase::Attack => 6,
538 Phase::End => 7,
539 }
540}
541
542fn decision_kind_to_i32(kind: Option<DecisionKind>) -> i32 {
543 match kind {
544 Some(DecisionKind::Mulligan) => 0,
545 Some(DecisionKind::Clock) => 1,
546 Some(DecisionKind::Main) => 2,
547 Some(DecisionKind::Climax) => 3,
548 Some(DecisionKind::AttackDeclaration) => 4,
549 Some(DecisionKind::LevelUp) => 5,
550 Some(DecisionKind::Encore) => 6,
551 Some(DecisionKind::TriggerOrder) => 7,
552 Some(DecisionKind::Choice) => 8,
553 None => -1,
554 }
555}
556
557fn attack_type_to_i32(attack_type: AttackType) -> i32 {
558 match attack_type {
559 AttackType::Frontal => 0,
560 AttackType::Side => 1,
561 AttackType::Direct => 2,
562 }
563}
564
565fn status_to_i32(status: StageStatus) -> i32 {
566 match status {
567 StageStatus::Stand => 1,
568 StageStatus::Rest => 2,
569 StageStatus::Reverse => 3,
570 }
571}
572
573fn terminal_to_i32(term: Option<TerminalResult>) -> i32 {
574 match term {
575 None => 0,
576 Some(TerminalResult::Win { winner }) => {
577 if winner == 0 {
578 1
579 } else {
580 2
581 }
582 }
583 Some(TerminalResult::Draw) => 3,
584 Some(TerminalResult::Timeout) => 4,
585 }
586}
587
588fn last_action_to_fields(
589 action: Option<&ActionDesc>,
590 actor: Option<u8>,
591 perspective: u8,
592 visibility: ObservationVisibility,
593) -> (i32, i32, i32) {
594 let mask = visibility == ObservationVisibility::Public
595 && actor.map(|p| p != perspective).unwrap_or(false);
596 match action {
597 None => (0, -1, -1),
598 Some(ActionDesc::MulliganConfirm) => (1, -1, -1),
599 Some(ActionDesc::MulliganSelect { hand_index }) => {
600 let idx = if mask { -1 } else { *hand_index as i32 };
601 (2, idx, -1)
602 }
603 Some(ActionDesc::Pass) => (3, -1, -1),
604 Some(ActionDesc::Clock { hand_index }) => {
605 let idx = if mask { -1 } else { *hand_index as i32 };
606 (4, idx, -1)
607 }
608 Some(ActionDesc::MainPlayCharacter {
609 hand_index,
610 stage_slot,
611 }) => {
612 let idx = if mask { -1 } else { *hand_index as i32 };
613 (6, idx, *stage_slot as i32)
614 }
615 Some(ActionDesc::MainPlayEvent { hand_index }) => {
616 let idx = if mask { -1 } else { *hand_index as i32 };
617 (7, idx, -1)
618 }
619 Some(ActionDesc::MainMove { from_slot, to_slot }) => {
620 (8, *from_slot as i32, *to_slot as i32)
621 }
622 Some(ActionDesc::MainActivateAbility {
623 slot,
624 ability_index,
625 }) => (9, *slot as i32, *ability_index as i32),
626 Some(ActionDesc::ClimaxPlay { hand_index }) => {
627 let idx = if mask { -1 } else { *hand_index as i32 };
628 (11, idx, -1)
629 }
630 Some(ActionDesc::Attack { slot, attack_type }) => {
631 (13, *slot as i32, attack_type_to_i32(*attack_type))
632 }
633 Some(ActionDesc::CounterPlay { hand_index }) => {
634 let idx = if mask { -1 } else { *hand_index as i32 };
635 (15, idx, -1)
636 }
637 Some(ActionDesc::LevelUp { index }) => (16, *index as i32, -1),
638 Some(ActionDesc::EncorePay { slot }) => (17, *slot as i32, -1),
639 Some(ActionDesc::EncoreDecline { slot }) => (22, *slot as i32, -1),
640 Some(ActionDesc::TriggerOrder { index }) => (18, *index as i32, -1),
641 Some(ActionDesc::ChoiceSelect { index }) => {
642 let idx = if mask { -1 } else { *index as i32 };
643 (19, idx, -1)
644 }
645 Some(ActionDesc::ChoicePrevPage) => (20, -1, -1),
646 Some(ActionDesc::ChoiceNextPage) => (21, -1, -1),
647 Some(ActionDesc::Concede) => (23, -1, -1),
648 }
649}
650
651pub fn action_id_for(action: &ActionDesc) -> Option<usize> {
652 match action {
653 ActionDesc::MulliganConfirm => Some(MULLIGAN_CONFIRM_ID),
654 ActionDesc::MulliganSelect { hand_index } => {
655 let hi = *hand_index as usize;
656 if hi < MULLIGAN_SELECT_COUNT {
657 Some(MULLIGAN_SELECT_BASE + hi)
658 } else {
659 None
660 }
661 }
662 ActionDesc::Pass => Some(PASS_ACTION_ID),
663 ActionDesc::Clock { hand_index } => {
664 let hi = *hand_index as usize;
665 if hi < MAX_HAND {
666 Some(CLOCK_HAND_BASE + hi)
667 } else {
668 None
669 }
670 }
671 ActionDesc::MainPlayCharacter {
672 hand_index,
673 stage_slot,
674 } => {
675 let hi = *hand_index as usize;
676 let ss = *stage_slot as usize;
677 if hi < MAX_HAND && ss < MAX_STAGE {
678 Some(MAIN_PLAY_CHAR_BASE + hi * MAX_STAGE + ss)
679 } else {
680 None
681 }
682 }
683 ActionDesc::MainPlayEvent { hand_index } => {
684 let hi = *hand_index as usize;
685 if hi < MAX_HAND {
686 Some(MAIN_PLAY_EVENT_BASE + hi)
687 } else {
688 None
689 }
690 }
691 ActionDesc::MainMove { from_slot, to_slot } => {
692 let fs = *from_slot as usize;
693 let ts = *to_slot as usize;
694 if fs < MAX_STAGE && ts < MAX_STAGE && fs != ts {
695 let to_index = if ts < fs { ts } else { ts - 1 };
696 Some(MAIN_MOVE_BASE + fs * (MAX_STAGE - 1) + to_index)
697 } else {
698 None
699 }
700 }
701 ActionDesc::MainActivateAbility {
702 slot,
703 ability_index,
704 } => {
705 let _ = (slot, ability_index);
706 None
707 }
708 ActionDesc::ClimaxPlay { hand_index } => {
709 let hi = *hand_index as usize;
710 if hi < MAX_HAND {
711 Some(CLIMAX_PLAY_BASE + hi)
712 } else {
713 None
714 }
715 }
716 ActionDesc::Attack { slot, attack_type } => {
717 let s = *slot as usize;
718 let t = attack_type_to_i32(*attack_type) as usize;
719 if s < ATTACK_SLOT_COUNT && t < 3 {
720 Some(ATTACK_BASE + s * 3 + t)
721 } else {
722 None
723 }
724 }
725 ActionDesc::CounterPlay { hand_index } => {
726 let _ = hand_index;
727 None
728 }
729 ActionDesc::LevelUp { index } => {
730 let idx = *index as usize;
731 if idx < LEVEL_UP_COUNT {
732 Some(LEVEL_UP_BASE + idx)
733 } else {
734 None
735 }
736 }
737 ActionDesc::EncorePay { slot } => {
738 let s = *slot as usize;
739 if s < ENCORE_PAY_COUNT {
740 Some(ENCORE_PAY_BASE + s)
741 } else {
742 None
743 }
744 }
745 ActionDesc::EncoreDecline { slot } => {
746 let s = *slot as usize;
747 if s < ENCORE_DECLINE_COUNT {
748 Some(ENCORE_DECLINE_BASE + s)
749 } else {
750 None
751 }
752 }
753 ActionDesc::TriggerOrder { index } => {
754 let idx = *index as usize;
755 if idx < TRIGGER_ORDER_COUNT {
756 Some(TRIGGER_ORDER_BASE + idx)
757 } else {
758 None
759 }
760 }
761 ActionDesc::ChoiceSelect { index } => {
762 let idx = *index as usize;
763 if idx < CHOICE_COUNT {
764 Some(CHOICE_BASE + idx)
765 } else {
766 None
767 }
768 }
769 ActionDesc::ChoicePrevPage => Some(CHOICE_PREV_ID),
770 ActionDesc::ChoiceNextPage => Some(CHOICE_NEXT_ID),
771 ActionDesc::Concede => Some(CONCEDE_ID),
772 }
773}
774
775pub fn fill_action_mask(
776 actions: &[ActionDesc],
777 mask: &mut [u8],
778 lookup: &mut [Option<ActionDesc>],
779) {
780 mask.fill(0);
781 for slot in lookup.iter_mut() {
782 *slot = None;
783 }
784 for action in actions {
785 if let Some(id) = action_id_for(action) {
786 if id < ACTION_SPACE_SIZE {
787 mask[id] = 1;
788 lookup[id] = Some(action.clone());
789 }
790 }
791 }
792}
793
794pub fn build_action_mask(actions: &[ActionDesc]) -> (Vec<u8>, Vec<Option<ActionDesc>>) {
795 let mut mask = vec![0u8; ACTION_SPACE_SIZE];
796 let mut lookup = vec![None; ACTION_SPACE_SIZE];
797 fill_action_mask(actions, &mut mask, &mut lookup);
798 (mask, lookup)
799}