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