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