weiss_core/env/visibility/
replay.rs

1use super::super::GameEnv;
2use crate::encode::{action_id_for, ACTION_ENCODING_VERSION, OBS_ENCODING_VERSION};
3use crate::events::Event;
4use crate::legal::ActionDesc;
5use crate::replay::{
6    EpisodeBody, EpisodeHeader, ReplayData, ReplayEvent, ReplayFinal, ReplayVisibilityMode,
7    REPLAY_ACTION_ID_UNKNOWN, REPLAY_SCHEMA_VERSION,
8};
9use crate::state::TerminalResult;
10
11impl GameEnv {
12    pub(in crate::env) fn log_event(&mut self, event: Event) {
13        if self.recording {
14            let ctx = self.replay_visibility_context();
15            self.canonical_events.push(event.clone());
16            let replay_event = self.sanitize_event_for_viewer(&event, ctx);
17            self.replay_events.push(replay_event);
18        }
19        if self.debug_event_ring.is_some() {
20            let mut sanitized = [None, None];
21            for viewer in 0..2u8 {
22                let ctx = self.debug_visibility_context(viewer);
23                sanitized[viewer as usize] = Some(self.sanitize_event_for_viewer(&event, ctx));
24            }
25            if let Some(rings) = self.debug_event_ring.as_mut() {
26                for viewer in 0..2u8 {
27                    if let Some(entry) = sanitized[viewer as usize].take() {
28                        rings[viewer as usize].push(entry);
29                    }
30                }
31            }
32        }
33    }
34
35    pub(in crate::env) fn log_action(&mut self, actor: u8, action: ActionDesc) {
36        if !self.recording || !self.replay_config.store_actions {
37            return;
38        }
39        let ctx = self.replay_visibility_context();
40        self.replay_actions_raw.push(action.clone());
41        let logged = self.sanitize_action_for_viewer(&action, actor, ctx);
42        self.replay_actions.push(logged);
43        let raw_id = action_id_for(&action)
44            .and_then(|id| u16::try_from(id).ok())
45            .unwrap_or(REPLAY_ACTION_ID_UNKNOWN);
46        let public_id = self
47            .replay_actions
48            .last()
49            .and_then(action_id_for)
50            .and_then(|id| u16::try_from(id).ok())
51            .unwrap_or(REPLAY_ACTION_ID_UNKNOWN);
52        self.replay_action_ids_raw.push(raw_id);
53        self.replay_action_ids.push(public_id);
54    }
55
56    /// Finalize and flush replay output for the current episode.
57    pub fn finish_episode_replay(&mut self) {
58        if !self.recording {
59            return;
60        }
61        if self.state.terminal.is_some() {
62            let need_terminal = !self
63                .replay_events
64                .iter()
65                .any(|e| matches!(e, ReplayEvent::Terminal { .. }));
66            if need_terminal {
67                let winner = match self.state.terminal {
68                    Some(TerminalResult::Win { winner }) => Some(winner),
69                    Some(TerminalResult::Draw | TerminalResult::Timeout) => None,
70                    None => None,
71                };
72                self.log_event(Event::Terminal { winner });
73            }
74        }
75        let writer = self.replay_writer.clone();
76        if let Some(writer) = writer {
77            let header = EpisodeHeader {
78                obs_version: OBS_ENCODING_VERSION,
79                action_version: ACTION_ENCODING_VERSION,
80                replay_version: REPLAY_SCHEMA_VERSION,
81                seed: self.episode_seed,
82                base_seed: self.base_seed,
83                episode_seed: self.episode_seed,
84                spec_hash: crate::encode::SPEC_HASH,
85                starting_player: self.state.turn.starting_player,
86                deck_ids: self.config.deck_ids,
87                curriculum_id: "default".to_string(),
88                config_hash: self.config.config_hash(&self.curriculum),
89                fingerprint_algo: crate::fingerprint::FINGERPRINT_ALGO.to_string(),
90                env_id: self.env_id,
91                episode_index: self.episode_index,
92            };
93            let (actions, action_ids, events) = match self.replay_config.visibility_mode {
94                ReplayVisibilityMode::Full => (
95                    if self.replay_config.store_actions {
96                        self.replay_actions_raw.clone()
97                    } else {
98                        Vec::new()
99                    },
100                    if self.replay_config.store_actions {
101                        self.replay_action_ids_raw.clone()
102                    } else {
103                        Vec::new()
104                    },
105                    Some(self.canonical_events.clone()),
106                ),
107                ReplayVisibilityMode::Public => (
108                    if self.replay_config.store_actions {
109                        self.replay_actions.clone()
110                    } else {
111                        Vec::new()
112                    },
113                    if self.replay_config.store_actions {
114                        self.replay_action_ids.clone()
115                    } else {
116                        Vec::new()
117                    },
118                    Some(self.replay_events.clone()),
119                ),
120            };
121            let body = EpisodeBody {
122                actions,
123                action_ids,
124                events,
125                steps: self.replay_steps.clone(),
126                final_state: Some(ReplayFinal {
127                    terminal: self.state.terminal,
128                    state_hash: crate::fingerprint::state_fingerprint(&self.state),
129                    decision_count: self.state.turn.decision_count,
130                    tick_count: self.state.turn.tick_count,
131                }),
132            };
133            if let Err(err) = writer.send(ReplayData { header, body }) {
134                eprintln!("Replay enqueue failed: {err}");
135            }
136        }
137        self.recording = false;
138    }
139}