1mod 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}