1use std::collections::HashSet;
2
3use crate::config::CurriculumConfig;
4use crate::db::CardDb;
5use crate::encode::{
6 ATTACK_BASE, CHOICE_BASE, CHOICE_COUNT, CHOICE_NEXT_ID, CHOICE_PREV_ID, CLIMAX_PLAY_BASE,
7 CLOCK_HAND_BASE, CONCEDE_ID, ENCORE_DECLINE_BASE, ENCORE_PAY_BASE, LEVEL_UP_BASE,
8 MAIN_MOVE_BASE, MAIN_PLAY_CHAR_BASE, MAIN_PLAY_EVENT_BASE, MULLIGAN_CONFIRM_ID,
9 MULLIGAN_SELECT_BASE, PASS_ACTION_ID, TRIGGER_ORDER_BASE,
10};
11use crate::state::{AttackType, GameState, StageStatus};
12
13use super::attack::can_declare_attack;
14use super::hand_play_requirements::card_set_allowed;
15use super::helpers::{
16 attack_type_to_index, can_pay_encore_for_slot, for_each_playable_hand_card, is_character_slot,
17 push_id, starting_player_first_turn_attack_used, HandScanMode, PlayableHandCard,
18 StageModifierCache,
19};
20use super::types::{Decision, DecisionKind, LegalActionIds};
21use super::{MAX_HAND, MAX_STAGE};
22
23#[inline(always)]
24fn append_mulligan_action_ids(state: &GameState, player: usize, out: &mut LegalActionIds) {
25 push_id(out, MULLIGAN_CONFIRM_ID);
26 let p = &state.players[player];
27 for (hand_index, _) in p.hand.iter().enumerate() {
28 if hand_index >= MAX_HAND || hand_index > u8::MAX as usize {
29 break;
30 }
31 push_id(out, MULLIGAN_SELECT_BASE + hand_index);
32 }
33}
34
35#[inline(always)]
36fn append_clock_action_ids(
37 state: &GameState,
38 player: usize,
39 db: &CardDb,
40 curriculum: &CurriculumConfig,
41 allowed_card_sets: Option<&HashSet<String>>,
42 out: &mut LegalActionIds,
43) {
44 push_id(out, PASS_ACTION_ID);
45 let p = &state.players[player];
46 for (hand_index, card_inst) in p.hand.iter().enumerate() {
47 if hand_index >= MAX_HAND || hand_index > u8::MAX as usize {
48 break;
49 }
50 if let Some(card) = db.get(card_inst.id) {
51 if !card_set_allowed(card, curriculum, allowed_card_sets) {
52 continue;
53 }
54 push_id(out, CLOCK_HAND_BASE + hand_index);
55 }
56 }
57}
58
59#[inline(always)]
60fn append_main_action_ids(
61 state: &GameState,
62 player: usize,
63 db: &CardDb,
64 curriculum: &CurriculumConfig,
65 allowed_card_sets: Option<&HashSet<String>>,
66 out: &mut LegalActionIds,
67) {
68 let p = &state.players[player];
69 let modifier_cache = StageModifierCache::build(state, player);
70 let max_slot = if curriculum.reduced_stage_mode {
71 1
72 } else {
73 MAX_STAGE
74 };
75 let events_locked = modifier_cache.cannot_play_events_from_hand;
76 push_id(out, PASS_ACTION_ID);
77 for_each_playable_hand_card(
78 p,
79 db,
80 curriculum,
81 allowed_card_sets,
82 HandScanMode::Main,
83 events_locked,
84 |playable| match playable {
85 PlayableHandCard::MainCharacter { hand_index } => {
86 for slot in 0..max_slot {
87 let id = MAIN_PLAY_CHAR_BASE + hand_index * MAX_STAGE + slot;
88 push_id(out, id);
89 }
90 }
91 PlayableHandCard::MainEvent { hand_index } => {
92 push_id(out, MAIN_PLAY_EVENT_BASE + hand_index);
93 }
94 PlayableHandCard::Climax { .. } => {}
95 },
96 );
97 if !state.turn.main_move_used {
98 for from in 0..max_slot {
99 for to in 0..max_slot {
100 if from == to {
101 continue;
102 }
103 let from_slot = &p.stage[from];
104 let to_slot = &p.stage[to];
105 if from_slot.card.is_some()
106 && is_character_slot(from_slot, db)
107 && (to_slot.card.is_none() || is_character_slot(to_slot, db))
108 && !modifier_cache.cannot_move_stage_position[from]
109 && (to_slot.card.is_none() || !modifier_cache.cannot_move_stage_position[to])
110 {
111 let to_index = if to < from { to } else { to - 1 };
112 let id = MAIN_MOVE_BASE + from * (MAX_STAGE - 1) + to_index;
113 push_id(out, id);
114 }
115 }
116 }
117 }
118}
119
120#[inline(always)]
121fn append_climax_action_ids(
122 state: &GameState,
123 player: usize,
124 db: &CardDb,
125 curriculum: &CurriculumConfig,
126 allowed_card_sets: Option<&HashSet<String>>,
127 out: &mut LegalActionIds,
128) {
129 let p = &state.players[player];
130 push_id(out, PASS_ACTION_ID);
131 for_each_playable_hand_card(
132 p,
133 db,
134 curriculum,
135 allowed_card_sets,
136 HandScanMode::Climax,
137 false,
138 |playable| {
139 if let PlayableHandCard::Climax { hand_index } = playable {
140 push_id(out, CLIMAX_PLAY_BASE + hand_index);
141 }
142 },
143 );
144}
145
146#[inline(always)]
147fn append_attack_declaration_action_ids(
148 state: &GameState,
149 player: u8,
150 curriculum: &CurriculumConfig,
151 out: &mut LegalActionIds,
152) {
153 push_id(out, PASS_ACTION_ID);
154 if starting_player_first_turn_attack_used(state, player) {
155 return;
156 }
157 let max_slot = if curriculum.reduced_stage_mode { 1 } else { 3 };
158 for slot in 0..max_slot {
159 let slot_u8 = slot as u8;
160 for attack_type in [AttackType::Frontal, AttackType::Side, AttackType::Direct] {
161 if can_declare_attack(state, player, slot_u8, attack_type, curriculum).is_ok() {
162 let id = ATTACK_BASE + slot * 3 + attack_type_to_index(attack_type);
163 push_id(out, id);
164 }
165 }
166 }
167}
168
169#[inline(always)]
170fn append_level_up_action_ids(state: &GameState, player: usize, out: &mut LegalActionIds) {
171 if state.players[player].clock.len() >= 7 {
172 for idx in 0..7 {
173 push_id(out, LEVEL_UP_BASE + idx);
174 }
175 }
176}
177
178#[inline(always)]
179fn append_encore_action_ids(
180 state: &GameState,
181 player: usize,
182 db: &CardDb,
183 curriculum: &CurriculumConfig,
184 out: &mut LegalActionIds,
185) {
186 let p = &state.players[player];
187 let modifier_cache = StageModifierCache::build(state, player);
188 for slot in 0..p.stage.len() {
189 if p.stage[slot].card.is_some()
190 && p.stage[slot].status == StageStatus::Reverse
191 && can_pay_encore_for_slot(state, db, curriculum, player, slot, Some(&modifier_cache))
192 {
193 push_id(out, ENCORE_PAY_BASE + slot);
194 }
195 }
196 for slot in 0..p.stage.len() {
197 if p.stage[slot].card.is_some() && p.stage[slot].status == StageStatus::Reverse {
198 push_id(out, ENCORE_DECLINE_BASE + slot);
199 }
200 }
201}
202
203#[inline(always)]
204fn append_trigger_order_action_ids(state: &GameState, out: &mut LegalActionIds) {
205 let choices = state
206 .turn
207 .trigger_order
208 .as_ref()
209 .map(|o| o.choices.len())
210 .unwrap_or(0);
211 let max = choices.min(10);
212 for idx in 0..max {
213 push_id(out, TRIGGER_ORDER_BASE + idx);
214 }
215}
216
217#[inline(always)]
218fn append_choice_action_ids(state: &GameState, out: &mut LegalActionIds) {
219 if let Some(choice) = state.turn.choice.as_ref() {
220 let total = choice.total_candidates as usize;
221 let page_start = choice.page_start as usize;
222 let safe_start = page_start.min(total);
223 let page_end = total.min(safe_start + CHOICE_COUNT);
224 for idx in 0..(page_end - safe_start) {
225 push_id(out, CHOICE_BASE + idx);
226 }
227 if page_start >= CHOICE_COUNT {
228 push_id(out, CHOICE_PREV_ID);
229 }
230 if page_start + CHOICE_COUNT < total {
231 push_id(out, CHOICE_NEXT_ID);
232 }
233 }
234}
235
236#[inline(always)]
238pub fn legal_action_ids_cached_into(
239 state: &GameState,
240 decision: &Decision,
241 db: &CardDb,
242 curriculum: &CurriculumConfig,
243 allowed_card_sets: Option<&HashSet<String>>,
244 out: &mut LegalActionIds,
245) {
246 let player = decision.player as usize;
250 out.clear();
251 match decision.kind {
252 DecisionKind::Mulligan => append_mulligan_action_ids(state, player, out),
253 DecisionKind::Clock => {
254 append_clock_action_ids(state, player, db, curriculum, allowed_card_sets, out)
255 }
256 DecisionKind::Main => {
257 append_main_action_ids(state, player, db, curriculum, allowed_card_sets, out)
258 }
259 DecisionKind::Climax => {
260 append_climax_action_ids(state, player, db, curriculum, allowed_card_sets, out)
261 }
262 DecisionKind::AttackDeclaration => {
263 append_attack_declaration_action_ids(state, decision.player, curriculum, out)
264 }
265 DecisionKind::LevelUp => append_level_up_action_ids(state, player, out),
266 DecisionKind::Encore => append_encore_action_ids(state, player, db, curriculum, out),
267 DecisionKind::TriggerOrder => append_trigger_order_action_ids(state, out),
268 DecisionKind::Choice => append_choice_action_ids(state, out),
269 }
270 if curriculum.allow_concede {
271 push_id(out, CONCEDE_ID);
272 }
273}