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