Skip to main content

weiss_core/encode/
mod.rs

1//! Observation/action encoding and spec helpers.
2//!
3//! Related docs:
4//! - <https://github.com/victorwp288/weiss-schwarz-simulator/blob/main/docs/README.md>
5//! - <https://github.com/victorwp288/weiss-schwarz-simulator/blob/main/docs/encodings.md>
6//! - <https://github.com/victorwp288/weiss-schwarz-simulator/blob/main/docs/encodings_changelog.md>
7
8mod action_ids;
9mod constants;
10mod mask;
11mod observation;
12mod spec;
13
14pub(crate) use action_ids::action_meta_for_id;
15pub use action_ids::{
16    action_desc_for_id, action_id_for, decode_action_id, decode_factorized_action_id,
17    encode_factorized_action, ActionIdDesc, ActionParam, ActionParamValue, FactorizedActionDesc,
18};
19pub use action_ids::{ACTION_META_UNUSED, ACTION_META_WIDTH};
20pub use constants::*;
21pub use mask::{build_action_mask, fill_action_mask, fill_action_mask_sparse};
22pub use observation::encode_observation;
23pub use spec::{
24    action_spec, action_spec_json, observation_spec, observation_spec_json,
25    ActionFactorizationSpec, ActionFamilySpec, ActionSpec, ObsFieldSpec, ObsSliceSpec,
26    ObservationSpec, PlayerBlockSpec,
27};
28
29pub(crate) use observation::{
30    encode_obs_context, encode_obs_header, encode_obs_player_block_into, encode_obs_reason,
31    encode_obs_reveal, encode_observation_with_slot_power,
32};
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37    use crate::ActionDesc;
38
39    const OBS_SPEC_HASH: u64 = 3922564485128559020;
40    const ACTION_SPEC_HASH: u64 = 11305511342814019290;
41
42    #[test]
43    fn observation_spec_json_snapshot_hash() {
44        let json = observation_spec_json();
45        let hash = crate::fingerprint::hash_bytes(json.as_bytes());
46        assert_eq!(hash, OBS_SPEC_HASH, "obs spec JSON hash changed");
47    }
48
49    #[test]
50    fn action_spec_json_snapshot_hash() {
51        let json = action_spec_json();
52        let hash = crate::fingerprint::hash_bytes(json.as_bytes());
53        assert_eq!(hash, ACTION_SPEC_HASH, "action spec JSON hash changed");
54    }
55
56    #[test]
57    fn action_spec_factorization_schema_smoke_test() {
58        let spec = action_spec();
59        assert_eq!(spec.factorization.meta_version, "action_meta_v1");
60        assert_eq!(
61            spec.factorization.meta_fields,
62            vec!["family_id", "arg0", "arg1", "arg2"]
63        );
64        assert_eq!(spec.factorization.families.len(), spec.families.len());
65        assert_eq!(spec.factorization.families[0].name, "mulligan_confirm");
66    }
67
68    fn param(name: &'static str, value: ActionParamValue) -> ActionParam {
69        ActionParam { name, value }
70    }
71
72    #[test]
73    fn factorized_action_id_roundtrip_samples() {
74        let samples = vec![
75            (
76                FactorizedActionDesc {
77                    family: "mulligan_confirm",
78                    arg0: None,
79                    arg1: None,
80                    arg2: None,
81                },
82                MULLIGAN_CONFIRM_ID,
83                ActionDesc::MulliganConfirm,
84            ),
85            (
86                FactorizedActionDesc {
87                    family: "mulligan_select",
88                    arg0: Some(2),
89                    arg1: None,
90                    arg2: None,
91                },
92                MULLIGAN_SELECT_BASE + 2,
93                ActionDesc::MulliganSelect { hand_index: 2 },
94            ),
95            (
96                FactorizedActionDesc {
97                    family: "main_play_character",
98                    arg0: Some(1),
99                    arg1: Some(2),
100                    arg2: None,
101                },
102                MAIN_PLAY_CHAR_BASE + MAX_STAGE + 2,
103                ActionDesc::MainPlayCharacter {
104                    hand_index: 1,
105                    stage_slot: 2,
106                },
107            ),
108            (
109                FactorizedActionDesc {
110                    family: "main_move",
111                    arg0: Some(0),
112                    arg1: Some(1),
113                    arg2: None,
114                },
115                MAIN_MOVE_BASE,
116                ActionDesc::MainMove {
117                    from_slot: 0,
118                    to_slot: 1,
119                },
120            ),
121            (
122                FactorizedActionDesc {
123                    family: "attack",
124                    arg0: Some(1),
125                    arg1: Some(1),
126                    arg2: None,
127                },
128                ATTACK_BASE + 4,
129                ActionDesc::Attack {
130                    slot: 1,
131                    attack_type: crate::state::AttackType::Side,
132                },
133            ),
134            (
135                FactorizedActionDesc {
136                    family: "choice_select",
137                    arg0: Some(3),
138                    arg1: None,
139                    arg2: None,
140                },
141                CHOICE_BASE + 3,
142                ActionDesc::ChoiceSelect { index: 3 },
143            ),
144            (
145                FactorizedActionDesc {
146                    family: "concede",
147                    arg0: None,
148                    arg1: None,
149                    arg2: None,
150                },
151                CONCEDE_ID,
152                ActionDesc::Concede,
153            ),
154        ];
155
156        for (factorized, expected_id, action) in samples {
157            let id = encode_factorized_action(&factorized).expect("factorized id");
158            assert_eq!(id, expected_id);
159            let decoded = decode_factorized_action_id(id).expect("factorized decode");
160            assert_eq!(decoded, factorized);
161            assert_eq!(encode_factorized_action(&decoded), Some(id));
162            assert_eq!(action_id_for(&action), Some(id));
163        }
164    }
165
166    #[test]
167    fn factorized_action_rejects_out_of_range_params() {
168        assert_eq!(
169            encode_factorized_action(&FactorizedActionDesc {
170                family: "mulligan_select",
171                arg0: Some(258),
172                arg1: None,
173                arg2: None,
174            }),
175            None
176        );
177        assert_eq!(
178            encode_factorized_action(&FactorizedActionDesc {
179                family: "attack",
180                arg0: Some(1),
181                arg1: Some(9),
182                arg2: None,
183            }),
184            None
185        );
186    }
187
188    #[test]
189    fn action_id_decode_roundtrip_samples() {
190        let samples = vec![
191            (
192                ActionDesc::MulliganConfirm,
193                ActionIdDesc {
194                    family: "mulligan_confirm",
195                    params: vec![],
196                },
197            ),
198            (
199                ActionDesc::MulliganSelect { hand_index: 2 },
200                ActionIdDesc {
201                    family: "mulligan_select",
202                    params: vec![param("hand_index", ActionParamValue::Int(2))],
203                },
204            ),
205            (
206                ActionDesc::Pass,
207                ActionIdDesc {
208                    family: "pass",
209                    params: vec![],
210                },
211            ),
212            (
213                ActionDesc::Clock { hand_index: 3 },
214                ActionIdDesc {
215                    family: "clock_from_hand",
216                    params: vec![param("hand_index", ActionParamValue::Int(3))],
217                },
218            ),
219            (
220                ActionDesc::MainPlayCharacter {
221                    hand_index: 1,
222                    stage_slot: 2,
223                },
224                ActionIdDesc {
225                    family: "main_play_character",
226                    params: vec![
227                        param("hand_index", ActionParamValue::Int(1)),
228                        param("stage_slot", ActionParamValue::Int(2)),
229                    ],
230                },
231            ),
232            (
233                ActionDesc::MainPlayEvent { hand_index: 4 },
234                ActionIdDesc {
235                    family: "main_play_event",
236                    params: vec![param("hand_index", ActionParamValue::Int(4))],
237                },
238            ),
239            (
240                ActionDesc::MainMove {
241                    from_slot: 0,
242                    to_slot: 1,
243                },
244                ActionIdDesc {
245                    family: "main_move",
246                    params: vec![
247                        param("from_slot", ActionParamValue::Int(0)),
248                        param("to_slot", ActionParamValue::Int(1)),
249                    ],
250                },
251            ),
252            (
253                ActionDesc::ClimaxPlay { hand_index: 2 },
254                ActionIdDesc {
255                    family: "climax_play",
256                    params: vec![param("hand_index", ActionParamValue::Int(2))],
257                },
258            ),
259            (
260                ActionDesc::Attack {
261                    slot: 1,
262                    attack_type: crate::state::AttackType::Side,
263                },
264                ActionIdDesc {
265                    family: "attack",
266                    params: vec![
267                        param("slot", ActionParamValue::Int(1)),
268                        param("attack_type", ActionParamValue::Str("side")),
269                    ],
270                },
271            ),
272            (
273                ActionDesc::LevelUp { index: 3 },
274                ActionIdDesc {
275                    family: "level_up",
276                    params: vec![param("index", ActionParamValue::Int(3))],
277                },
278            ),
279            (
280                ActionDesc::EncorePay { slot: 2 },
281                ActionIdDesc {
282                    family: "encore_pay",
283                    params: vec![param("slot", ActionParamValue::Int(2))],
284                },
285            ),
286            (
287                ActionDesc::EncoreDecline { slot: 2 },
288                ActionIdDesc {
289                    family: "encore_decline",
290                    params: vec![param("slot", ActionParamValue::Int(2))],
291                },
292            ),
293            (
294                ActionDesc::TriggerOrder { index: 5 },
295                ActionIdDesc {
296                    family: "trigger_order",
297                    params: vec![param("index", ActionParamValue::Int(5))],
298                },
299            ),
300            (
301                ActionDesc::ChoiceSelect { index: 3 },
302                ActionIdDesc {
303                    family: "choice_select",
304                    params: vec![param("index", ActionParamValue::Int(3))],
305                },
306            ),
307            (
308                ActionDesc::ChoicePrevPage,
309                ActionIdDesc {
310                    family: "choice_prev_page",
311                    params: vec![],
312                },
313            ),
314            (
315                ActionDesc::ChoiceNextPage,
316                ActionIdDesc {
317                    family: "choice_next_page",
318                    params: vec![],
319                },
320            ),
321            (
322                ActionDesc::Concede,
323                ActionIdDesc {
324                    family: "concede",
325                    params: vec![],
326                },
327            ),
328        ];
329
330        for (action, expected) in samples {
331            let id = action_id_for(&action).expect("id");
332            let decoded = decode_action_id(id).expect("decode");
333            assert_eq!(decoded, expected);
334            let back = action_desc_for_id(id).expect("back");
335            assert_eq!(back, action);
336        }
337    }
338}