1use std::collections::HashSet;
2
3use crate::config::CurriculumConfig;
4use crate::db::CardDb;
5use crate::encode::CHOICE_COUNT;
6use crate::state::{GameState, StageStatus};
7
8use super::attack::legal_attack_actions_into;
9use super::hand_play_requirements::card_set_allowed;
10use super::helpers::{
11 can_pay_encore_for_slot, for_each_playable_hand_card, is_character_slot, HandScanMode,
12 PlayableHandCard, StageModifierCache,
13};
14use super::types::{ActionDesc, Decision, DecisionKind, LegalActions};
15use super::{MAX_HAND, MAX_STAGE};
16
17#[inline(always)]
18fn append_mulligan_actions(state: &GameState, player: usize, actions: &mut LegalActions) {
19 let p = &state.players[player];
20 actions.push(ActionDesc::MulliganConfirm);
21 for (hand_index, _) in p.hand.iter().enumerate() {
22 if hand_index >= MAX_HAND || hand_index > u8::MAX as usize {
23 break;
24 }
25 actions.push(ActionDesc::MulliganSelect {
26 hand_index: hand_index as u8,
27 });
28 }
29}
30
31#[inline(always)]
32fn append_clock_actions(
33 state: &GameState,
34 player: usize,
35 db: &CardDb,
36 curriculum: &CurriculumConfig,
37 allowed_card_sets: Option<&HashSet<String>>,
38 actions: &mut LegalActions,
39) {
40 actions.push(ActionDesc::Pass);
41 let p = &state.players[player];
42 for (hand_index, card_inst) in p.hand.iter().enumerate() {
43 if hand_index >= MAX_HAND || hand_index > u8::MAX as usize {
44 break;
45 }
46 if let Some(card) = db.get(card_inst.id) {
47 if !card_set_allowed(card, curriculum, allowed_card_sets) {
48 continue;
49 }
50 actions.push(ActionDesc::Clock {
51 hand_index: hand_index as u8,
52 });
53 }
54 }
55}
56
57#[inline(always)]
58fn append_main_actions(
59 state: &GameState,
60 player: usize,
61 db: &CardDb,
62 curriculum: &CurriculumConfig,
63 allowed_card_sets: Option<&HashSet<String>>,
64 actions: &mut LegalActions,
65) {
66 let p = &state.players[player];
67 let modifier_cache = StageModifierCache::build(state, player);
68 let max_slot = if curriculum.reduced_stage_mode {
69 1
70 } else {
71 MAX_STAGE
72 };
73 let events_locked = modifier_cache.cannot_play_events_from_hand;
74 actions.push(ActionDesc::Pass);
75 for_each_playable_hand_card(
76 p,
77 db,
78 curriculum,
79 allowed_card_sets,
80 HandScanMode::Main,
81 events_locked,
82 |playable| match playable {
83 PlayableHandCard::MainCharacter { hand_index } => {
84 for slot in 0..max_slot {
85 actions.push(ActionDesc::MainPlayCharacter {
86 hand_index: hand_index as u8,
87 stage_slot: slot as u8,
88 });
89 }
90 }
91 PlayableHandCard::MainEvent { hand_index } => {
92 actions.push(ActionDesc::MainPlayEvent {
93 hand_index: hand_index as u8,
94 });
95 }
96 PlayableHandCard::Climax { .. } => {}
97 },
98 );
99 if !state.turn.main_move_used {
100 for from in 0..max_slot {
101 for to in 0..max_slot {
102 if from == to {
103 continue;
104 }
105 let from_slot = &p.stage[from];
106 let to_slot = &p.stage[to];
107 if from_slot.card.is_some()
108 && is_character_slot(from_slot, db)
109 && (to_slot.card.is_none() || is_character_slot(to_slot, db))
110 && !modifier_cache.cannot_move_stage_position[from]
111 && (to_slot.card.is_none() || !modifier_cache.cannot_move_stage_position[to])
112 {
113 actions.push(ActionDesc::MainMove {
114 from_slot: from as u8,
115 to_slot: to as u8,
116 });
117 }
118 }
119 }
120 }
121}
122
123#[inline(always)]
124fn append_climax_actions(
125 state: &GameState,
126 player: usize,
127 db: &CardDb,
128 curriculum: &CurriculumConfig,
129 allowed_card_sets: Option<&HashSet<String>>,
130 actions: &mut LegalActions,
131) {
132 let p = &state.players[player];
133 actions.push(ActionDesc::Pass);
134 for_each_playable_hand_card(
135 p,
136 db,
137 curriculum,
138 allowed_card_sets,
139 HandScanMode::Climax,
140 false,
141 |playable| {
142 if let PlayableHandCard::Climax { hand_index } = playable {
143 actions.push(ActionDesc::ClimaxPlay {
144 hand_index: hand_index as u8,
145 });
146 }
147 },
148 );
149}
150
151#[inline(always)]
152fn append_attack_declaration_actions(
153 state: &GameState,
154 player: u8,
155 curriculum: &CurriculumConfig,
156 actions: &mut LegalActions,
157) {
158 actions.push(ActionDesc::Pass);
159 legal_attack_actions_into(state, player, curriculum, actions);
160}
161
162#[inline(always)]
163fn append_level_up_actions(state: &GameState, player: usize, actions: &mut LegalActions) {
164 if state.players[player].clock.len() >= 7 {
165 for idx in 0..7 {
166 actions.push(ActionDesc::LevelUp { index: idx as u8 });
167 }
168 }
169}
170
171#[inline(always)]
172fn append_encore_actions(
173 state: &GameState,
174 player: usize,
175 db: &CardDb,
176 curriculum: &CurriculumConfig,
177 actions: &mut LegalActions,
178) {
179 let p = &state.players[player];
180 let modifier_cache = StageModifierCache::build(state, player);
181 for slot in 0..p.stage.len() {
182 if p.stage[slot].card.is_some()
183 && p.stage[slot].status == StageStatus::Reverse
184 && can_pay_encore_for_slot(state, db, curriculum, player, slot, Some(&modifier_cache))
185 {
186 actions.push(ActionDesc::EncorePay { slot: slot as u8 });
187 }
188 }
189 for slot in 0..p.stage.len() {
190 if p.stage[slot].card.is_some() && p.stage[slot].status == StageStatus::Reverse {
191 actions.push(ActionDesc::EncoreDecline { slot: slot as u8 });
192 }
193 }
194}
195
196#[inline(always)]
197fn append_trigger_order_actions(state: &GameState, actions: &mut LegalActions) {
198 let choices = state
199 .turn
200 .trigger_order
201 .as_ref()
202 .map(|o| o.choices.len())
203 .unwrap_or(0);
204 let max = choices.min(10);
205 for idx in 0..max {
206 actions.push(ActionDesc::TriggerOrder { index: idx as u8 });
207 }
208}
209
210#[inline(always)]
211fn append_choice_actions(state: &GameState, actions: &mut LegalActions) {
212 if let Some(choice) = state.turn.choice.as_ref() {
213 let total = choice.total_candidates as usize;
214 let page_start = choice.page_start as usize;
215 let safe_start = page_start.min(total);
216 let page_end = total.min(safe_start + CHOICE_COUNT);
217 for idx in 0..(page_end - safe_start) {
218 actions.push(ActionDesc::ChoiceSelect { index: idx as u8 });
219 }
220 if page_start >= CHOICE_COUNT {
221 actions.push(ActionDesc::ChoicePrevPage);
222 }
223 if page_start + CHOICE_COUNT < total {
224 actions.push(ActionDesc::ChoiceNextPage);
225 }
226 }
227}
228
229#[inline(always)]
231pub fn legal_actions(
232 state: &GameState,
233 decision: &Decision,
234 db: &CardDb,
235 curriculum: &CurriculumConfig,
236) -> LegalActions {
237 legal_actions_cached(state, decision, db, curriculum, None)
238}
239
240#[inline(always)]
242pub fn legal_actions_cached(
243 state: &GameState,
244 decision: &Decision,
245 db: &CardDb,
246 curriculum: &CurriculumConfig,
247 allowed_card_sets: Option<&HashSet<String>>,
248) -> LegalActions {
249 let mut actions = LegalActions::new();
250 legal_actions_cached_into(
251 state,
252 decision,
253 db,
254 curriculum,
255 allowed_card_sets,
256 &mut actions,
257 );
258 actions
259}
260
261#[inline(always)]
263pub fn legal_actions_cached_into(
264 state: &GameState,
265 decision: &Decision,
266 db: &CardDb,
267 curriculum: &CurriculumConfig,
268 allowed_card_sets: Option<&HashSet<String>>,
269 actions: &mut LegalActions,
270) {
271 let player = decision.player as usize;
275 actions.clear();
276 match decision.kind {
277 DecisionKind::Mulligan => append_mulligan_actions(state, player, actions),
278 DecisionKind::Clock => {
279 append_clock_actions(state, player, db, curriculum, allowed_card_sets, actions)
280 }
281 DecisionKind::Main => {
282 append_main_actions(state, player, db, curriculum, allowed_card_sets, actions)
283 }
284 DecisionKind::Climax => {
285 append_climax_actions(state, player, db, curriculum, allowed_card_sets, actions)
286 }
287 DecisionKind::AttackDeclaration => {
288 append_attack_declaration_actions(state, decision.player, curriculum, actions)
289 }
290 DecisionKind::LevelUp => append_level_up_actions(state, player, actions),
291 DecisionKind::Encore => append_encore_actions(state, player, db, curriculum, actions),
292 DecisionKind::TriggerOrder => append_trigger_order_actions(state, actions),
293 DecisionKind::Choice => append_choice_actions(state, actions),
294 }
295 if curriculum.allow_concede {
296 actions.push(ActionDesc::Concede);
297 }
298}