weiss_core/
fingerprint.rs

1use serde::Serialize;
2
3use crate::config::{
4    CurriculumConfig, EndConditionPolicy, EnvConfig, ErrorPolicy, ObservationVisibility,
5    RewardConfig,
6};
7use crate::db::CardId;
8use crate::effects::ReplacementSpec;
9use crate::events::Event;
10use crate::state::{
11    AttackContext, ChoiceState, GameState, PendingTrigger, PlayerState, PriorityState, StackItem,
12    StackOrderState, TargetSelectionState, TerminalResult, TimingWindow, TurnState,
13};
14
15pub const FINGERPRINT_ALGO: &str = "postcard+blake3+u64le v1";
16
17pub fn hash_bytes(bytes: &[u8]) -> u64 {
18    let hash = blake3::hash(bytes);
19    let mut out = [0u8; 8];
20    out.copy_from_slice(&hash.as_bytes()[..8]);
21    u64::from_le_bytes(out)
22}
23
24pub fn hash_postcard<T: Serialize + ?Sized>(value: &T) -> u64 {
25    let bytes = postcard::to_allocvec(value).expect("fingerprint serialization failed");
26    hash_bytes(&bytes)
27}
28
29pub fn config_fingerprint(config: &EnvConfig, curriculum: &CurriculumConfig) -> u64 {
30    let canonical = CanonicalConfigForHash::from_config(config, curriculum);
31    hash_postcard(&canonical)
32}
33
34pub fn state_fingerprint(state: &GameState) -> u64 {
35    let canonical = CanonicalStateForHash::from_state(state);
36    hash_postcard(&canonical)
37}
38
39pub fn events_fingerprint(events: &[Event]) -> u64 {
40    hash_postcard(events)
41}
42
43#[derive(Clone, Debug, Serialize)]
44struct CanonicalConfigForHash {
45    env: CanonicalEnvConfig,
46    curriculum: CanonicalCurriculumConfig,
47}
48
49impl CanonicalConfigForHash {
50    fn from_config(config: &EnvConfig, curriculum: &CurriculumConfig) -> Self {
51        Self {
52            env: CanonicalEnvConfig::from_config(config),
53            curriculum: CanonicalCurriculumConfig::from_curriculum(curriculum),
54        }
55    }
56}
57
58#[derive(Clone, Debug, Serialize)]
59struct CanonicalEnvConfig {
60    deck_lists: [Vec<CardId>; 2],
61    deck_ids: [u32; 2],
62    max_decisions: u32,
63    max_ticks: u32,
64    reward: RewardConfig,
65    error_policy: ErrorPolicy,
66    observation_visibility: ObservationVisibility,
67    end_condition_policy: EndConditionPolicy,
68}
69
70impl CanonicalEnvConfig {
71    fn from_config(config: &EnvConfig) -> Self {
72        Self {
73            deck_lists: config.deck_lists.clone(),
74            deck_ids: config.deck_ids,
75            max_decisions: config.max_decisions,
76            max_ticks: config.max_ticks,
77            reward: config.reward.clone(),
78            error_policy: config.error_policy,
79            observation_visibility: config.observation_visibility,
80            end_condition_policy: config.end_condition_policy.clone(),
81        }
82    }
83}
84
85#[derive(Clone, Debug, Serialize)]
86struct CanonicalCurriculumConfig {
87    allowed_card_sets: Vec<String>,
88    allow_character: bool,
89    allow_event: bool,
90    allow_climax: bool,
91    enable_clock_phase: bool,
92    enable_climax_phase: bool,
93    enable_side_attacks: bool,
94    enable_direct_attacks: bool,
95    enable_counters: bool,
96    enable_triggers: bool,
97    enable_trigger_soul: bool,
98    enable_trigger_draw: bool,
99    enable_trigger_shot: bool,
100    enable_trigger_bounce: bool,
101    enable_trigger_treasure: bool,
102    enable_trigger_gate: bool,
103    enable_trigger_standby: bool,
104    enable_backup: bool,
105    enable_encore: bool,
106    enable_refresh_penalty: bool,
107    enable_level_up_choice: bool,
108    enable_activated_abilities: bool,
109    enable_continuous_modifiers: bool,
110    enable_priority_windows: bool,
111    enable_visibility_policies: bool,
112    use_alternate_end_conditions: bool,
113    priority_autopick_single_action: bool,
114    priority_allow_pass: bool,
115    strict_priority_mode: bool,
116    reduced_stage_mode: bool,
117    enforce_color_requirement: bool,
118    enforce_cost_requirement: bool,
119    allow_concede: bool,
120    memory_is_public: bool,
121}
122
123impl CanonicalCurriculumConfig {
124    fn from_curriculum(curriculum: &CurriculumConfig) -> Self {
125        let mut allowed_card_sets = curriculum.allowed_card_sets.clone();
126        allowed_card_sets.sort();
127        allowed_card_sets.dedup();
128        Self {
129            allowed_card_sets,
130            allow_character: curriculum.allow_character,
131            allow_event: curriculum.allow_event,
132            allow_climax: curriculum.allow_climax,
133            enable_clock_phase: curriculum.enable_clock_phase,
134            enable_climax_phase: curriculum.enable_climax_phase,
135            enable_side_attacks: curriculum.enable_side_attacks,
136            enable_direct_attacks: curriculum.enable_direct_attacks,
137            enable_counters: curriculum.enable_counters,
138            enable_triggers: curriculum.enable_triggers,
139            enable_trigger_soul: curriculum.enable_trigger_soul,
140            enable_trigger_draw: curriculum.enable_trigger_draw,
141            enable_trigger_shot: curriculum.enable_trigger_shot,
142            enable_trigger_bounce: curriculum.enable_trigger_bounce,
143            enable_trigger_treasure: curriculum.enable_trigger_treasure,
144            enable_trigger_gate: curriculum.enable_trigger_gate,
145            enable_trigger_standby: curriculum.enable_trigger_standby,
146            enable_backup: curriculum.enable_backup,
147            enable_encore: curriculum.enable_encore,
148            enable_refresh_penalty: curriculum.enable_refresh_penalty,
149            enable_level_up_choice: curriculum.enable_level_up_choice,
150            enable_activated_abilities: curriculum.enable_activated_abilities,
151            enable_continuous_modifiers: curriculum.enable_continuous_modifiers,
152            enable_priority_windows: curriculum.enable_priority_windows,
153            enable_visibility_policies: curriculum.enable_visibility_policies,
154            use_alternate_end_conditions: curriculum.use_alternate_end_conditions,
155            priority_autopick_single_action: curriculum.priority_autopick_single_action,
156            priority_allow_pass: curriculum.priority_allow_pass,
157            strict_priority_mode: curriculum.strict_priority_mode,
158            reduced_stage_mode: curriculum.reduced_stage_mode,
159            enforce_color_requirement: curriculum.enforce_color_requirement,
160            enforce_cost_requirement: curriculum.enforce_cost_requirement,
161            allow_concede: curriculum.allow_concede,
162            memory_is_public: curriculum.memory_is_public,
163        }
164    }
165}
166
167#[derive(Clone, Debug, Serialize)]
168struct CanonicalStateForHash {
169    players: [CanonicalPlayerState; 2],
170    reveal_history: [crate::state::RevealHistory; 2],
171    turn: CanonicalTurnState,
172    rng_state: u64,
173    modifiers: Vec<crate::state::ModifierInstance>,
174    next_modifier_id: u32,
175    replacements: Vec<ReplacementSpec>,
176    next_replacement_insertion: u32,
177    terminal: Option<TerminalResult>,
178}
179
180impl CanonicalStateForHash {
181    fn from_state(state: &GameState) -> Self {
182        Self {
183            players: [
184                CanonicalPlayerState::from_player(&state.players[0]),
185                CanonicalPlayerState::from_player(&state.players[1]),
186            ],
187            reveal_history: state.reveal_history.clone(),
188            turn: CanonicalTurnState::from_turn(&state.turn),
189            rng_state: state.rng.state(),
190            modifiers: state.modifiers.clone(),
191            next_modifier_id: state.next_modifier_id,
192            replacements: state.replacements.clone(),
193            next_replacement_insertion: state.next_replacement_insertion,
194            terminal: state.terminal,
195        }
196    }
197}
198
199#[derive(Clone, Debug, Serialize)]
200struct CanonicalPlayerState {
201    deck: Vec<crate::state::CardInstance>,
202    hand: Vec<crate::state::CardInstance>,
203    waiting_room: Vec<crate::state::CardInstance>,
204    clock: Vec<crate::state::CardInstance>,
205    level: Vec<crate::state::CardInstance>,
206    stock: Vec<crate::state::CardInstance>,
207    memory: Vec<crate::state::CardInstance>,
208    climax: Vec<crate::state::CardInstance>,
209    resolution: Vec<crate::state::CardInstance>,
210    stage: [crate::state::StageSlot; 5],
211}
212
213impl CanonicalPlayerState {
214    fn from_player(player: &PlayerState) -> Self {
215        Self {
216            deck: player.deck.clone(),
217            hand: player.hand.clone(),
218            waiting_room: player.waiting_room.clone(),
219            clock: player.clock.clone(),
220            level: player.level.clone(),
221            stock: player.stock.clone(),
222            memory: player.memory.clone(),
223            climax: player.climax.clone(),
224            resolution: player.resolution.clone(),
225            stage: player.stage.clone(),
226        }
227    }
228}
229
230#[derive(Clone, Debug, Serialize)]
231struct CanonicalTurnState {
232    active_player: u8,
233    starting_player: u8,
234    turn_number: u32,
235    phase: crate::state::Phase,
236    mulligan_done: [bool; 2],
237    mulligan_selected: [u64; 2],
238    main_passed: bool,
239    decision_count: u32,
240    tick_count: u32,
241    attack: Option<AttackContext>,
242    attack_subphase_count: u8,
243    pending_level_up: Option<u8>,
244    encore_queue: Vec<crate::state::EncoreRequest>,
245    encore_step_player: Option<u8>,
246    pending_triggers: Vec<PendingTrigger>,
247    trigger_order: Option<crate::state::TriggerOrderState>,
248    choice: Option<ChoiceState>,
249    target_selection: Option<TargetSelectionState>,
250    pending_cost: Option<crate::state::CostPaymentState>,
251    priority: Option<PriorityState>,
252    stack: Vec<StackItem>,
253    pending_stack_groups: Vec<StackOrderState>,
254    stack_order: Option<StackOrderState>,
255    next_trigger_id: u32,
256    next_trigger_group_id: u32,
257    next_choice_id: u32,
258    next_stack_group_id: u32,
259    next_damage_event_id: u32,
260    next_effect_instance_id: u32,
261    active_window: Option<TimingWindow>,
262    end_phase_window_done: bool,
263    end_phase_discard_done: bool,
264    end_phase_climax_done: bool,
265    end_phase_cleanup_done: bool,
266    encore_window_done: bool,
267    pending_losses: [bool; 2],
268    damage_resolution_target: Option<u8>,
269    cost_payment_depth: u8,
270    pending_resolution_cleanup: Vec<(u8, crate::state::CardInstanceId)>,
271    phase_step: u8,
272    attack_phase_begin_done: bool,
273    attack_decl_check_done: bool,
274    encore_begin_done: bool,
275    end_phase_pending: bool,
276}
277
278impl CanonicalTurnState {
279    fn from_turn(turn: &TurnState) -> Self {
280        Self {
281            active_player: turn.active_player,
282            starting_player: turn.starting_player,
283            turn_number: turn.turn_number,
284            phase: turn.phase,
285            mulligan_done: turn.mulligan_done,
286            mulligan_selected: turn.mulligan_selected,
287            main_passed: turn.main_passed,
288            decision_count: turn.decision_count,
289            tick_count: turn.tick_count,
290            attack: turn.attack.clone(),
291            attack_subphase_count: turn.attack_subphase_count,
292            pending_level_up: turn.pending_level_up,
293            encore_queue: turn.encore_queue.clone(),
294            encore_step_player: turn.encore_step_player,
295            pending_triggers: turn.pending_triggers.clone(),
296            trigger_order: turn.trigger_order.clone(),
297            choice: turn.choice.clone(),
298            target_selection: turn.target_selection.clone(),
299            pending_cost: turn.pending_cost.clone(),
300            priority: turn.priority.clone(),
301            stack: turn.stack.clone(),
302            pending_stack_groups: turn.pending_stack_groups.iter().cloned().collect(),
303            stack_order: turn.stack_order.clone(),
304            next_trigger_id: turn.next_trigger_id,
305            next_trigger_group_id: turn.next_trigger_group_id,
306            next_choice_id: turn.next_choice_id,
307            next_stack_group_id: turn.next_stack_group_id,
308            next_damage_event_id: turn.next_damage_event_id,
309            next_effect_instance_id: turn.next_effect_instance_id,
310            active_window: turn.active_window,
311            end_phase_window_done: turn.end_phase_window_done,
312            end_phase_discard_done: turn.end_phase_discard_done,
313            end_phase_climax_done: turn.end_phase_climax_done,
314            end_phase_cleanup_done: turn.end_phase_cleanup_done,
315            encore_window_done: turn.encore_window_done,
316            pending_losses: turn.pending_losses,
317            damage_resolution_target: turn.damage_resolution_target,
318            cost_payment_depth: turn.cost_payment_depth,
319            pending_resolution_cleanup: turn.pending_resolution_cleanup.clone(),
320            phase_step: turn.phase_step,
321            attack_phase_begin_done: turn.attack_phase_begin_done,
322            attack_decl_check_done: turn.attack_decl_check_done,
323            encore_begin_done: turn.encore_begin_done,
324            end_phase_pending: turn.end_phase_pending,
325        }
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn no_hashmap_leakage_regression() {
335        let config_name = std::any::type_name::<CanonicalConfigForHash>();
336        let state_name = std::any::type_name::<CanonicalStateForHash>();
337        assert!(!config_name.contains("HashMap"));
338        assert!(!config_name.contains("HashSet"));
339        assert!(!state_name.contains("HashMap"));
340        assert!(!state_name.contains("HashSet"));
341    }
342}