Skip to main content

weiss_core/encode/
action_ids.rs

1use serde::Serialize;
2
3use crate::legal::ActionDesc;
4use crate::state::AttackType;
5
6use super::constants::*;
7
8/// Parameter value for an action id description.
9#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
10#[serde(untagged)]
11pub enum ActionParamValue {
12    /// Integer parameter.
13    Int(i32),
14    /// String parameter.
15    Str(&'static str),
16}
17
18/// Named parameter for an action id description.
19#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
20pub struct ActionParam {
21    /// Parameter name.
22    pub name: &'static str,
23    /// Parameter value.
24    pub value: ActionParamValue,
25}
26
27/// Human-readable description of an action id.
28#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
29pub struct ActionIdDesc {
30    /// Action family name.
31    pub family: &'static str,
32    /// Parameters associated with the action.
33    pub params: Vec<ActionParam>,
34}
35
36/// Machine-friendly factorized description of an action id.
37#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
38pub struct FactorizedActionDesc {
39    /// Action family name.
40    pub family: &'static str,
41    /// First factorized argument slot.
42    pub arg0: Option<u16>,
43    /// Second factorized argument slot.
44    pub arg1: Option<u16>,
45    /// Third factorized argument slot.
46    pub arg2: Option<u16>,
47}
48
49/// Number of `u16` fields exported for each legal action metadata row.
50pub const ACTION_META_WIDTH: usize = 4;
51/// Sentinel value used for unused action metadata arguments.
52pub const ACTION_META_UNUSED: u16 = u16::MAX;
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55#[repr(u8)]
56enum ActionFamily {
57    MulliganConfirm,
58    MulliganSelect,
59    Pass,
60    ClockFromHand,
61    MainPlayCharacter,
62    MainPlayEvent,
63    MainMove,
64    ClimaxPlay,
65    Attack,
66    LevelUp,
67    EncorePay,
68    EncoreDecline,
69    TriggerOrder,
70    ChoiceSelect,
71    ChoicePrevPage,
72    ChoiceNextPage,
73    Concede,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77enum ActionKey {
78    MulliganConfirm,
79    MulliganSelect {
80        hand_index: usize,
81    },
82    Pass,
83    ClockFromHand {
84        hand_index: usize,
85    },
86    MainPlayCharacter {
87        hand_index: usize,
88        stage_slot: usize,
89    },
90    MainPlayEvent {
91        hand_index: usize,
92    },
93    MainMove {
94        from_slot: usize,
95        to_slot: usize,
96    },
97    ClimaxPlay {
98        hand_index: usize,
99    },
100    Attack {
101        slot: usize,
102        attack_type_code: usize,
103    },
104    LevelUp {
105        index: usize,
106    },
107    EncorePay {
108        slot: usize,
109    },
110    EncoreDecline {
111        slot: usize,
112    },
113    TriggerOrder {
114        index: usize,
115    },
116    ChoiceSelect {
117        index: usize,
118    },
119    ChoicePrevPage,
120    ChoiceNextPage,
121    Concede,
122}
123
124const ACTION_FAMILY_ORDER: [ActionFamily; 17] = [
125    ActionFamily::MulliganConfirm,
126    ActionFamily::MulliganSelect,
127    ActionFamily::Pass,
128    ActionFamily::ClockFromHand,
129    ActionFamily::MainPlayCharacter,
130    ActionFamily::MainPlayEvent,
131    ActionFamily::MainMove,
132    ActionFamily::ClimaxPlay,
133    ActionFamily::Attack,
134    ActionFamily::LevelUp,
135    ActionFamily::EncorePay,
136    ActionFamily::EncoreDecline,
137    ActionFamily::TriggerOrder,
138    ActionFamily::ChoiceSelect,
139    ActionFamily::ChoicePrevPage,
140    ActionFamily::ChoiceNextPage,
141    ActionFamily::Concede,
142];
143
144const ACTION_FAMILY_BASES: [usize; ACTION_FAMILY_ORDER.len()] = [
145    MULLIGAN_CONFIRM_ID,
146    MULLIGAN_SELECT_BASE,
147    PASS_ACTION_ID,
148    CLOCK_HAND_BASE,
149    MAIN_PLAY_CHAR_BASE,
150    MAIN_PLAY_EVENT_BASE,
151    MAIN_MOVE_BASE,
152    CLIMAX_PLAY_BASE,
153    ATTACK_BASE,
154    LEVEL_UP_BASE,
155    ENCORE_PAY_BASE,
156    ENCORE_DECLINE_BASE,
157    TRIGGER_ORDER_BASE,
158    CHOICE_BASE,
159    CHOICE_PREV_ID,
160    CHOICE_NEXT_ID,
161    CONCEDE_ID,
162];
163
164const ACTION_FAMILY_COUNTS: [usize; ACTION_FAMILY_ORDER.len()] = [
165    1,
166    MULLIGAN_SELECT_COUNT,
167    1,
168    CLOCK_HAND_COUNT,
169    MAIN_PLAY_CHAR_COUNT,
170    MAIN_PLAY_EVENT_COUNT,
171    MAIN_MOVE_COUNT,
172    CLIMAX_PLAY_COUNT,
173    ATTACK_COUNT,
174    LEVEL_UP_COUNT,
175    ENCORE_PAY_COUNT,
176    ENCORE_DECLINE_COUNT,
177    TRIGGER_ORDER_COUNT,
178    CHOICE_COUNT,
179    1,
180    1,
181    1,
182];
183
184#[inline]
185const fn action_family_idx(family: ActionFamily) -> usize {
186    family as usize
187}
188
189#[inline]
190fn action_family_base(family: ActionFamily) -> usize {
191    ACTION_FAMILY_BASES[action_family_idx(family)]
192}
193
194#[inline]
195fn action_family_count(family: ActionFamily) -> usize {
196    ACTION_FAMILY_COUNTS[action_family_idx(family)]
197}
198
199#[inline]
200fn action_family_offset_for_id(id: usize) -> Option<(ActionFamily, usize)> {
201    if id >= ACTION_SPACE_SIZE {
202        return None;
203    }
204    for family in ACTION_FAMILY_ORDER {
205        let base = action_family_base(family);
206        if id < base {
207            break;
208        }
209        let offset = id - base;
210        if offset < action_family_count(family) {
211            return Some((family, offset));
212        }
213    }
214    None
215}
216
217#[inline]
218fn action_key_from_family_offset(family: ActionFamily, offset: usize) -> ActionKey {
219    match family {
220        ActionFamily::MulliganConfirm => ActionKey::MulliganConfirm,
221        ActionFamily::MulliganSelect => ActionKey::MulliganSelect { hand_index: offset },
222        ActionFamily::Pass => ActionKey::Pass,
223        ActionFamily::ClockFromHand => ActionKey::ClockFromHand { hand_index: offset },
224        ActionFamily::MainPlayCharacter => ActionKey::MainPlayCharacter {
225            hand_index: offset / MAX_STAGE,
226            stage_slot: offset % MAX_STAGE,
227        },
228        ActionFamily::MainPlayEvent => ActionKey::MainPlayEvent { hand_index: offset },
229        ActionFamily::MainMove => {
230            let from_slot = offset / (MAX_STAGE - 1);
231            let to_index = offset % (MAX_STAGE - 1);
232            let to_slot = if to_index >= from_slot {
233                to_index + 1
234            } else {
235                to_index
236            };
237            ActionKey::MainMove { from_slot, to_slot }
238        }
239        ActionFamily::ClimaxPlay => ActionKey::ClimaxPlay { hand_index: offset },
240        ActionFamily::Attack => ActionKey::Attack {
241            slot: offset / 3,
242            attack_type_code: offset % 3,
243        },
244        ActionFamily::LevelUp => ActionKey::LevelUp { index: offset },
245        ActionFamily::EncorePay => ActionKey::EncorePay { slot: offset },
246        ActionFamily::EncoreDecline => ActionKey::EncoreDecline { slot: offset },
247        ActionFamily::TriggerOrder => ActionKey::TriggerOrder { index: offset },
248        ActionFamily::ChoiceSelect => ActionKey::ChoiceSelect { index: offset },
249        ActionFamily::ChoicePrevPage => ActionKey::ChoicePrevPage,
250        ActionFamily::ChoiceNextPage => ActionKey::ChoiceNextPage,
251        ActionFamily::Concede => ActionKey::Concede,
252    }
253}
254
255#[inline]
256fn action_key_for_id(id: usize) -> Option<ActionKey> {
257    let (family, offset) = action_family_offset_for_id(id)?;
258    Some(action_key_from_family_offset(family, offset))
259}
260
261#[inline]
262fn action_desc_for_key(action: ActionKey) -> ActionDesc {
263    match action {
264        ActionKey::MulliganConfirm => ActionDesc::MulliganConfirm,
265        ActionKey::MulliganSelect { hand_index } => ActionDesc::MulliganSelect {
266            hand_index: hand_index as u8,
267        },
268        ActionKey::Pass => ActionDesc::Pass,
269        ActionKey::ClockFromHand { hand_index } => ActionDesc::Clock {
270            hand_index: hand_index as u8,
271        },
272        ActionKey::MainPlayCharacter {
273            hand_index,
274            stage_slot,
275        } => ActionDesc::MainPlayCharacter {
276            hand_index: hand_index as u8,
277            stage_slot: stage_slot as u8,
278        },
279        ActionKey::MainPlayEvent { hand_index } => ActionDesc::MainPlayEvent {
280            hand_index: hand_index as u8,
281        },
282        ActionKey::MainMove { from_slot, to_slot } => ActionDesc::MainMove {
283            from_slot: from_slot as u8,
284            to_slot: to_slot as u8,
285        },
286        ActionKey::ClimaxPlay { hand_index } => ActionDesc::ClimaxPlay {
287            hand_index: hand_index as u8,
288        },
289        ActionKey::Attack {
290            slot,
291            attack_type_code,
292        } => ActionDesc::Attack {
293            slot: slot as u8,
294            attack_type: attack_type_from_code(attack_type_code),
295        },
296        ActionKey::LevelUp { index } => ActionDesc::LevelUp { index: index as u8 },
297        ActionKey::EncorePay { slot } => ActionDesc::EncorePay { slot: slot as u8 },
298        ActionKey::EncoreDecline { slot } => ActionDesc::EncoreDecline { slot: slot as u8 },
299        ActionKey::TriggerOrder { index } => ActionDesc::TriggerOrder { index: index as u8 },
300        ActionKey::ChoiceSelect { index } => ActionDesc::ChoiceSelect { index: index as u8 },
301        ActionKey::ChoicePrevPage => ActionDesc::ChoicePrevPage,
302        ActionKey::ChoiceNextPage => ActionDesc::ChoiceNextPage,
303        ActionKey::Concede => ActionDesc::Concede,
304    }
305}
306
307#[inline]
308fn action_id_desc_for_key(action: ActionKey) -> ActionIdDesc {
309    match action {
310        ActionKey::MulliganConfirm => ActionIdDesc {
311            family: "mulligan_confirm",
312            params: vec![],
313        },
314        ActionKey::MulliganSelect { hand_index } => ActionIdDesc {
315            family: "mulligan_select",
316            params: vec![ActionParam {
317                name: "hand_index",
318                value: ActionParamValue::Int(hand_index as i32),
319            }],
320        },
321        ActionKey::Pass => ActionIdDesc {
322            family: "pass",
323            params: vec![],
324        },
325        ActionKey::ClockFromHand { hand_index } => ActionIdDesc {
326            family: "clock_from_hand",
327            params: vec![ActionParam {
328                name: "hand_index",
329                value: ActionParamValue::Int(hand_index as i32),
330            }],
331        },
332        ActionKey::MainPlayCharacter {
333            hand_index,
334            stage_slot,
335        } => ActionIdDesc {
336            family: "main_play_character",
337            params: vec![
338                ActionParam {
339                    name: "hand_index",
340                    value: ActionParamValue::Int(hand_index as i32),
341                },
342                ActionParam {
343                    name: "stage_slot",
344                    value: ActionParamValue::Int(stage_slot as i32),
345                },
346            ],
347        },
348        ActionKey::MainPlayEvent { hand_index } => ActionIdDesc {
349            family: "main_play_event",
350            params: vec![ActionParam {
351                name: "hand_index",
352                value: ActionParamValue::Int(hand_index as i32),
353            }],
354        },
355        ActionKey::MainMove { from_slot, to_slot } => ActionIdDesc {
356            family: "main_move",
357            params: vec![
358                ActionParam {
359                    name: "from_slot",
360                    value: ActionParamValue::Int(from_slot as i32),
361                },
362                ActionParam {
363                    name: "to_slot",
364                    value: ActionParamValue::Int(to_slot as i32),
365                },
366            ],
367        },
368        ActionKey::ClimaxPlay { hand_index } => ActionIdDesc {
369            family: "climax_play",
370            params: vec![ActionParam {
371                name: "hand_index",
372                value: ActionParamValue::Int(hand_index as i32),
373            }],
374        },
375        ActionKey::Attack {
376            slot,
377            attack_type_code,
378        } => ActionIdDesc {
379            family: "attack",
380            params: vec![
381                ActionParam {
382                    name: "slot",
383                    value: ActionParamValue::Int(slot as i32),
384                },
385                ActionParam {
386                    name: "attack_type",
387                    value: ActionParamValue::Str(match attack_type_code {
388                        0 => "frontal",
389                        1 => "side",
390                        _ => "direct",
391                    }),
392                },
393            ],
394        },
395        ActionKey::LevelUp { index } => ActionIdDesc {
396            family: "level_up",
397            params: vec![ActionParam {
398                name: "index",
399                value: ActionParamValue::Int(index as i32),
400            }],
401        },
402        ActionKey::EncorePay { slot } => ActionIdDesc {
403            family: "encore_pay",
404            params: vec![ActionParam {
405                name: "slot",
406                value: ActionParamValue::Int(slot as i32),
407            }],
408        },
409        ActionKey::EncoreDecline { slot } => ActionIdDesc {
410            family: "encore_decline",
411            params: vec![ActionParam {
412                name: "slot",
413                value: ActionParamValue::Int(slot as i32),
414            }],
415        },
416        ActionKey::TriggerOrder { index } => ActionIdDesc {
417            family: "trigger_order",
418            params: vec![ActionParam {
419                name: "index",
420                value: ActionParamValue::Int(index as i32),
421            }],
422        },
423        ActionKey::ChoiceSelect { index } => ActionIdDesc {
424            family: "choice_select",
425            params: vec![ActionParam {
426                name: "index",
427                value: ActionParamValue::Int(index as i32),
428            }],
429        },
430        ActionKey::ChoicePrevPage => ActionIdDesc {
431            family: "choice_prev_page",
432            params: vec![],
433        },
434        ActionKey::ChoiceNextPage => ActionIdDesc {
435            family: "choice_next_page",
436            params: vec![],
437        },
438        ActionKey::Concede => ActionIdDesc {
439            family: "concede",
440            params: vec![],
441        },
442    }
443}
444
445fn action_meta_for_key(action: ActionKey) -> [u16; ACTION_META_WIDTH] {
446    let unused = ACTION_META_UNUSED;
447    match action {
448        ActionKey::MulliganConfirm => {
449            [ActionFamily::MulliganConfirm as u16, unused, unused, unused]
450        }
451        ActionKey::MulliganSelect { hand_index } => [
452            ActionFamily::MulliganSelect as u16,
453            hand_index as u16,
454            unused,
455            unused,
456        ],
457        ActionKey::Pass => [ActionFamily::Pass as u16, unused, unused, unused],
458        ActionKey::ClockFromHand { hand_index } => [
459            ActionFamily::ClockFromHand as u16,
460            hand_index as u16,
461            unused,
462            unused,
463        ],
464        ActionKey::MainPlayCharacter {
465            hand_index,
466            stage_slot,
467        } => [
468            ActionFamily::MainPlayCharacter as u16,
469            hand_index as u16,
470            stage_slot as u16,
471            unused,
472        ],
473        ActionKey::MainPlayEvent { hand_index } => [
474            ActionFamily::MainPlayEvent as u16,
475            hand_index as u16,
476            unused,
477            unused,
478        ],
479        ActionKey::MainMove { from_slot, to_slot } => [
480            ActionFamily::MainMove as u16,
481            from_slot as u16,
482            to_slot as u16,
483            unused,
484        ],
485        ActionKey::ClimaxPlay { hand_index } => [
486            ActionFamily::ClimaxPlay as u16,
487            hand_index as u16,
488            unused,
489            unused,
490        ],
491        ActionKey::Attack {
492            slot,
493            attack_type_code,
494        } => [
495            ActionFamily::Attack as u16,
496            slot as u16,
497            attack_type_code as u16,
498            unused,
499        ],
500        ActionKey::LevelUp { index } => {
501            [ActionFamily::LevelUp as u16, index as u16, unused, unused]
502        }
503        ActionKey::EncorePay { slot } => {
504            [ActionFamily::EncorePay as u16, slot as u16, unused, unused]
505        }
506        ActionKey::EncoreDecline { slot } => [
507            ActionFamily::EncoreDecline as u16,
508            slot as u16,
509            unused,
510            unused,
511        ],
512        ActionKey::TriggerOrder { index } => [
513            ActionFamily::TriggerOrder as u16,
514            index as u16,
515            unused,
516            unused,
517        ],
518        ActionKey::ChoiceSelect { index } => [
519            ActionFamily::ChoiceSelect as u16,
520            index as u16,
521            unused,
522            unused,
523        ],
524        ActionKey::ChoicePrevPage => [ActionFamily::ChoicePrevPage as u16, unused, unused, unused],
525        ActionKey::ChoiceNextPage => [ActionFamily::ChoiceNextPage as u16, unused, unused, unused],
526        ActionKey::Concede => [ActionFamily::Concede as u16, unused, unused, unused],
527    }
528}
529
530#[inline]
531fn factorized_action_desc_for_key(action: ActionKey) -> FactorizedActionDesc {
532    match action {
533        ActionKey::MulliganConfirm => FactorizedActionDesc {
534            family: "mulligan_confirm",
535            arg0: None,
536            arg1: None,
537            arg2: None,
538        },
539        ActionKey::MulliganSelect { hand_index } => FactorizedActionDesc {
540            family: "mulligan_select",
541            arg0: Some(hand_index as u16),
542            arg1: None,
543            arg2: None,
544        },
545        ActionKey::Pass => FactorizedActionDesc {
546            family: "pass",
547            arg0: None,
548            arg1: None,
549            arg2: None,
550        },
551        ActionKey::ClockFromHand { hand_index } => FactorizedActionDesc {
552            family: "clock_from_hand",
553            arg0: Some(hand_index as u16),
554            arg1: None,
555            arg2: None,
556        },
557        ActionKey::MainPlayCharacter {
558            hand_index,
559            stage_slot,
560        } => FactorizedActionDesc {
561            family: "main_play_character",
562            arg0: Some(hand_index as u16),
563            arg1: Some(stage_slot as u16),
564            arg2: None,
565        },
566        ActionKey::MainPlayEvent { hand_index } => FactorizedActionDesc {
567            family: "main_play_event",
568            arg0: Some(hand_index as u16),
569            arg1: None,
570            arg2: None,
571        },
572        ActionKey::MainMove { from_slot, to_slot } => FactorizedActionDesc {
573            family: "main_move",
574            arg0: Some(from_slot as u16),
575            arg1: Some(to_slot as u16),
576            arg2: None,
577        },
578        ActionKey::ClimaxPlay { hand_index } => FactorizedActionDesc {
579            family: "climax_play",
580            arg0: Some(hand_index as u16),
581            arg1: None,
582            arg2: None,
583        },
584        ActionKey::Attack {
585            slot,
586            attack_type_code,
587        } => FactorizedActionDesc {
588            family: "attack",
589            arg0: Some(slot as u16),
590            arg1: Some(attack_type_code as u16),
591            arg2: None,
592        },
593        ActionKey::LevelUp { index } => FactorizedActionDesc {
594            family: "level_up",
595            arg0: Some(index as u16),
596            arg1: None,
597            arg2: None,
598        },
599        ActionKey::EncorePay { slot } => FactorizedActionDesc {
600            family: "encore_pay",
601            arg0: Some(slot as u16),
602            arg1: None,
603            arg2: None,
604        },
605        ActionKey::EncoreDecline { slot } => FactorizedActionDesc {
606            family: "encore_decline",
607            arg0: Some(slot as u16),
608            arg1: None,
609            arg2: None,
610        },
611        ActionKey::TriggerOrder { index } => FactorizedActionDesc {
612            family: "trigger_order",
613            arg0: Some(index as u16),
614            arg1: None,
615            arg2: None,
616        },
617        ActionKey::ChoiceSelect { index } => FactorizedActionDesc {
618            family: "choice_select",
619            arg0: Some(index as u16),
620            arg1: None,
621            arg2: None,
622        },
623        ActionKey::ChoicePrevPage => FactorizedActionDesc {
624            family: "choice_prev_page",
625            arg0: None,
626            arg1: None,
627            arg2: None,
628        },
629        ActionKey::ChoiceNextPage => FactorizedActionDesc {
630            family: "choice_next_page",
631            arg0: None,
632            arg1: None,
633            arg2: None,
634        },
635        ActionKey::Concede => FactorizedActionDesc {
636            family: "concede",
637            arg0: None,
638            arg1: None,
639            arg2: None,
640        },
641    }
642}
643
644#[inline]
645fn action_key_for_factorized_desc(desc: &FactorizedActionDesc) -> Option<ActionKey> {
646    match desc.family {
647        "mulligan_confirm" if desc.arg0.is_none() && desc.arg1.is_none() && desc.arg2.is_none() => {
648            Some(ActionKey::MulliganConfirm)
649        }
650        "mulligan_select" if desc.arg1.is_none() && desc.arg2.is_none() => {
651            let hand_index = usize::from(desc.arg0?);
652            (hand_index < MULLIGAN_SELECT_COUNT).then_some(ActionKey::MulliganSelect { hand_index })
653        }
654        "pass" if desc.arg0.is_none() && desc.arg1.is_none() && desc.arg2.is_none() => {
655            Some(ActionKey::Pass)
656        }
657        "clock_from_hand" if desc.arg1.is_none() && desc.arg2.is_none() => {
658            let hand_index = usize::from(desc.arg0?);
659            (hand_index < CLOCK_HAND_COUNT).then_some(ActionKey::ClockFromHand { hand_index })
660        }
661        "main_play_character" if desc.arg2.is_none() => match (desc.arg0, desc.arg1) {
662            (Some(hand_index), Some(stage_slot)) => {
663                let hand_index = usize::from(hand_index);
664                let stage_slot = usize::from(stage_slot);
665                (hand_index < MAX_HAND && stage_slot < MAX_STAGE).then_some(
666                    ActionKey::MainPlayCharacter {
667                        hand_index,
668                        stage_slot,
669                    },
670                )
671            }
672            _ => None,
673        },
674        "main_play_event" if desc.arg1.is_none() && desc.arg2.is_none() => {
675            let hand_index = usize::from(desc.arg0?);
676            (hand_index < MAIN_PLAY_EVENT_COUNT).then_some(ActionKey::MainPlayEvent { hand_index })
677        }
678        "main_move" if desc.arg2.is_none() => match (desc.arg0, desc.arg1) {
679            (Some(from_slot), Some(to_slot)) => {
680                let from_slot = usize::from(from_slot);
681                let to_slot = usize::from(to_slot);
682                (from_slot < MAX_STAGE && to_slot < MAX_STAGE && from_slot != to_slot)
683                    .then_some(ActionKey::MainMove { from_slot, to_slot })
684            }
685            _ => None,
686        },
687        "climax_play" if desc.arg1.is_none() && desc.arg2.is_none() => {
688            let hand_index = usize::from(desc.arg0?);
689            (hand_index < CLIMAX_PLAY_COUNT).then_some(ActionKey::ClimaxPlay { hand_index })
690        }
691        "attack" if desc.arg2.is_none() => match (desc.arg0, desc.arg1) {
692            (Some(slot), Some(attack_type_code)) => {
693                let slot = usize::from(slot);
694                let attack_type_code = usize::from(attack_type_code);
695                (slot < ATTACK_SLOT_COUNT && attack_type_code < 3).then_some(ActionKey::Attack {
696                    slot,
697                    attack_type_code,
698                })
699            }
700            _ => None,
701        },
702        "level_up" if desc.arg1.is_none() && desc.arg2.is_none() => {
703            let index = usize::from(desc.arg0?);
704            (index < LEVEL_UP_COUNT).then_some(ActionKey::LevelUp { index })
705        }
706        "encore_pay" if desc.arg1.is_none() && desc.arg2.is_none() => {
707            let slot = usize::from(desc.arg0?);
708            (slot < ENCORE_PAY_COUNT).then_some(ActionKey::EncorePay { slot })
709        }
710        "encore_decline" if desc.arg1.is_none() && desc.arg2.is_none() => {
711            let slot = usize::from(desc.arg0?);
712            (slot < ENCORE_DECLINE_COUNT).then_some(ActionKey::EncoreDecline { slot })
713        }
714        "trigger_order" if desc.arg1.is_none() && desc.arg2.is_none() => {
715            let index = usize::from(desc.arg0?);
716            (index < TRIGGER_ORDER_COUNT).then_some(ActionKey::TriggerOrder { index })
717        }
718        "choice_select" if desc.arg1.is_none() && desc.arg2.is_none() => {
719            let index = usize::from(desc.arg0?);
720            (index < CHOICE_COUNT).then_some(ActionKey::ChoiceSelect { index })
721        }
722        "choice_prev_page" if desc.arg0.is_none() && desc.arg1.is_none() && desc.arg2.is_none() => {
723            Some(ActionKey::ChoicePrevPage)
724        }
725        "choice_next_page" if desc.arg0.is_none() && desc.arg1.is_none() && desc.arg2.is_none() => {
726            Some(ActionKey::ChoiceNextPage)
727        }
728        "concede" if desc.arg0.is_none() && desc.arg1.is_none() && desc.arg2.is_none() => {
729            Some(ActionKey::Concede)
730        }
731        _ => None,
732    }
733}
734
735/// Decode an action id into a human-readable description.
736pub fn decode_action_id(id: usize) -> Option<ActionIdDesc> {
737    let action = action_key_for_id(id)?;
738    Some(action_id_desc_for_key(action))
739}
740
741/// Decode an action id into a factorized family/argument description.
742pub fn decode_factorized_action_id(id: usize) -> Option<FactorizedActionDesc> {
743    let action = action_key_for_id(id)?;
744    Some(factorized_action_desc_for_key(action))
745}
746
747/// Encode a factorized family/argument description into an action id.
748pub fn encode_factorized_action(desc: &FactorizedActionDesc) -> Option<usize> {
749    let action = action_key_for_factorized_desc(desc)?;
750    action_id_for(&action_desc_for_key(action))
751}
752
753/// Decode an action id into packed legal-action metadata.
754pub(crate) fn action_meta_for_id(id: usize) -> Option<[u16; ACTION_META_WIDTH]> {
755    let action = action_key_for_id(id)?;
756    Some(action_meta_for_key(action))
757}
758
759/// Decode an action id into a canonical action descriptor.
760pub fn action_desc_for_id(id: usize) -> Option<ActionDesc> {
761    let action = action_key_for_id(id)?;
762    Some(action_desc_for_key(action))
763}
764
765/// Encode a canonical action descriptor into an action id.
766pub fn action_id_for(action: &ActionDesc) -> Option<usize> {
767    match action {
768        ActionDesc::MulliganConfirm => Some(MULLIGAN_CONFIRM_ID),
769        ActionDesc::MulliganSelect { hand_index } => {
770            let hi = *hand_index as usize;
771            if hi < MULLIGAN_SELECT_COUNT {
772                Some(MULLIGAN_SELECT_BASE + hi)
773            } else {
774                None
775            }
776        }
777        ActionDesc::Pass => Some(PASS_ACTION_ID),
778        ActionDesc::Clock { hand_index } => {
779            let hi = *hand_index as usize;
780            if hi < MAX_HAND {
781                Some(CLOCK_HAND_BASE + hi)
782            } else {
783                None
784            }
785        }
786        ActionDesc::MainPlayCharacter {
787            hand_index,
788            stage_slot,
789        } => {
790            let hi = *hand_index as usize;
791            let ss = *stage_slot as usize;
792            if hi < MAX_HAND && ss < MAX_STAGE {
793                Some(MAIN_PLAY_CHAR_BASE + hi * MAX_STAGE + ss)
794            } else {
795                None
796            }
797        }
798        ActionDesc::MainPlayEvent { hand_index } => {
799            let hi = *hand_index as usize;
800            if hi < MAX_HAND {
801                Some(MAIN_PLAY_EVENT_BASE + hi)
802            } else {
803                None
804            }
805        }
806        ActionDesc::MainMove { from_slot, to_slot } => {
807            let fs = *from_slot as usize;
808            let ts = *to_slot as usize;
809            if fs < MAX_STAGE && ts < MAX_STAGE && fs != ts {
810                let to_index = if ts < fs { ts } else { ts - 1 };
811                Some(MAIN_MOVE_BASE + fs * (MAX_STAGE - 1) + to_index)
812            } else {
813                None
814            }
815        }
816        ActionDesc::MainActivateAbility { .. } => None,
817        ActionDesc::ClimaxPlay { hand_index } => {
818            let hi = *hand_index as usize;
819            if hi < MAX_HAND {
820                Some(CLIMAX_PLAY_BASE + hi)
821            } else {
822                None
823            }
824        }
825        ActionDesc::Attack { slot, attack_type } => {
826            let s = *slot as usize;
827            let t = attack_type_to_i32(*attack_type) as usize;
828            if s < ATTACK_SLOT_COUNT && t < 3 {
829                Some(ATTACK_BASE + s * 3 + t)
830            } else {
831                None
832            }
833        }
834        ActionDesc::CounterPlay { .. } => None,
835        ActionDesc::LevelUp { index } => {
836            let idx = *index as usize;
837            if idx < LEVEL_UP_COUNT {
838                Some(LEVEL_UP_BASE + idx)
839            } else {
840                None
841            }
842        }
843        ActionDesc::EncorePay { slot } => {
844            let s = *slot as usize;
845            if s < ENCORE_PAY_COUNT {
846                Some(ENCORE_PAY_BASE + s)
847            } else {
848                None
849            }
850        }
851        ActionDesc::EncoreDecline { slot } => {
852            let s = *slot as usize;
853            if s < ENCORE_DECLINE_COUNT {
854                Some(ENCORE_DECLINE_BASE + s)
855            } else {
856                None
857            }
858        }
859        ActionDesc::TriggerOrder { index } => {
860            let idx = *index as usize;
861            if idx < TRIGGER_ORDER_COUNT {
862                Some(TRIGGER_ORDER_BASE + idx)
863            } else {
864                None
865            }
866        }
867        ActionDesc::ChoiceSelect { index } => {
868            let idx = *index as usize;
869            if idx < CHOICE_COUNT {
870                Some(CHOICE_BASE + idx)
871            } else {
872                None
873            }
874        }
875        ActionDesc::ChoicePrevPage => Some(CHOICE_PREV_ID),
876        ActionDesc::ChoiceNextPage => Some(CHOICE_NEXT_ID),
877        ActionDesc::Concede => Some(CONCEDE_ID),
878    }
879}
880
881fn attack_type_to_i32(attack_type: AttackType) -> i32 {
882    match attack_type {
883        AttackType::Frontal => 0,
884        AttackType::Side => 1,
885        AttackType::Direct => 2,
886    }
887}
888
889#[inline]
890fn attack_type_from_code(code: usize) -> AttackType {
891    match code {
892        0 => AttackType::Frontal,
893        1 => AttackType::Side,
894        _ => AttackType::Direct,
895    }
896}