1use anyhow::{anyhow, Result};
2
3use crate::db::CardId;
4use crate::legal::DecisionKind;
5use crate::state::{CardInstance, CardInstanceId, Phase};
6
7use super::debug_fingerprints;
8use super::{EngineErrorCode, FaultSource, GameEnv};
9
10impl GameEnv {
11 pub(crate) fn should_validate_state(&self) -> bool {
13 if cfg!(debug_assertions) {
14 return true;
15 }
16 self.validate_state_enabled
17 }
18
19 pub(crate) fn maybe_validate_state(&mut self, context: &str) -> bool {
23 if !self.should_validate_state() {
24 return false;
25 }
26 if let Err(err) = self.validate_state() {
27 eprintln!("State validation failed in {context}: {err}");
28 let actor = self.decision.as_ref().map(|d| d.player);
29 self.latch_fault_deferred(
30 EngineErrorCode::InvariantViolation,
31 actor,
32 FaultSource::Step,
33 );
34 return true;
35 }
36 false
37 }
38
39 pub fn validate_state(&self) -> Result<()> {
43 use std::collections::{HashMap, HashSet};
44 let mut errors = Vec::new();
45
46 let mut counts: [HashMap<CardId, i32>; 2] = [HashMap::new(), HashMap::new()];
47 for (owner, owner_counts) in counts.iter_mut().enumerate() {
48 let deck_list = &self.config.deck_lists[owner];
49 for card in deck_list.iter().copied() {
50 *owner_counts.entry(card).or_insert(0) += 1;
51 }
52 }
53
54 fn consume(
55 counts: &mut [HashMap<CardId, i32>; 2],
56 errors: &mut Vec<String>,
57 owner: u8,
58 card: CardId,
59 zone: &str,
60 ) {
61 let owner_idx = owner as usize;
62 let entry = counts[owner_idx].entry(card).or_insert(0);
63 *entry -= 1;
64 if *entry < 0 {
65 errors.push(format!("Owner {owner} has extra card {card} in {zone}"));
66 }
67 }
68
69 let mut instance_ids: HashSet<CardInstanceId> = HashSet::new();
70 fn check_instance(
71 instance_ids: &mut HashSet<CardInstanceId>,
72 errors: &mut Vec<String>,
73 card: &CardInstance,
74 zone: &str,
75 ) {
76 if card.instance_id == 0 {
77 errors.push(format!("Card instance id 0 in {zone}"));
78 return;
79 }
80 if !instance_ids.insert(card.instance_id) {
81 errors.push(format!(
82 "Duplicate instance id {} in {zone}",
83 card.instance_id
84 ));
85 }
86 }
87
88 for zone_player in 0..2 {
89 let p = &self.state.players[zone_player];
90 for card in &p.deck {
91 consume(
92 &mut counts,
93 &mut errors,
94 card.owner,
95 card.id,
96 &format!("p{zone_player} deck"),
97 );
98 check_instance(
99 &mut instance_ids,
100 &mut errors,
101 card,
102 &format!("p{zone_player} deck"),
103 );
104 }
105 for card in &p.hand {
106 consume(
107 &mut counts,
108 &mut errors,
109 card.owner,
110 card.id,
111 &format!("p{zone_player} hand"),
112 );
113 check_instance(
114 &mut instance_ids,
115 &mut errors,
116 card,
117 &format!("p{zone_player} hand"),
118 );
119 }
120 for card in &p.waiting_room {
121 consume(
122 &mut counts,
123 &mut errors,
124 card.owner,
125 card.id,
126 &format!("p{zone_player} waiting_room"),
127 );
128 check_instance(
129 &mut instance_ids,
130 &mut errors,
131 card,
132 &format!("p{zone_player} waiting_room"),
133 );
134 }
135 for card in &p.clock {
136 consume(
137 &mut counts,
138 &mut errors,
139 card.owner,
140 card.id,
141 &format!("p{zone_player} clock"),
142 );
143 check_instance(
144 &mut instance_ids,
145 &mut errors,
146 card,
147 &format!("p{zone_player} clock"),
148 );
149 }
150 for card in &p.level {
151 consume(
152 &mut counts,
153 &mut errors,
154 card.owner,
155 card.id,
156 &format!("p{zone_player} level"),
157 );
158 check_instance(
159 &mut instance_ids,
160 &mut errors,
161 card,
162 &format!("p{zone_player} level"),
163 );
164 }
165 for card in &p.stock {
166 consume(
167 &mut counts,
168 &mut errors,
169 card.owner,
170 card.id,
171 &format!("p{zone_player} stock"),
172 );
173 check_instance(
174 &mut instance_ids,
175 &mut errors,
176 card,
177 &format!("p{zone_player} stock"),
178 );
179 }
180 for card in &p.memory {
181 consume(
182 &mut counts,
183 &mut errors,
184 card.owner,
185 card.id,
186 &format!("p{zone_player} memory"),
187 );
188 check_instance(
189 &mut instance_ids,
190 &mut errors,
191 card,
192 &format!("p{zone_player} memory"),
193 );
194 }
195 for card in &p.climax {
196 consume(
197 &mut counts,
198 &mut errors,
199 card.owner,
200 card.id,
201 &format!("p{zone_player} climax"),
202 );
203 check_instance(
204 &mut instance_ids,
205 &mut errors,
206 card,
207 &format!("p{zone_player} climax"),
208 );
209 }
210 for card in &p.resolution {
211 consume(
212 &mut counts,
213 &mut errors,
214 card.owner,
215 card.id,
216 &format!("p{zone_player} resolution"),
217 );
218 check_instance(
219 &mut instance_ids,
220 &mut errors,
221 card,
222 &format!("p{zone_player} resolution"),
223 );
224 }
225 for (slot_idx, slot) in p.stage.iter().enumerate() {
226 if let Some(card) = slot.card {
227 consume(
228 &mut counts,
229 &mut errors,
230 card.owner,
231 card.id,
232 &format!("p{zone_player} stage[{slot_idx}]"),
233 );
234 check_instance(
235 &mut instance_ids,
236 &mut errors,
237 &card,
238 &format!("p{zone_player} stage[{slot_idx}]"),
239 );
240 }
241 for marker in &slot.markers {
242 consume(
243 &mut counts,
244 &mut errors,
245 marker.owner,
246 marker.id,
247 &format!("p{zone_player} stage[{slot_idx}] marker"),
248 );
249 check_instance(
250 &mut instance_ids,
251 &mut errors,
252 marker,
253 &format!("p{zone_player} stage[{slot_idx}] marker"),
254 );
255 }
256 }
257 }
258
259 for (owner, owner_counts) in counts.iter().enumerate() {
260 for (card, remaining) in owner_counts.iter() {
261 if *remaining != 0 {
262 errors.push(format!(
263 "Owner {owner} card {card} count mismatch ({remaining})"
264 ));
265 }
266 }
267 }
268
269 if let Some(decision) = &self.decision {
270 if let Some(slot) = decision.focus_slot {
271 if slot as usize >= self.state.players[decision.player as usize].stage.len() {
272 errors.push("Decision focus slot out of range".to_string());
273 }
274 }
275 match decision.kind {
276 DecisionKind::AttackDeclaration => {
277 if self.state.turn.attack.is_some() {
278 errors.push("Attack declaration while attack context active".to_string());
279 }
280 }
281 DecisionKind::LevelUp => {
282 if self.state.turn.pending_level_up.is_none() {
283 errors.push("Level up decision without pending level".to_string());
284 }
285 }
286 DecisionKind::Encore => {
287 let has = self
288 .state
289 .turn
290 .encore_queue
291 .iter()
292 .any(|r| r.player == decision.player);
293 if !has {
294 errors.push("Encore decision without reversed options".to_string());
295 }
296 }
297 DecisionKind::TriggerOrder => {
298 if self.state.turn.trigger_order.is_none() {
299 errors.push("Trigger order decision without pending order".to_string());
300 }
301 }
302 DecisionKind::Choice => {
303 if let Some(choice) = &self.state.turn.choice {
304 if choice.player != decision.player {
305 errors.push("Choice decision player mismatch".to_string());
306 }
307 } else {
308 errors.push("Choice decision without pending choice".to_string());
309 }
310 }
311 _ => {}
312 }
313 }
314
315 if self.state.turn.attack.is_some() && self.state.turn.phase != Phase::Attack {
316 errors.push("Attack context outside Attack phase".to_string());
317 }
318
319 if errors.is_empty() {
320 return Ok(());
321 }
322
323 let state_hash = debug_fingerprints::state_fingerprint(&self.state);
324 let phase = self.state.turn.phase;
325 let attack_step = self.state.turn.attack.as_ref().map(|c| c.step);
326 let tail_len = 8usize;
327 let actions_tail: Vec<String> = self
328 .replay_actions
329 .iter()
330 .rev()
331 .take(tail_len)
332 .rev()
333 .map(|a| format!("{a:?}"))
334 .collect();
335 let decisions_tail: Vec<String> = self
336 .replay_steps
337 .iter()
338 .rev()
339 .take(tail_len)
340 .rev()
341 .map(|s| format!("{:?}/{:?}", s.decision_kind, s.actor))
342 .collect();
343 let fallback_action = self
344 .last_action_desc
345 .as_ref()
346 .map(|a| format!("{a:?}"))
347 .unwrap_or_else(|| "None".to_string());
348 let payload = format!(
349 "seed={}\nphase={:?}\nattack_step={:?}\nlast_action={}\nactions_tail={:?}\ndecisions_tail={:?}\nstate_hash={}",
350 self.episode_seed,
351 phase,
352 attack_step,
353 fallback_action,
354 actions_tail,
355 decisions_tail,
356 state_hash,
357 );
358 Err(anyhow!("{}\n{}", payload, errors.join("; ")))
359 }
360}