Skip to main content

weiss_core/legal/
attack.rs

1use crate::config::CurriculumConfig;
2use crate::modifier_queries::collect_attack_slot_state;
3use crate::state::{AttackType, GameState, StageStatus};
4
5use super::helpers::starting_player_first_turn_attack_used;
6use super::types::{ActionDesc, LegalActions};
7use super::MAX_STAGE;
8
9/// Validate whether an attack can be declared from a slot.
10pub fn can_declare_attack(
11    state: &GameState,
12    player: u8,
13    slot: u8,
14    attack_type: AttackType,
15    curriculum: &CurriculumConfig,
16) -> Result<(), &'static str> {
17    let p = player as usize;
18    let s = slot as usize;
19    if s >= MAX_STAGE || (curriculum.reduced_stage_mode && s > 0) {
20        return Err("Attack slot out of range");
21    }
22    if s >= 3 {
23        return Err("Attack must be from center stage");
24    }
25    let attacker_slot = &state.players[p].stage[s];
26    if attacker_slot.card.is_none() {
27        return Err("No attacker in slot");
28    }
29    if attacker_slot.status != StageStatus::Stand {
30        return Err("Attacker is rested");
31    }
32    if attacker_slot.has_attacked {
33        return Err("Attacker already attacked");
34    }
35    if starting_player_first_turn_attack_used(state, player) {
36        return Err("Starting player can only attack once on first turn");
37    }
38    let (cannot_attack, cannot_side_attack, cannot_frontal_attack, attack_cost) =
39        if let Some(derived) = state.turn.derived_attack.as_ref() {
40            let entry = derived.per_player[p][s];
41            (
42                entry.cannot_attack,
43                entry.cannot_side_attack,
44                entry.cannot_frontal_attack,
45                entry.attack_cost,
46            )
47        } else if let Some(card_inst) = attacker_slot.card {
48            collect_attack_slot_state(
49                state,
50                p,
51                s,
52                card_inst.id,
53                attacker_slot.cannot_attack,
54                attacker_slot.attack_cost,
55            )
56        } else {
57            (
58                attacker_slot.cannot_attack,
59                false,
60                false,
61                attacker_slot.attack_cost,
62            )
63        };
64    if cannot_attack {
65        return Err("Attacker cannot attack");
66    }
67    if attack_cost as usize > state.players[p].stock.len() {
68        return Err("Attack cost not payable");
69    }
70    let defender_player = 1 - p;
71    let defender_present = state.players[defender_player].stage[s].card.is_some();
72    match attack_type {
73        AttackType::Frontal | AttackType::Side if !defender_present => {
74            return Err("No defender for frontal/side attack");
75        }
76        AttackType::Frontal if cannot_frontal_attack => {
77            return Err("Attacker cannot frontal attack");
78        }
79        AttackType::Side if cannot_side_attack => {
80            return Err("Attacker cannot side attack");
81        }
82        AttackType::Direct if defender_present => {
83            return Err("Direct attack requires empty opposing slot");
84        }
85        AttackType::Side if !curriculum.enable_side_attacks => {
86            return Err("Side attacks disabled");
87        }
88        AttackType::Direct if !curriculum.enable_direct_attacks => {
89            return Err("Direct attacks disabled");
90        }
91        _ => {}
92    }
93    Ok(())
94}
95
96/// Compute legal attack actions into a reusable buffer.
97#[inline(always)]
98pub fn legal_attack_actions_into(
99    state: &GameState,
100    player: u8,
101    curriculum: &CurriculumConfig,
102    actions: &mut LegalActions,
103) {
104    if starting_player_first_turn_attack_used(state, player) {
105        return;
106    }
107    let max_slot = if curriculum.reduced_stage_mode { 1 } else { 3 };
108    for slot in 0..max_slot {
109        let slot_u8 = slot as u8;
110        for attack_type in [AttackType::Frontal, AttackType::Side, AttackType::Direct] {
111            if can_declare_attack(state, player, slot_u8, attack_type, curriculum).is_ok() {
112                actions.push(ActionDesc::Attack {
113                    slot: slot_u8,
114                    attack_type,
115                });
116            }
117        }
118    }
119}
120
121/// Compute legal attack actions for a player.
122#[inline(always)]
123pub fn legal_attack_actions(
124    state: &GameState,
125    player: u8,
126    curriculum: &CurriculumConfig,
127) -> LegalActions {
128    let mut actions = LegalActions::new();
129    legal_attack_actions_into(state, player, curriculum, &mut actions);
130    actions
131}