weiss_core/legal/
attack.rs1use 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
9pub 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#[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#[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}