1use crate::config::RewardConfig;
9use crate::db::{
10 AbilitySpec, AbilityTemplate, CardId, CardStatic, CountCmp, CountZone, ZoneCountCondition,
11};
12use crate::state::{ModifierDuration, ModifierKind, TerminalResult};
13
14mod actions;
15mod advance;
16mod cache;
17mod constants;
18mod core;
19mod debug_events;
20mod debug_fingerprints;
21mod debug_validate;
22mod fault;
23pub(crate) mod heuristic_public;
24mod lifecycle;
25mod live_abilities;
26mod obs;
27mod shared;
28mod types;
29
30mod interaction;
31mod modifiers;
32mod movement;
33mod phases;
34mod visibility;
35
36#[cfg(feature = "test-harness")]
37pub mod harness;
39
40pub use actions::legal_action_ids_cached_into;
41pub use core::GameEnv;
42pub use types::{DebugConfig, EngineErrorCode, EnvInfo, FaultRecord, FaultSource, StepOutcome};
43
44pub(crate) use cache::{ActionCache, EnvScratch};
45pub use constants::{CHECK_TIMING_QUIESCENCE_CAP, HAND_LIMIT, STACK_AUTO_RESOLVE_CAP};
46pub(crate) use constants::{
47 MAX_CHOICE_OPTIONS, TRIGGER_EFFECT_BOUNCE, TRIGGER_EFFECT_DRAW, TRIGGER_EFFECT_GATE,
48 TRIGGER_EFFECT_POOL_MOVE, TRIGGER_EFFECT_POOL_STOCK, TRIGGER_EFFECT_SHOT, TRIGGER_EFFECT_SOUL,
49 TRIGGER_EFFECT_STANDBY, TRIGGER_EFFECT_TREASURE_MOVE, TRIGGER_EFFECT_TREASURE_STOCK,
50};
51pub(crate) use types::{
52 DamageIntentLocal, DamageResolveResult, TriggerCompileContext, VisibilityContext,
53};
54
55impl GameEnv {
56 pub fn add_modifier(
58 &mut self,
59 source: CardId,
60 target_player: u8,
61 target_slot: u8,
62 kind: ModifierKind,
63 magnitude: i32,
64 duration: ModifierDuration,
65 ) -> Option<u32> {
66 self.add_modifier_instance(
67 source,
68 None,
69 target_player,
70 target_slot,
71 kind,
72 magnitude,
73 duration,
74 crate::state::ModifierLayer::Effect,
75 )
76 }
77
78 pub(crate) fn mark_rule_actions_dirty(&mut self) {
84 self.rule_actions_dirty = true;
85 }
86
87 pub(crate) fn mark_continuous_modifiers_dirty(&mut self) {
92 self.continuous_modifiers_dirty = true;
93 }
94
95 fn run_rule_actions_if_needed(&mut self) {
96 if self.state.turn.phase != self.last_rule_action_phase {
97 self.rule_actions_dirty = true;
98 self.last_rule_action_phase = self.state.turn.phase;
99 }
100 if !self.rule_actions_dirty {
101 return;
102 }
103 self.rule_actions_dirty = false;
104 self.resolve_rule_actions_until_stable();
105 }
106
107 fn card_set_allowed(&self, card: &CardStatic) -> bool {
108 match (&self.curriculum.allowed_card_sets_cache, &card.card_set) {
109 (None, _) => true,
110 (Some(set), Some(set_id)) => set.contains(set_id),
111 (Some(_), None) => false,
112 }
113 }
114
115 fn ability_conditions_met(
116 &self,
117 controller: u8,
118 conditions: &crate::db::AbilityDefConditions,
119 ) -> bool {
120 if conditions.requires_approx_effects && !self.curriculum.enable_approx_effects {
121 return false;
122 }
123 if let Some(turn) = conditions.turn {
124 let is_self_turn = self.state.turn.active_player == controller;
125 match turn {
126 crate::db::ConditionTurn::SelfTurn if !is_self_turn => return false,
127 crate::db::ConditionTurn::OpponentTurn if is_self_turn => return false,
128 _ => {}
129 }
130 }
131 if let Some(max_memory) = conditions.self_memory_at_most {
132 if self.state.players[controller as usize].memory.len() > max_memory as usize {
133 return false;
134 }
135 }
136 if !conditions.self_memory_card_ids_any.is_empty() {
137 let has_required = self.state.players[controller as usize]
138 .memory
139 .iter()
140 .any(|card_inst| conditions.self_memory_card_ids_any.contains(&card_inst.id));
141 if !has_required {
142 return false;
143 }
144 }
145 if conditions.trigger_check_revealed_climax {
146 let Some(ctx) = self.state.turn.attack.as_ref() else {
147 return false;
148 };
149 if self.state.turn.active_player != controller {
150 return false;
151 }
152 let Some(trigger_card) = ctx.trigger_card else {
153 return false;
154 };
155 let Some(card) = self.db.get(trigger_card) else {
156 return false;
157 };
158 if card.card_type != crate::db::CardType::Climax {
159 return false;
160 }
161 }
162 if let Some(required_icon) = conditions.trigger_check_revealed_icon {
163 let Some(ctx) = self.state.turn.attack.as_ref() else {
164 return false;
165 };
166 if self.state.turn.active_player != controller {
167 return false;
168 }
169 let Some(trigger_card) = ctx.trigger_card else {
170 return false;
171 };
172 let Some(card) = self.db.get(trigger_card) else {
173 return false;
174 };
175 if !card.triggers.contains(&required_icon) {
176 return false;
177 }
178 }
179 if let Some(zone_condition) = conditions.zone_count.as_ref() {
180 if !self.zone_count_condition_met(controller, zone_condition) {
181 return false;
182 }
183 }
184 let Some(climax_condition) = conditions.climax_area.as_ref() else {
185 return true;
186 };
187 let target_player = match climax_condition.side {
188 crate::state::TargetSide::SelfSide => controller,
189 crate::state::TargetSide::Opponent => 1 - controller,
190 } as usize;
191 let climax = self
192 .state
193 .players
194 .get(target_player)
195 .map(|player| &player.climax);
196 let Some(climax) = climax else {
197 return false;
198 };
199 if climax.is_empty() {
200 return false;
201 }
202 if climax_condition.card_ids.is_empty() {
203 return true;
204 }
205 climax
206 .iter()
207 .any(|card_inst| climax_condition.card_ids.contains(&card_inst.id))
208 }
209
210 pub(in crate::env) fn auto_ability_conditions_met(
211 &self,
212 controller: u8,
213 _source_card: CardId,
214 spec: &AbilitySpec,
215 ) -> bool {
216 let AbilityTemplate::AbilityDef(def) = &spec.template else {
217 return true;
218 };
219 self.ability_conditions_met(controller, &def.conditions)
220 }
221
222 pub(crate) fn terminal_reward_for(&self, perspective: u8) -> f32 {
227 let RewardConfig {
228 terminal_win,
229 terminal_loss,
230 terminal_draw,
231 terminal_timeout,
232 ..
233 } = &self.config.reward;
234 match self.state.terminal {
235 Some(TerminalResult::Win { winner }) => {
236 if winner == perspective {
237 *terminal_win
238 } else {
239 *terminal_loss
240 }
241 }
242 Some(TerminalResult::Draw) => *terminal_draw,
243 Some(TerminalResult::Timeout) => *terminal_timeout,
244 None => 0.0,
245 }
246 }
247
248 pub(in crate::env) fn zone_count_value_for_condition(
249 &self,
250 controller: u8,
251 condition: &ZoneCountCondition,
252 ) -> usize {
253 let player = match condition.side {
254 crate::state::TargetSide::SelfSide => controller,
255 crate::state::TargetSide::Opponent => 1 - controller,
256 } as usize;
257 let matches_id = |card_id: CardId| {
258 condition.card_ids.is_empty() || condition.card_ids.contains(&card_id)
259 };
260 match condition.zone {
261 CountZone::Stock => self.state.players[player]
262 .stock
263 .iter()
264 .filter(|card| matches_id(card.id))
265 .count(),
266 CountZone::WaitingRoom => self.state.players[player]
267 .waiting_room
268 .iter()
269 .filter(|card| matches_id(card.id))
270 .count(),
271 CountZone::Hand => self.state.players[player]
272 .hand
273 .iter()
274 .filter(|card| matches_id(card.id))
275 .count(),
276 CountZone::Stage => self.state.players[player]
277 .stage
278 .iter()
279 .filter(|slot| slot.card.map(|card| matches_id(card.id)).unwrap_or(false))
280 .count(),
281 CountZone::BackStage => {
282 if self.curriculum.reduced_stage_mode {
283 0
284 } else {
285 self.state.players[player]
286 .stage
287 .iter()
288 .enumerate()
289 .filter(|(idx, slot)| {
290 *idx >= 3 && slot.card.map(|card| matches_id(card.id)).unwrap_or(false)
291 })
292 .count()
293 }
294 }
295 CountZone::WaitingRoomClimax => self.state.players[player]
296 .waiting_room
297 .iter()
298 .filter(|card| {
299 if !matches_id(card.id) {
300 return false;
301 }
302 self.db
303 .get(card.id)
304 .map(|static_card| static_card.card_type == crate::db::CardType::Climax)
305 .unwrap_or(false)
306 })
307 .count(),
308 CountZone::LevelTotal => self.state.players[player]
309 .level
310 .iter()
311 .filter(|card| matches_id(card.id))
312 .map(|card| {
313 self.db
314 .get(card.id)
315 .map(|static_card| static_card.level as usize)
316 .unwrap_or(0)
317 })
318 .sum(),
319 }
320 }
321
322 pub(in crate::env) fn zone_count_condition_met(
323 &self,
324 controller: u8,
325 condition: &ZoneCountCondition,
326 ) -> bool {
327 let count = self.zone_count_value_for_condition(controller, condition);
328 match condition.cmp {
329 CountCmp::AtLeast => count >= condition.value as usize,
330 CountCmp::AtMost => count <= condition.value as usize,
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests;