#![allow(dead_code)] use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_json::json; use crate::core::enums::*; use crate::core::mcts::{MCTS, SearchHorizon, EvalMode}; use crate::core::heuristics::{Heuristic, OriginalHeuristic, SimpleHeuristic}; use std::collections::HashMap; use rand::seq::SliceRandom; use rand::prelude::IndexedRandom; use rand_pcg::Pcg64; use rand::SeedableRng; use rand::rngs::SmallRng; use smallvec::SmallVec; // --- OPCODES --- pub const O_RETURN: i32 = 1; pub const O_JUMP: i32 = 2; pub const O_JUMP_F: i32 = 3; // State Modification (10-99) pub const O_DRAW: i32 = 10; pub const O_BLADES: i32 = 11; pub const O_HEARTS: i32 = 12; pub const O_REDUCE_COST: i32 = 13; pub const O_LOOK_DECK: i32 = 14; pub const O_RECOV_L: i32 = 15; pub const O_BOOST: i32 = 16; pub const O_RECOV_M: i32 = 17; pub const O_BUFF: i32 = 18; pub const O_IMMUNITY: i32 = 19; pub const O_MOVE_MEMBER: i32 = 20; pub const O_SWAP_CARDS: i32 = 21; pub const O_SEARCH_DECK: i32 = 22; pub const O_CHARGE: i32 = 23; pub const O_SET_BLADES: i32 = 24; pub const O_SET_HEARTS: i32 = 25; pub const O_FORMATION: i32 = 26; pub const O_NEGATE: i32 = 27; pub const O_ORDER_DECK: i32 = 28; pub const O_META_RULE: i32 = 29; pub const O_SELECT_MODE: i32 = 30; pub const O_MOVE_TO_DECK: i32 = 31; pub const O_TAP_O: i32 = 32; pub const O_PLACE_UNDER: i32 = 33; pub const O_FLAVOR: i32 = 34; pub const O_RESTRICTION: i32 = 35; pub const O_BATON_MOD: i32 = 36; pub const O_SET_SCORE: i32 = 37; pub const O_SWAP_ZONE: i32 = 38; pub const O_TRANSFORM_COLOR: i32 = 39; pub const O_REVEAL: i32 = 40; pub const O_LOOK_AND_CHOOSE: i32 = 41; pub const O_CHEER_REVEAL: i32 = 42; pub const O_ACTIVATE_MEMBER: i32 = 43; pub const O_ADD_H: i32 = 44; pub const O_COLOR_SELECT: i32 = 45; pub const O_REPLACE_EFFECT: i32 = 46; pub const O_TRIGGER_REMOTE: i32 = 47; pub const O_REDUCE_HEART_REQ: i32 = 48; pub const O_MODIFY_SCORE_RULE: i32 = 49; pub const O_ADD_STAGE_ENERGY: i32 = 50; pub const O_SET_TAPPED: i32 = 51; pub const O_ACTIVATE_ENERGY: i32 = 81; pub const O_ADD_CONTINUOUS: i32 = 52; pub const O_TAP_M: i32 = 53; pub const O_PLAY_MEMBER_FROM_HAND: i32 = 57; pub const O_MOVE_TO_DISCARD: i32 = 58; pub const O_GRANT_ABILITY: i32 = 60; pub const O_INCREASE_COST: i32 = 70; pub const O_PAY_ENERGY: i32 = 64; pub const O_DRAW_UNTIL: i32 = 66; pub const O_REVEAL_UNTIL: i32 = 69; pub const O_PLAY_MEMBER_FROM_DISCARD: i32 = 63; pub const O_SWAP_AREA: i32 = 72; pub const O_PLAY_LIVE_FROM_DISCARD: i32 = 76; pub const O_SELECT_CARDS: i32 = 74; pub const O_OPPONENT_CHOOSE: i32 = 75; // Conditions (200-299) pub const C_TR1: i32 = 200; pub const C_HAS_MEMBER: i32 = 201; pub const C_CLR: i32 = 202; pub const C_STG: i32 = 203; pub const C_HND: i32 = 204; pub const C_DSR: i32 = 205; pub const C_CTR: i32 = 206; pub const C_LLD: i32 = 207; pub const C_GRP: i32 = 208; pub const C_GRP_FLT: i32 = 209; pub const C_OPH: i32 = 210; pub const C_SLF_GRP: i32 = 211; pub const C_MODAL_ANSWER: i32 = 212; pub const C_ENR: i32 = 213; pub const C_HAS_LIVE: i32 = 214; pub const C_COST_CHK: i32 = 215; pub const C_RARITY_CHK: i32 = 216; pub const C_HND_NO_LIVE: i32 = 217; pub const C_SCS_LIV: i32 = 218; pub const C_OPP_HND_DIF: i32 = 219; pub const C_CMP: i32 = 220; pub const C_HAS_CHOICE: i32 = 221; pub const C_OPPONENT_CHOICE: i32 = 222; pub const C_HRT: i32 = 223; pub const C_BLD: i32 = 224; pub const C_OPP_ENR_DIF: i32 = 225; pub const C_HAS_KWD: i32 = 226; pub const C_DK_REFR: i32 = 227; pub const C_HAS_MOVED: i32 = 228; pub const C_HND_INC: i32 = 229; pub const C_LIV_ZN: i32 = 230; pub const C_BATON: i32 = 231; pub const C_TYPE_CHECK: i32 = 232; pub const C_IS_IN_DISCARD: i32 = 233; // --- ABILITY FLAGS --- // Pre-computed bitflags for fast heuristic checks pub const FLAG_DRAW: u64 = 1 << 0; // 10, 41 pub const FLAG_SEARCH: u64 = 1 << 1; // 22 pub const FLAG_RECOVER: u64 = 1 << 2; // 15, 17 pub const FLAG_BUFF: u64 = 1 << 3; // 11, 12 pub const FLAG_CHARGE: u64 = 1 << 4; // 23 pub const FLAG_TEMPO: u64 = 1 << 5; // 43, 51 (Untap/Tap) pub const FLAG_REDUCE: u64 = 1 << 6; // 13 pub const FLAG_BOOST: u64 = 1 << 7; // 16 pub const FLAG_TRANSFORM: u64 = 1 << 8; // 39 pub const FLAG_WIN_COND: u64 = 1 << 9; // 48 (Reduce Heart) pub const FLAG_MOVE: u64 = 1 << 10; // 20, 21 (Move/Swap) pub const FLAG_TAP: u64 = 1 << 11; // 32, 53 (Tap O/M) // --- CHOICE FLAGS (For Ability Struct) --- pub const CHOICE_FLAG_LOOK: u8 = 1 << 0; // O_LOOK_AND_CHOOSE pub const CHOICE_FLAG_MODE: u8 = 1 << 1; // O_SELECT_MODE pub const CHOICE_FLAG_COLOR: u8 = 1 << 2; // O_COLOR_SELECT pub const CHOICE_FLAG_ORDER: u8 = 1 << 3; // O_ORDER_DECK // --- FEATURE FLAGS (For Heuristic Encoding) --- pub const SYN_FLAG_GROUP: u32 = 1 << 0; pub const SYN_FLAG_COLOR: u32 = 1 << 1; pub const SYN_FLAG_BATON: u32 = 1 << 2; pub const SYN_FLAG_CENTER: u32 = 1 << 3; pub const SYN_FLAG_LIFE_LEAD: u32 = 1 << 4; pub const COST_FLAG_DISCARD: u32 = 1 << 0; pub const COST_FLAG_TAP: u32 = 1 << 1; // --- MODELS --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr, Default)] #[repr(i8)] pub enum Phase { #[default] Setup = -2, MulliganP1 = -1, MulliganP2 = 0, Active = 1, Energy = 2, Draw = 3, Main = 4, LiveSet = 5, PerformanceP1 = 6, PerformanceP2 = 7, LiveResult = 8, Terminal = 9, Response = 10, } pub const ACTION_SPACE: usize = 3000; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Condition { #[serde(rename = "type")] pub condition_type: ConditionType, #[serde(default)] pub value: i32, #[serde(default)] pub attr: u32, #[serde(default)] pub target_slot: u8, #[serde(default)] pub is_negated: bool, #[serde(default)] pub params: serde_json::Value, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Effect { pub effect_type: EffectType, #[serde(default)] pub value: i32, #[serde(default)] pub target: TargetType, #[serde(default)] pub is_optional: bool, #[serde(default)] pub params: serde_json::Value, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Cost { #[serde(rename = "type")] pub cost_type: AbilityCostType, #[serde(default)] pub value: i32, #[serde(default)] pub params: serde_json::Value, } #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct AbilityContext { pub player_id: u8, pub area_idx: i16, pub source_card_id: i16, pub target_slot: i16, pub choice_index: i16, pub selected_color: i16, pub program_counter: u16, pub ability_index: i16, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] pub struct Ability { #[serde(default)] pub raw_text: String, pub trigger: TriggerType, #[serde(default)] pub effects: Vec, #[serde(default)] pub conditions: Vec, #[serde(default)] pub costs: Vec, #[serde(default)] pub is_once_per_turn: bool, #[serde(default)] pub bytecode: Vec, #[serde(default)] pub modal_options: serde_json::Value, #[serde(skip)] pub choice_flags: u8, #[serde(skip)] pub choice_count: u8, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct MemberCard { pub card_id: u16, pub card_no: String, pub name: String, pub cost: u32, pub hearts: [u8; 7], pub blade_hearts: [u8; 7], pub blades: u32, pub groups: Vec, pub units: Vec, pub abilities: Vec, pub volume_icons: u32, pub draw_icons: u32, #[serde(default)] pub ability_text: String, #[serde(default)] pub char_id: u32, #[serde(default)] pub img_path: String, #[serde(skip)] pub semantic_flags: u32, #[serde(skip)] pub ability_flags: u64, #[serde(skip)] pub synergy_flags: u32, #[serde(skip)] pub cost_flags: u32, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct LiveCard { pub card_id: u16, pub card_no: String, pub name: String, pub score: u32, pub required_hearts: [u8; 7], pub abilities: Vec, pub groups: Vec, pub units: Vec, pub volume_icons: u32, pub blade_hearts: [u8; 7], #[serde(default)] pub rare: String, #[serde(default)] pub ability_text: String, #[serde(default)] pub img_path: String, #[serde(skip)] pub semantic_flags: u32, #[serde(skip)] pub synergy_flags: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Card { Member(MemberCard), Live(LiveCard), } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct PlayerState { pub player_id: u8, pub hand: SmallVec<[u16; 16]>, pub deck: SmallVec<[u16; 64]>, pub discard: SmallVec<[u16; 32]>, pub exile: SmallVec<[u16; 16]>, pub energy_deck: SmallVec<[u16; 16]>, pub energy_zone: SmallVec<[u16; 16]>, pub success_lives: SmallVec<[u16; 8]>, pub live_zone: [i16; 3], // -1 or ID (max 30144 fits in i16) pub live_zone_revealed: [bool; 3], pub stage: [i16; 3], pub stage_energy_count: [u8; 3], pub tapped_members: [bool; 3], pub tapped_energy: SmallVec<[bool; 16]>, pub baton_touch_count: u8, pub baton_touch_limit: u8, pub score: u32, pub current_turn_volume: u32, pub used_abilities: SmallVec<[u32; 16]>, pub cannot_live: bool, pub live_score_bonus: i32, pub blade_buffs: [i32; 3], pub heart_buffs: [[i32; 7]; 3], pub cost_reduction: i32, pub deck_refreshed_this_turn: bool, pub moved_members_this_turn: [bool; 3], pub hand_increased_this_turn: u32, pub stage_energy: [SmallVec<[u16; 4]>; 3], pub color_transforms: SmallVec<[(u8, u8); 4]>, pub heart_req_reductions: [i32; 7], // pub meta_rules: Vec, // Removed: Unused heap allocation pub mulligan_selection: u64, pub hand_added_turn: SmallVec<[u16; 16]>, pub has_immunity: bool, pub restrictions: SmallVec<[u8; 8]>, pub looked_cards: SmallVec<[u16; 16]>, // Shared buffer for revealing cards to UI pub live_deck: SmallVec<[u16; 32]>, // Live cards available for Live Set phase pub granted_abilities: Vec<(i16, u16, u16)>, // (target_cid, source_cid, ab_idx) pub cost_modifiers: Vec<(Condition, i32)>, // (condition, amount) } impl Default for PlayerState { fn default() -> Self { PlayerState { player_id: 0, hand: SmallVec::new(), deck: SmallVec::new(), discard: SmallVec::new(), exile: SmallVec::new(), energy_deck: SmallVec::new(), energy_zone: SmallVec::new(), success_lives: SmallVec::new(), live_zone: [-1; 3], live_zone_revealed: [false; 3], stage: [-1; 3], stage_energy_count: [0; 3], tapped_members: [false; 3], tapped_energy: SmallVec::new(), baton_touch_count: 0, baton_touch_limit: 1, score: 0, current_turn_volume: 0, used_abilities: SmallVec::new(), cannot_live: false, live_score_bonus: 0, blade_buffs: [0; 3], heart_buffs: [[0; 7]; 3], cost_reduction: 0, deck_refreshed_this_turn: false, moved_members_this_turn: [false; 3], hand_increased_this_turn: 0, stage_energy: [SmallVec::new(), SmallVec::new(), SmallVec::new()], color_transforms: SmallVec::new(), heart_req_reductions: [0; 7], // meta_rules: Vec::new(), mulligan_selection: 0, hand_added_turn: SmallVec::new(), has_immunity: false, restrictions: SmallVec::new(), looked_cards: SmallVec::new(), live_deck: SmallVec::new(), granted_abilities: Vec::new(), cost_modifiers: Vec::new(), } } } impl PlayerState { pub fn untap_all(&mut self) { self.tapped_members = [false; 3]; for tapped in &mut self.tapped_energy { *tapped = false; } self.baton_touch_count = 0; self.blade_buffs = [0; 3]; self.heart_buffs = [[0; 7]; 3]; self.cost_reduction = 0; self.live_score_bonus = 0; self.used_abilities.clear(); self.color_transforms.clear(); self.heart_req_reductions = [0; 7]; self.mulligan_selection = 0; self.moved_members_this_turn = [false; 3]; self.granted_abilities.clear(); self.cost_modifiers.clear(); } pub fn copy_from(&mut self, other: &PlayerState) { self.player_id = other.player_id; self.hand.clear(); self.hand.extend_from_slice(&other.hand); self.deck.clear(); self.deck.extend_from_slice(&other.deck); self.discard.clear(); self.discard.extend_from_slice(&other.discard); self.exile.clear(); self.exile.extend_from_slice(&other.exile); self.energy_deck.clear(); self.energy_deck.extend_from_slice(&other.energy_deck); self.energy_zone.clear(); self.energy_zone.extend_from_slice(&other.energy_zone); self.success_lives.clear(); self.success_lives.extend_from_slice(&other.success_lives); self.live_zone = other.live_zone; self.live_zone_revealed = other.live_zone_revealed; self.stage = other.stage; self.stage_energy_count = other.stage_energy_count; self.tapped_members = other.tapped_members; self.tapped_energy.clear(); self.tapped_energy.extend_from_slice(&other.tapped_energy); self.baton_touch_count = other.baton_touch_count; self.baton_touch_limit = other.baton_touch_limit; self.score = other.score; self.current_turn_volume = other.current_turn_volume; self.used_abilities.clear(); self.used_abilities.extend_from_slice(&other.used_abilities); self.cannot_live = other.cannot_live; self.live_score_bonus = other.live_score_bonus; self.blade_buffs = other.blade_buffs; self.heart_buffs = other.heart_buffs; self.cost_reduction = other.cost_reduction; self.deck_refreshed_this_turn = other.deck_refreshed_this_turn; self.moved_members_this_turn = other.moved_members_this_turn; self.hand_increased_this_turn = other.hand_increased_this_turn; for i in 0..3 { self.stage_energy[i].clear(); self.stage_energy[i].extend_from_slice(&other.stage_energy[i]); } self.color_transforms.clear(); self.color_transforms.extend_from_slice(&other.color_transforms); self.heart_req_reductions = other.heart_req_reductions; // self.meta_rules.clear(); self.meta_rules.extend_from_slice(&other.meta_rules); self.mulligan_selection = other.mulligan_selection; self.hand_added_turn.clear(); self.hand_added_turn.extend_from_slice(&other.hand_added_turn); self.has_immunity = other.has_immunity; self.restrictions.clear(); self.restrictions.extend_from_slice(&other.restrictions); self.looked_cards.clear(); self.looked_cards.extend_from_slice(&other.looked_cards); self.live_deck.clear(); self.live_deck.extend_from_slice(&other.live_deck); self.granted_abilities.clear(); self.granted_abilities.extend_from_slice(&other.granted_abilities); self.cost_modifiers.clear(); self.cost_modifiers.extend(other.cost_modifiers.iter().cloned()); } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct GameState { pub players: [PlayerState; 2], pub current_player: u8, pub first_player: u8, pub phase: Phase, pub yell_cards: SmallVec<[u16; 8]>, pub prev_card_id: i16, pub turn: u16, #[serde(default)] pub silent: bool, #[serde(default)] pub rule_log: Vec, #[serde(skip)] pub performance_results: HashMap, #[serde(skip)] pub last_performance_results: HashMap, #[serde(default)] #[serde(skip)] pub performance_history: Vec, #[serde(default)] pub trigger_depth: u16, #[serde(default)] pub live_set_pending_draws: [u8; 2], #[serde(default)] pub pending_ctx: Option, #[serde(default)] pub pending_card_id: i16, #[serde(default)] pub pending_ab_idx: i16, #[serde(default)] pub pending_effect_opcode: i16, #[serde(default)] pub pending_choice_type: String, #[serde(skip, default = "SmallRng::from_os_rng")] pub rng: SmallRng, } impl Default for GameState { fn default() -> Self { GameState { players: [PlayerState::default(), PlayerState::default()], current_player: 0, first_player: 0, phase: Phase::Setup, yell_cards: SmallVec::new(), prev_card_id: -1, turn: 1, silent: false, rule_log: Vec::new(), performance_results: std::collections::HashMap::new(), last_performance_results: std::collections::HashMap::new(), performance_history: Vec::new(), trigger_depth: 0, live_set_pending_draws: [0, 0], pending_ctx: None, pending_card_id: -1, pending_ab_idx: -1, pending_effect_opcode: -1, pending_choice_type: String::new(), rng: SmallRng::from_os_rng(), } } } // --- LOGIC --- pub struct CardDatabase { pub members: HashMap, pub lives: HashMap, // Optimization 1: Fast Lookup Vectors pub members_vec: Vec>, pub lives_vec: Vec>, } pub const LIVE_ID_OFFSET: u32 = 10000; pub const BASE_ID_MASK: u32 = 0xFFFFF; pub const ENERGY_ID_OFFSET: u32 = 20000; impl CardDatabase { pub fn from_json(json_str: &str) -> serde_json::Result { let raw: serde_json::Value = serde_json::from_str(json_str)?; let mut db = Self { members: HashMap::new(), lives: HashMap::new(), members_vec: vec![None; 10000], // Large enough for members lives_vec: vec![None; 2000], // 2000 enough for ~200 lives }; if let Some(members_raw) = raw.get("member_db").and_then(|m| m.as_object()) { for (_, val) in members_raw { let mut card = serde_json::from_value::(val.clone())?; // Pre-compute ability flags let mut flags = 0u64; for ab in &mut card.abilities { if Self::has_opcode_static_fast(&ab.bytecode, 10) || Self::has_opcode_static_fast(&ab.bytecode, 41) { flags |= FLAG_DRAW; } if Self::has_opcode_static_fast(&ab.bytecode, 22) { flags |= FLAG_SEARCH; } if Self::has_opcode_static_fast(&ab.bytecode, 15) || Self::has_opcode_static_fast(&ab.bytecode, 17) { flags |= FLAG_RECOVER; } if Self::has_opcode_static_fast(&ab.bytecode, 11) || Self::has_opcode_static_fast(&ab.bytecode, 12) { flags |= FLAG_BUFF; } if Self::has_opcode_static_fast(&ab.bytecode, 20) || Self::has_opcode_static_fast(&ab.bytecode, 21) { flags |= FLAG_MOVE; } if Self::has_opcode_static_fast(&ab.bytecode, 32) || Self::has_opcode_static_fast(&ab.bytecode, 53) { flags |= FLAG_TAP; } if Self::has_opcode_static_fast(&ab.bytecode, 23) { flags |= FLAG_CHARGE; } if Self::has_opcode_static_fast(&ab.bytecode, 43) || Self::has_opcode_static_fast(&ab.bytecode, 51) { flags |= FLAG_TEMPO; } if Self::has_opcode_static_fast(&ab.bytecode, 13) { flags |= FLAG_REDUCE; } if Self::has_opcode_static_fast(&ab.bytecode, 16) { flags |= FLAG_BOOST; } if Self::has_opcode_static_fast(&ab.bytecode, 39) { flags |= FLAG_TRANSFORM; } if Self::has_opcode_static_fast(&ab.bytecode, 48) { flags |= FLAG_WIN_COND; } let mut s_flags = 0u32; if ab.trigger == TriggerType::OnPlay { s_flags |= 0x01; } if ab.trigger == TriggerType::Activated { s_flags |= 0x02; } if ab.trigger == TriggerType::TurnStart || ab.trigger == TriggerType::TurnEnd { s_flags |= 0x04; } if ab.is_once_per_turn { s_flags |= 0x08; } // Check for unflagged logic (semantic extension) let flagged_ops = [ O_DRAW, 17, 15, 11, 12, // O_RECOV_M, O_RECOV_L, O_BLADES, O_HEARTS 22, 16, 23, 20, // O_SEARCH_DECK, O_BOOST, O_CHARGE, O_MOVE_MEMBER 21, 32, 49, // O_SWAP_CARDS, O_TAP_O, O_MODIFY_SCORE_RULE 13, 48 // O_REDUCE_COST, O_REDUCE_HEART_REQ ]; if ab.bytecode.iter().any(|op| !flagged_ops.contains(op)) { s_flags |= 0x10; } card.semantic_flags |= s_flags; // Compute Choice Flags and counts ab.choice_flags = 0; ab.choice_count = 0; if Self::has_opcode_static_fast(&ab.bytecode, O_LOOK_AND_CHOOSE) { ab.choice_flags |= CHOICE_FLAG_LOOK; ab.choice_count = ab.bytecode.chunks(4).find(|c| !c.is_empty() && c[0] == O_LOOK_AND_CHOOSE).map(|c| c[1] as u8).unwrap_or(3); } if Self::has_opcode_static_fast(&ab.bytecode, O_SELECT_MODE) { ab.choice_flags |= CHOICE_FLAG_MODE; let count = ab.bytecode.chunks(4).find(|c| !c.is_empty() && c[0] == O_SELECT_MODE).map(|c| c[1] as u8).unwrap_or(2); if ab.choice_count == 0 { ab.choice_count = count; } } if Self::has_opcode_static_fast(&ab.bytecode, O_COLOR_SELECT) { ab.choice_flags |= CHOICE_FLAG_COLOR; if ab.choice_count == 0 { ab.choice_count = 6; } } if Self::has_opcode_static_fast(&ab.bytecode, O_ORDER_DECK) { ab.choice_flags |= CHOICE_FLAG_ORDER; if ab.choice_count == 0 { ab.choice_count = 3; } } // Compute Synergy Flags let mut syn_flags = 0u32; if ab.conditions.iter().any(|c| c.condition_type == ConditionType::CountGroup || c.condition_type == ConditionType::SelfIsGroup) { syn_flags |= SYN_FLAG_GROUP; } if ab.conditions.iter().any(|c| c.condition_type == ConditionType::HasColor) { syn_flags |= SYN_FLAG_COLOR; } if ab.conditions.iter().any(|c| c.condition_type == ConditionType::Baton) { syn_flags |= SYN_FLAG_BATON; } if ab.conditions.iter().any(|c| c.condition_type == ConditionType::IsCenter) { syn_flags |= SYN_FLAG_CENTER; } if ab.conditions.iter().any(|c| c.condition_type == ConditionType::LifeLead) { syn_flags |= SYN_FLAG_LIFE_LEAD; } card.synergy_flags |= syn_flags; // Compute Cost Flags let mut c_flags = 0u32; if ab.costs.iter().any(|c| c.cost_type == AbilityCostType::DiscardHand || c.cost_type == AbilityCostType::DiscardMember) { c_flags |= COST_FLAG_DISCARD; } if ab.costs.iter().any(|c| c.cost_type == AbilityCostType::TapSelf || c.cost_type == AbilityCostType::TapMember) { c_flags |= COST_FLAG_TAP; } card.cost_flags |= c_flags; } card.ability_flags = flags; db.members.insert(card.card_id, card.clone()); // Populate Vector if (card.card_id as usize) < db.members_vec.len() { db.members_vec[card.card_id as usize] = Some(card.clone()); } } } if let Some(lives_raw) = raw.get("live_db").and_then(|l| l.as_object()) { for (_, val) in lives_raw { let mut card = serde_json::from_value::(val.clone())?; let mut s_flags = 0u32; for ab in &card.abilities { if ab.trigger == TriggerType::OnPlay { s_flags |= 0x01; } // Compute Synergy Flags let mut syn_flags = 0u32; if ab.conditions.iter().any(|c| c.condition_type == ConditionType::CountGroup || c.condition_type == ConditionType::SelfIsGroup) { syn_flags |= SYN_FLAG_GROUP; } if ab.conditions.iter().any(|c| c.condition_type == ConditionType::HasColor) { syn_flags |= SYN_FLAG_COLOR; } if ab.conditions.iter().any(|c| c.condition_type == ConditionType::Baton) { syn_flags |= SYN_FLAG_BATON; } if ab.conditions.iter().any(|c| c.condition_type == ConditionType::IsCenter) { syn_flags |= SYN_FLAG_CENTER; } if ab.conditions.iter().any(|c| c.condition_type == ConditionType::LifeLead) { syn_flags |= SYN_FLAG_LIFE_LEAD; } card.synergy_flags |= syn_flags; } card.semantic_flags = s_flags; db.lives.insert(card.card_id, card.clone()); // Populate Vector (Shift back for index) if card.card_id >= LIVE_ID_OFFSET as u16 { let raw_id = card.card_id - LIVE_ID_OFFSET as u16; if (raw_id as usize) < db.lives_vec.len() { db.lives_vec[raw_id as usize] = Some(card.clone()); } } } } Ok(db) } // Fast Lookups pub fn get_member(&self, id: u16) -> Option<&MemberCard> { let base_id = id & BASE_ID_MASK as u16; if (base_id as usize) < self.members_vec.len() { self.members_vec[base_id as usize].as_ref() } else { self.members.get(&base_id) } } pub fn get_live(&self, id: u16) -> Option<&LiveCard> { let base_id = id & BASE_ID_MASK as u16; if base_id >= LIVE_ID_OFFSET as u16 { let idx = (base_id - LIVE_ID_OFFSET as u16) as usize; if idx < self.lives_vec.len() { self.lives_vec[idx].as_ref() } else { self.lives.get(&base_id) } } else { None } } fn has_opcode_static(bytecode: &[i32], target_op: i32) -> bool { let mut i = 0; while i < bytecode.len() { if i + 3 >= bytecode.len() { break; } let op = bytecode[i]; if op == target_op { return true; } i += 4; } false } // Optimized opcode check that just checks 0th element of chunks(4) fn has_opcode_static_fast(bytecode: &[i32], target_op: i32) -> bool { let len = bytecode.len(); let mut i = 0; while i < len { if bytecode[i] == target_op { return true; } i += 4; } false } } pub fn bytecode_has_choice(bytecode: &[i32]) -> bool { bytecode.chunks(4).any(|chunk| { if chunk.is_empty() { return false; } let op = chunk[0]; op == O_SELECT_MODE || op == O_LOOK_AND_CHOOSE || op == O_COLOR_SELECT || op == O_TAP_O || op == O_ORDER_DECK || op == O_PLAY_MEMBER_FROM_HAND || op == O_PLAY_MEMBER_FROM_DISCARD || op == O_OPPONENT_CHOOSE }) } /// Helper to check if bytecode requires an EARLY pause (before execution begins). /// O_ORDER_DECK is EXCLUDED because it needs O_LOOK_DECK to run first to populate looked_cards. /// O_LOOK_AND_CHOOSE may also need prior opcodes to run, but typically doesn't have setup. pub fn bytecode_needs_early_pause(bytecode: &[i32]) -> bool { bytecode.chunks(4).any(|chunk| { if chunk.is_empty() { return false; } let op = chunk[0]; // Only opcodes that can pause BEFORE any execution op == O_SELECT_MODE || op == O_COLOR_SELECT }) } pub fn bytecode_needs_early_pause_opcode(bytecode: &[i32]) -> i32 { bytecode.chunks(4).find(|chunk| { if chunk.is_empty() { return false; } let op = chunk[0]; op == O_SELECT_MODE || op == O_COLOR_SELECT }).map(|chunk| chunk[0]).unwrap_or(-1) } impl GameState { pub fn safe_set_mask(&self, mask: &mut [bool], aid: usize) { if aid < mask.len() { mask[aid] = true; } else { panic!("[ACTION_OOB] Action ID {} exceeds mask len {} in phase {:?}", aid, mask.len(), self.phase); } } pub fn log(&mut self, msg: String) { if self.silent { return; } let full_msg = if msg.starts_with("[Turn") { msg } else { format!("[Turn {}] {}", self.turn, msg) }; println!("{}", full_msg); self.rule_log.push(full_msg); } pub fn log_rule(&mut self, rule: &str, msg: &str) { if self.silent { return; } self.log(format!("[{}] {}", rule, msg)); } pub fn setup_turn_log(&mut self) { if self.silent { return; } let p_idx = self.current_player; self.log(format!("=== Player {}'s Turn ===", p_idx)); } pub fn initialize_game(&mut self, p0_deck: Vec, p1_deck: Vec, p0_energy: Vec, p1_energy: Vec, p0_lives: Vec, p1_lives: Vec) { self.initialize_game_with_seed(p0_deck, p1_deck, p0_energy, p1_energy, p0_lives, p1_lives, None); } pub fn copy_from(&mut self, other: &GameState) { self.players[0].copy_from(&other.players[0]); self.players[1].copy_from(&other.players[1]); self.current_player = other.current_player; self.first_player = other.first_player; self.phase = other.phase; self.yell_cards.clear(); self.yell_cards.extend_from_slice(&other.yell_cards); self.prev_card_id = other.prev_card_id; self.turn = other.turn; self.silent = other.silent; // self.rule_log.clear(); // We purposely do not copy rule log to avoid overhead self.trigger_depth = other.trigger_depth; self.live_set_pending_draws = other.live_set_pending_draws; self.pending_ctx = other.pending_ctx; self.pending_card_id = other.pending_card_id; self.pending_ab_idx = other.pending_ab_idx; self.pending_effect_opcode = other.pending_effect_opcode; self.pending_choice_type.clear(); self.pending_choice_type.push_str(&other.pending_choice_type); } pub fn initialize_game_with_seed(&mut self, p0_deck: Vec, p1_deck: Vec, p0_energy: Vec, p1_energy: Vec, p0_lives: Vec, p1_lives: Vec, seed: Option) { // Lives are NOT added to deck - they are separate cards for Live Set phase // Rule 5.1: Deck contains exactly 60 member cards self.players[0].deck = SmallVec::from_vec(p0_deck); self.players[1].deck = SmallVec::from_vec(p1_deck); // Store lives separately for Live Set phase selection self.players[0].live_deck = SmallVec::from_vec(p0_lives); self.players[1].live_deck = SmallVec::from_vec(p1_lives); self.players[0].energy_deck = SmallVec::from_vec(p0_energy); self.players[1].energy_deck = SmallVec::from_vec(p1_energy); // Reset state for i in 0..2 { self.players[i].hand.clear(); self.players[i].energy_zone.clear(); self.players[i].tapped_energy.clear(); self.players[i].stage = [-1; 3]; self.players[i].tapped_members = [false; 3]; self.players[i].discard.clear(); self.players[i].success_lives.clear(); self.players[i].mulligan_selection = 0; self.players[i].hand_added_turn.clear(); } let mut rng = match seed { Some(s) => Pcg64::seed_from_u64(s), None => Pcg64::from_os_rng(), }; self.players[0].deck.shuffle(&mut rng); self.players[1].deck.shuffle(&mut rng); self.players[0].energy_deck.shuffle(&mut rng); self.players[1].energy_deck.shuffle(&mut rng); // Initial Hands (6 cards) self.log("Rule 6.2.1.5: Both players draw 6 cards as starting hand.".to_string()); self.draw_cards(0, 6); self.draw_cards(1, 6); // Initial Energy (3 cards) self.log("Rule 6.2.1.7: Both players place 3 cards from Energy Deck to Energy Zone.".to_string()); for i in 0..2 { for _ in 0..3 { if let Some(cid) = self.players[i].energy_deck.pop() { self.players[i].energy_zone.push(cid); self.players[i].tapped_energy.push(false); } } } // Setup Lives self.players[0].live_zone = [-1; 3]; self.players[1].live_zone = [-1; 3]; for i in 0..3 { self.players[0].live_zone_revealed[i] = false; self.players[1].live_zone_revealed[i] = false; } // Setup first player self.phase = Phase::MulliganP1; self.current_player = self.first_player; self.setup_turn_log(); } // Helper for shuffling decks fn fisher_yates_shuffle(&mut self, player_idx: usize) { self.players[player_idx].deck.shuffle(&mut self.rng); } pub fn resolve_deck_refresh(&mut self, player_idx: usize) { let player = &mut self.players[player_idx]; if player.deck.is_empty() && !player.discard.is_empty() { let discard = player.discard.drain(..).collect::>(); player.deck.extend(discard); player.deck.shuffle(&mut self.rng); self.players[player_idx].deck_refreshed_this_turn = true; } } pub fn draw_cards(&mut self, player_idx: usize, count: u32) { for _ in 0..count { if self.players[player_idx].deck.is_empty() { self.resolve_deck_refresh(player_idx); } if let Some(card_id) = self.players[player_idx].deck.pop() { self.players[player_idx].hand.push(card_id); self.players[player_idx].hand_added_turn.push(self.turn); } } } pub fn draw_energy_cards(&mut self, player_idx: usize, count: i32) { for _ in 0..count { if let Some(cid) = self.players[player_idx].energy_deck.pop() { self.players[player_idx].energy_zone.push(cid); self.players[player_idx].tapped_energy.push(false); } } } pub fn pay_energy(&mut self, player_idx: usize, count: i32) { let mut paid = 0; for i in 0..self.players[player_idx].tapped_energy.len() { if paid >= count { break; } if !self.players[player_idx].tapped_energy[i] { self.players[player_idx].tapped_energy[i] = true; paid += 1; } } } pub fn activate_energy(&mut self, player_idx: usize, count: i32) { let mut activated = 0; for i in 0..self.players[player_idx].tapped_energy.len() { if activated >= count { break; } if self.players[player_idx].tapped_energy[i] { self.players[player_idx].tapped_energy[i] = false; activated += 1; } } } pub fn set_member_tapped(&mut self, player_idx: usize, slot_idx: usize, tapped: bool) { if slot_idx < self.players[player_idx].tapped_members.len() { self.players[player_idx].tapped_members[slot_idx] = tapped; } } pub fn check_win_condition(&mut self) { if self.phase == Phase::Terminal { return; } let p0_lives = self.players[0].success_lives.len(); let p1_lives = self.players[1].success_lives.len(); if p0_lives >= 3 || p1_lives >= 3 { self.phase = Phase::Terminal; } // Rule 1.2.1.1: Win condition based on successful lives let p0_success = self.players[0].success_lives.len(); let p1_success = self.players[1].success_lives.len(); if p0_success >= 3 && p1_success < 3 { self.phase = Phase::Terminal; self.log("Rule 1.2.1.1: Player 0 wins by 3 successful lives.".to_string()); } else if p1_success >= 3 && p0_success < 3 { self.phase = Phase::Terminal; self.log("Rule 1.2.1.1: Player 1 wins by 3 successful lives.".to_string()); } else if p0_success >= 3 && p1_success >= 3 { self.phase = Phase::Terminal; self.log("Rule 1.2.1.2: Draw (Both players reached 3 successful lives).".to_string()); } // Turn Limit if self.turn > 40 { self.phase = Phase::Terminal; } } // --- AI Encoding --- /// Encodes game state into a fixed-size feature vector for neural network input. /// /// Layout (~750 floats, padded to 800): /// - Global Features (20): Turn, Phase, Scores, Resources, Heuristic Scores /// - P0 Stage (3 * 32 = 96): Card feature vectors /// - P1 Stage (3 * 32 = 96): Card feature vectors /// - P0 Live Zone (3 * 32 = 96): Card feature vectors /// - P1 Live Zone (3 * 32 = 96): Card feature vectors (revealed only) /// - P0 Hand (10 * 32 = 320): Card feature vectors /// - P1 Hand Count (1): Hidden info /// - Derived Heuristics (20): Suitability, Power Differential, etc. pub fn encode_state(&self, db: &CardDatabase) -> Vec { const CARD_FEAT_SIZE: usize = 48; const TOTAL_SIZE: usize = 1200; let mut feats = Vec::with_capacity(TOTAL_SIZE); // ===================== // GLOBAL FEATURES (20) // ===================== feats.push(self.turn as f32 / 50.0); feats.push(self.phase as i32 as f32 / 10.0); feats.push(self.current_player as f32); feats.push(self.players[0].score as f32 / 10.0); feats.push(self.players[1].score as f32 / 10.0); feats.push(self.players[0].success_lives.len() as f32 / 3.0); feats.push(self.players[1].success_lives.len() as f32 / 3.0); feats.push(self.players[0].hand.len() as f32 / 10.0); feats.push(self.players[1].hand.len() as f32 / 10.0); feats.push(self.players[0].energy_zone.len() as f32 / 10.0); feats.push(self.players[1].energy_zone.len() as f32 / 10.0); feats.push(self.players[0].deck.len() as f32 / 50.0); feats.push(self.players[1].deck.len() as f32 / 50.0); let p0_tapped = self.players[0].tapped_energy.iter().filter(|&&t| t).count(); let p1_tapped = self.players[1].tapped_energy.iter().filter(|&&t| t).count(); feats.push(p0_tapped as f32 / 10.0); feats.push(p1_tapped as f32 / 10.0); feats.push(self.players[0].baton_touch_count as f32 / 3.0); feats.push(self.players[1].baton_touch_count as f32 / 3.0); // Pad globals to 20 while feats.len() < 20 { feats.push(0.0); } // ===================== // P0 STAGE (3 slots * 32) // ===================== for i in 0..3 { self.encode_member_slot(&mut feats, 0, i, db); } // ===================== // P1 STAGE (3 slots * 32) // ===================== for i in 0..3 { self.encode_member_slot(&mut feats, 1, i, db); } // ===================== // P0 LIVE ZONE (3 slots * 32) - CRITICAL FIX: Was missing! // ===================== for i in 0..3 { self.encode_live_slot(&mut feats, 0, i, db, true); // Always visible to self } // ===================== // P1 LIVE ZONE (3 slots * 32) - Only if revealed // ===================== for i in 0..3 { let revealed = self.players[1].live_zone_revealed[i]; self.encode_live_slot(&mut feats, 1, i, db, revealed); } // ===================== // P0 HAND (10 slots * 32) - Feature-based, not ID-based // ===================== for slot in 0..10 { if slot < self.players[0].hand.len() { let cid = self.players[0].hand[slot]; self.encode_card_features(&mut feats, cid, db); } else { // Empty slot for _ in 0..CARD_FEAT_SIZE { feats.push(0.0); } } } // P1 Hand Count (hidden info, just count) feats.push(self.players[1].hand.len() as f32 / 10.0); // ===================== // DERIVED HEURISTIC FEATURES (20) // ===================== // Total board power let mut p0_power = 0i32; let mut p1_power = 0i32; for i in 0..3 { p0_power += self.get_effective_blades(0, i, db) as i32; p1_power += self.get_effective_blades(1, i, db) as i32; } feats.push(p0_power as f32 / 30.0); feats.push(p1_power as f32 / 30.0); feats.push((p0_power - p1_power) as f32 / 30.0); // Power differential // Heart suitability (how close P0 is to clearing a live) let mut p0_stage_hearts = [0u32; 7]; for i in 0..3 { let h = self.get_effective_hearts(0, i, db); for c in 0..7 { p0_stage_hearts[c] += h[c] as u32; } } // Calculate suitability against P0's first live let mut suitability = 0.0f32; if self.players[0].live_zone[0] >= 0 { if let Some(l) = db.get_live(self.players[0].live_zone[0] as u16) { let mut total_req = 0u32; let mut total_sat = 0u32; for c in 0..7 { let req = l.required_hearts[c] as u32; let have = p0_stage_hearts[c].min(req); total_req += req; total_sat += have; } // Use any-color hearts for remaining let remaining = total_req.saturating_sub(total_sat); let any_color = p0_stage_hearts[6]; total_sat += remaining.min(any_color); if total_req > 0 { suitability = total_sat as f32 / total_req as f32; } } } feats.push(suitability); // Lives remaining urgency feats.push((3 - self.players[0].success_lives.len()) as f32 / 3.0); feats.push((3 - self.players[1].success_lives.len()) as f32 / 3.0); // Volume accumulated this turn feats.push(self.players[0].current_turn_volume as f32 / 20.0); feats.push(self.players[1].current_turn_volume as f32 / 20.0); // Pad heuristics section while feats.len() < TOTAL_SIZE - 10 { feats.push(0.0); } // Final padding to exact size while feats.len() < TOTAL_SIZE { feats.push(0.0); } if feats.len() > TOTAL_SIZE { feats.truncate(TOTAL_SIZE); } feats } /// Encode a member card slot into a 32-float feature vector fn encode_member_slot(&self, feats: &mut Vec, p_idx: usize, slot: usize, db: &CardDatabase) { const CARD_FEAT_SIZE: usize = 48; let cid = self.players[p_idx].stage[slot]; if cid >= 0 { if let Some(m) = db.get_member(cid as u16) { feats.push(1.0); // [0] Present feats.push(1.0); // [1] Type: Member feats.push(0.0); // [2] Type: Live feats.push(0.0); // [3] Type: Energy feats.push(m.cost as f32 / 10.0); // [4] Cost feats.push(self.get_effective_blades(p_idx, slot, db) as f32 / 10.0); // [5] Power // [6-12] Hearts provided let hearts = self.get_effective_hearts(p_idx, slot, db); for h in hearts { feats.push(h as f32 / 5.0); } // [13-19] Required hearts (N/A for members, zero) for _ in 0..7 { feats.push(0.0); } // [20-26] Blade hearts bonus for &bh in &m.blade_hearts { feats.push(bh as f32 / 5.0); } // [27] Volume icons feats.push(m.volume_icons as f32 / 5.0); // Trigger Type Flags (3) feats.push(if (m.semantic_flags & 0x01) != 0 { 1.0 } else { 0.0 }); // OnPlay feats.push(if (m.semantic_flags & 0x04) != 0 { 1.0 } else { 0.0 }); // Turn feats.push(if (m.semantic_flags & 0x02) != 0 { 1.0 } else { 0.0 }); // Activated // [31] Tapped status feats.push(if self.players[p_idx].tapped_members[slot] { 1.0 } else { 0.0 }); // Pad to 48 while (feats.len() % 48) != 0 { feats.push(0.0); } return; } } // Empty slot for _ in 0..CARD_FEAT_SIZE { feats.push(0.0); } } /// Encode a live card slot into a 48-float feature vector fn encode_live_slot(&self, feats: &mut Vec, p_idx: usize, slot: usize, db: &CardDatabase, visible: bool) { const CARD_FEAT_SIZE: usize = 48; let cid = self.players[p_idx].live_zone[slot]; if cid >= 0 && visible { if let Some(l) = db.get_live(cid as u16) { feats.push(1.0); // [0] Present feats.push(0.0); // [1] Type: Member feats.push(1.0); // [2] Type: Live feats.push(0.0); // [3] Type: Energy feats.push(0.0); // [4] Cost (N/A for lives) feats.push(l.score as f32 / 10.0); // [5] Score // [6-12] Hearts provided (N/A for lives) for _ in 0..7 { feats.push(0.0); } // [13-19] Required hearts for &h in &l.required_hearts { feats.push(h as f32 / 5.0); } // [20-26] Blade hearts bonus for &bh in &l.blade_hearts { feats.push(bh as f32 / 5.0); } // [27] Volume icons feats.push(l.volume_icons as f32 / 5.0); // [28-31] Ability triggers and padding feats.push(if (l.semantic_flags & 0x01) != 0 { 1.0 } else { 0.0 }); feats.push(0.0); // Turn trigger feats.push(0.0); // Activate // Pad to 48 while (feats.len() % 48) != 0 { feats.push(0.0); } return; } } // Empty or hidden slot for _ in 0..CARD_FEAT_SIZE { feats.push(0.0); } } /// Encode any card (from hand) into a 48-float feature vector /// Now includes semantic ability flags, conditions (synergies), and costs. fn encode_card_features(&self, feats: &mut Vec, cid: u16, db: &CardDatabase) { const CARD_FEAT_SIZE: usize = 48; // Helper: Check if any ability's bytecode contains a specific opcode fn bytecode_has_op(abilities: &[Ability], op: i32) -> bool { abilities.iter().any(|a| a.bytecode.contains(&op)) } // Helper: Check if any ability has a specific condition type // Try member first if let Some(m) = db.get_member(cid) { feats.push(1.0); // Present feats.push(1.0); feats.push(0.0); feats.push(0.0); // Type: Member feats.push(m.cost as f32 / 10.0); feats.push(m.blades as f32 / 10.0); for &h in &m.hearts { feats.push(h as f32 / 5.0); } for _ in 0..7 { feats.push(0.0); } // Required hearts N/A for &bh in &m.blade_hearts { feats.push(bh as f32 / 5.0); } feats.push(m.volume_icons as f32 / 5.0); // --- Flags --- // Trigger Type Flags (3) feats.push(if (m.semantic_flags & 0x01) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.semantic_flags & 0x04) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.semantic_flags & 0x02) != 0 { 1.0 } else { 0.0 }); // Semantic Ability Flags (10) feats.push(if (m.ability_flags & FLAG_DRAW) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_RECOVER) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_RECOVER) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_BUFF) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_SEARCH) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_BOOST) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_CHARGE) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_MOVE) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_TAP) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.ability_flags & FLAG_WIN_COND) != 0 { 1.0 } else { 0.0 }); // Synergy/Condition Flags (5) feats.push(if (m.synergy_flags & SYN_FLAG_GROUP) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.synergy_flags & SYN_FLAG_COLOR) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.synergy_flags & SYN_FLAG_BATON) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.synergy_flags & SYN_FLAG_CENTER) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.synergy_flags & SYN_FLAG_LIFE_LEAD) != 0 { 1.0 } else { 0.0 }); // Cost Flags (2) feats.push(if (m.cost_flags & COST_FLAG_DISCARD) != 0 { 1.0 } else { 0.0 }); feats.push(if (m.cost_flags & COST_FLAG_TAP) != 0 { 1.0 } else { 0.0 }); // New Semantic Extension (2) let is_reduction = (m.ability_flags & FLAG_REDUCE) != 0; feats.push(if is_reduction { 1.0 } else { 0.0 }); feats.push(if (m.semantic_flags & 0x10) != 0 { 1.0 } else { 0.0 }); // Padding to CARD_FEAT_SIZE while (feats.len() % CARD_FEAT_SIZE) != 0 { feats.push(0.0); } return; } // Try live if let Some(l) = db.get_live(cid) { feats.push(1.0); // Present feats.push(0.0); feats.push(1.0); feats.push(0.0); // Type: Live feats.push(0.0); // Cost N/A feats.push(l.score as f32 / 10.0); for _ in 0..7 { feats.push(0.0); } // Hearts provided N/A for &h in &l.required_hearts { feats.push(h as f32 / 5.0); } for &bh in &l.blade_hearts { feats.push(bh as f32 / 5.0); } feats.push(l.volume_icons as f32 / 5.0); // Flags (Trigger + Ability + Synergy + Cost) = 3 + 10 + 5 + 2 = 20 total for _ in 0..20 { feats.push(0.0); } while (feats.len() % CARD_FEAT_SIZE) != 0 { feats.push(0.0); } return; } // Energy or unknown - minimal encoding feats.push(1.0); // Present feats.push(0.0); feats.push(0.0); feats.push(1.0); // Type: Energy while (feats.len() % CARD_FEAT_SIZE) != 0 { feats.push(0.0); } } pub fn process_rule_checks(&mut self) { for i in 0..2 { if self.players[i].deck.is_empty() && !self.players[i].discard.is_empty() { self.resolve_deck_refresh(i); } // Rule 10.5.3: Energy in empty member area -> Energy Deck for slot_idx in 0..3 { if self.players[i].stage[slot_idx] < 0 { if !self.players[i].stage_energy[slot_idx].is_empty() { if !self.silent { self.log(format!("Rule 10.5.3: Reclaiming energy from empty slot {} for player {}.", slot_idx, i)); } let reclaimed: Vec = self.players[i].stage_energy[slot_idx].drain(..).collect(); self.players[i].energy_deck.extend(reclaimed); self.players[i].stage_energy_count[slot_idx] = 0; // Shuffle energy deck after returning cards? Rules say "put in energy deck". // Usually implies random position or shuffle if non-ordered. // Energy deck is unordered (Rule 4.9.2), but typically we push back. // Let's shuffle to be safe since it's "deck". let mut rng = Pcg64::from_os_rng(); self.players[i].energy_deck.shuffle(&mut rng); } } } } for p in 0..2 { for i in 0..3 { if self.players[p].stage[i] < 0 && self.players[p].stage_energy_count[i] > 0 { self.players[p].stage_energy_count[i] = 0; } } } self.check_win_condition(); } pub fn get_effective_blades(&self, player_idx: usize, slot_idx: usize, db: &CardDatabase) -> u32 { let cid = self.players[player_idx].stage[slot_idx]; if cid < 0 || self.players[player_idx].tapped_members[slot_idx] { return 0; } let base = db.get_member(cid as u16).map_or(0, |m| m.blades); let buff = self.players[player_idx].blade_buffs[slot_idx]; (base as i32 + buff).max(0) as u32 } pub fn get_effective_hearts(&self, player_idx: usize, slot_idx: usize, db: &CardDatabase) -> [u8; 7] { let cid = self.players[player_idx].stage[slot_idx]; if cid < 0 { return [0; 7]; } let hearts = db.get_member(cid as u16).map_or([0; 7], |m| { let mut h = [0i32; 7]; for i in 0..7 { h[i] = m.hearts[i] as i32; } h }); let buffs = self.players[player_idx].heart_buffs[slot_idx]; let mut result = [0u8; 7]; for i in 0..7 { result[i] = (hearts[i] + buffs[i]).max(0) as u8; } result } pub fn get_total_blades(&self, p_idx: usize, db: &CardDatabase) -> u32 { let mut total = 0u32; for i in 0..3 { total += self.get_effective_blades(p_idx, i, db); } total } pub fn get_total_hearts(&self, p_idx: usize, db: &CardDatabase) -> [u32; 7] { let mut total = [0u32; 7]; for i in 0..3 { let h = self.get_effective_hearts(p_idx, i, db); for j in 0..7 { total[j] += h[j] as u32; } } total } pub fn get_member_cost(&self, p_idx: usize, card_id: i32, slot_idx: i32, db: &CardDatabase) -> i32 { let base_id = if card_id >= 0 { card_id as u16 } else { return 0 }; let m = if let Some(m) = db.get_member(base_id) { m } else { return 0 }; let mut cost = m.cost as i32; // 1. Global reduction cost -= self.players[p_idx].cost_reduction; // 2. Baton Touch (Rule 12) if slot_idx >= 0 && slot_idx < 3 { let old_cid = self.players[p_idx].stage[slot_idx as usize]; if old_cid >= 0 { if let Some(old_m) = db.get_member(old_cid as u16) { cost -= old_m.cost as i32; } } } // 3. Self constant abilities for ab in &m.abilities { if ab.trigger == TriggerType::Constant { let ctx = AbilityContext { source_card_id: card_id as i16, player_id: p_idx as u8, area_idx: slot_idx as i16, ..Default::default() }; if ab.conditions.iter().all(|c| self.check_condition(db, p_idx, c, &ctx)) { let bc = &ab.bytecode; let mut i = 0; while i + 3 < bc.len() { let op = bc[i]; let v = bc[i+1]; if op == O_REDUCE_COST { cost -= v; } i += 4; } } } } // 4. Temporary cost modifiers for (cond, amount) in &self.players[p_idx].cost_modifiers { let ctx = AbilityContext { source_card_id: card_id as i16, player_id: p_idx as u8, area_idx: slot_idx as i16, ..Default::default() }; if self.check_condition(db, p_idx, cond, &ctx) { cost += amount; } } cost.max(0) } pub fn get_context_card_id(&self, ctx: &AbilityContext) -> Option { if ctx.source_card_id >= 0 { return Some(ctx.source_card_id as i32); } if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { let cid = self.players[ctx.player_id as usize].stage[ctx.area_idx as usize]; if cid >= 0 { return Some(cid as i32); } } None } pub fn check_condition(&self, db: &CardDatabase, p_idx: usize, cond: &Condition, ctx: &AbilityContext) -> bool { let player = &self.players[p_idx]; let opponent = &self.players[1 - p_idx]; // Helper to get cid from context or source let get_cid = || { if ctx.source_card_id >= 0 { return ctx.source_card_id as i32; } if ctx.area_idx >= 0 && ctx.area_idx < 3 { return player.stage[ctx.area_idx as usize] as i32; } -1 }; let result = match cond.condition_type { ConditionType::None => true, ConditionType::Turn1 => self.turn == 1, ConditionType::CountStage => player.stage.iter().filter(|&&id| id >= 0).count() as i32 >= cond.value, ConditionType::CountHand => player.hand.len() as i32 >= cond.value, ConditionType::CountEnergy => player.energy_zone.len() as i32 >= cond.value, ConditionType::CountSuccessLive => player.success_lives.len() as i32 >= cond.value, ConditionType::Baton => { if self.prev_card_id >= 0 { if let Some(m) = db.get_member(self.prev_card_id as u16) { m.char_id as i32 == cond.value } else { false } } else { player.baton_touch_count as i32 >= cond.value } } ConditionType::CountDiscard => player.discard.len() as i32 >= cond.value, ConditionType::IsCenter => ctx.area_idx == 1, ConditionType::LifeLead => player.score > opponent.score, ConditionType::HasMember => { player.stage.iter().any(|&cid| cid as i32 == cond.value || cid as u16 == cond.attr as u16) } ConditionType::HasColor => { let color_idx = cond.attr as usize; if color_idx > 0 && color_idx < 7 { player.stage.iter().filter(|&&cid| cid >= 0).any(|&cid| { db.get_member(cid as u16).map_or(false, |m| m.hearts[color_idx] > 0) }) } else { false } } ConditionType::CountGroup => { let group_id = cond.attr as u8; let count = player.stage.iter().filter_map(|&cid| { if cid >= 0 { db.get_member(cid as u16) } else { None } }).filter(|m| m.groups.contains(&group_id)).count(); count as i32 >= cond.value } ConditionType::CostCheck => { let cid = get_cid(); if cid >= 0 { let cost = db.get_member(cid as u16).map_or(0, |m| m.cost); if cond.attr == 0 { cost as i32 <= cond.value } else { cost as i32 >= cond.value } } else { false } } ConditionType::GroupFilter => { let group_id = cond.attr as u8; if group_id > 0 { // Check self first let cid = get_cid(); if cid >= 0 { if let Some(m) = db.get_member(cid as u16) { if m.groups.contains(&group_id) { return true; } } } // Check stage player.stage.iter().filter_map(|&cid| { if cid >= 0 { db.get_member(cid as u16) } else { None } }).any(|m| m.groups.contains(&group_id)) } else { // Check for "all_revealed" filter in params if let Some(ft) = cond.params.get("filter_type").and_then(|v| v.as_str()) { if ft == "all_revealed" { // Use live_zone_revealed as a proxy for now, or check generic revealed scope if added // Actually, this condition is usually for "Cheer" checks where cards are revealed. // For now, return false if we can't track revealed cards yet. false } else { false } } else { false } } } ConditionType::ModalAnswer => cond.value == ctx.choice_index as i32, ConditionType::HandHasNoLive => { !player.hand.iter().any(|&cid| db.get_live(cid).is_some()) } ConditionType::HasLiveCard => { player.stage.iter().filter(|&&cid| cid >= 0).any(|&cid| { if let Some(l) = db.get_live(cid as u16) { if let Some(mode) = cond.params.get("mode").and_then(|v| v.as_str()) { if mode == "no_ability" { return l.abilities.is_empty(); } } true } else { false } }) } ConditionType::ScoreCompare => { let comp_type = cond.params.get("type").and_then(|v| v.as_str()).unwrap_or("score"); let comp_op = cond.params.get("comparison").and_then(|v| v.as_str()).unwrap_or("GT"); let mode = cond.params.get("mode").and_then(|v| v.as_str()).unwrap_or(""); let get_val = |p_idx_local: usize, p: &PlayerState| -> i32 { match comp_type { "score" => p.score as i32, "cost" => { let mut total = 0; for i in 0..3 { if p.stage[i] >= 0 { total += self.get_member_cost(p_idx_local, p.stage[i] as i32, i as i32, db); } } total }, _ => 0 } }; let my_val = get_val(p_idx, player); let opp_val = get_val(1 - p_idx, opponent); let final_op = if mode == "REVERSED" || mode == "reversed" { match comp_op { "GT" => "LT", "GE" => "LE", "LT" => "GT", "LE" => "GE", _ => comp_op } } else { comp_op }; match final_op { "GT" => my_val > opp_val, "GE" => my_val >= opp_val, "LT" => my_val < opp_val, "LE" => my_val <= opp_val, "EQ" => my_val == opp_val, _ => my_val > opp_val } } ConditionType::CountHearts => { let totals = self.get_total_hearts(p_idx, db); let sum: u32 = totals.iter().sum(); sum as i32 >= cond.value } ConditionType::CountBlades => { let total = self.get_total_blades(p_idx, db); total as i32 >= cond.value } ConditionType::TypeCheck => { // value: 1 = live, 0 = member if let Some(card_id) = self.get_context_card_id(ctx) { if cond.value == 1 { db.get_live(card_id as u16).is_some() } else { db.get_member(card_id as u16).is_some() } } else { false } } ConditionType::IsInDiscard => { let cid = ctx.source_card_id; if cid >= 0 { player.discard.contains(&(cid as u16)) } else { false } } _ => false, }; if cond.is_negated { !result } else { result } } pub fn check_condition_opcode(&self, db: &CardDatabase, op: i32, val: i32, attr: i32, slot: i32, ctx: &AbilityContext) -> bool { let p_idx = self.current_player as usize; let player = &self.players[p_idx]; let opponent = &self.players[1 - p_idx]; let get_cid = || { if ctx.source_card_id >= 0 { return ctx.source_card_id as i32; } if ctx.area_idx >= 0 && ctx.area_idx < 3 { return player.stage[ctx.area_idx as usize] as i32; } -1 }; match op { C_TR1 => self.turn == 1, C_STG => player.stage.iter().filter(|&&id| id >= 0).count() as i32 >= val, C_HND => player.hand.len() as i32 >= val, C_ENR => player.energy_zone.len() as i32 >= val, C_SCS_LIV => player.success_lives.len() as i32 >= val, C_DSR => player.discard.len() as i32 >= val, C_CTR => ctx.area_idx == 1, C_LLD => player.score > opponent.score, C_HAS_MEMBER => player.stage.iter().any(|&cid| cid as i32 == val || cid as i32 == attr), // attr often 0 or unit id C_CLR => { let color_idx = attr as usize; if color_idx > 0 && color_idx < 7 { player.stage.iter().filter(|&&cid| cid >= 0).any(|&cid| { db.get_member(cid as u16).map_or(false, |m| m.hearts[color_idx] > 0) }) } else { false } }, C_GRP => { let group_id = attr as u8; let count = player.stage.iter().filter_map(|&cid| { if cid >= 0 { db.get_member(cid as u16) } else { None } }).filter(|m| m.groups.contains(&group_id)).count(); count as i32 >= val }, C_GRP_FLT => { let group_id = attr as u8; let cid = get_cid(); if cid >= 0 { if let Some(m) = db.get_member(cid as u16) { m.groups.contains(&group_id) } else { false } } else { false } }, C_BATON => { if self.prev_card_id >= 0 { if let Some(m) = db.get_member(self.prev_card_id as u16) { m.char_id as i32 == val } else { false } } else { player.baton_touch_count as i32 >= val } }, C_MODAL_ANSWER => val == ctx.choice_index as i32, C_HND_NO_LIVE => !player.hand.iter().any(|&cid| db.get_live(cid).is_some()), C_CMP => { // attr: 0=score, 1=cost // slot >> 4: 0=GE, 1=LE, 2=GT, 3=LT, 4=EQ let comp_op = (slot >> 4) & 0x0F; let get_val = |p: &PlayerState| -> i32 { if attr == 0 { p.score as i32 } else if attr == 1 { // Simple cost sum let mut total = 0; for i in 0..3 { if p.stage[i] >= 0 { // Raw cost usually sufficient for comparisons if let Some(m) = db.get_member(p.stage[i] as u16) { total += m.cost as i32; } } } total } else { 0 } }; let my_val = get_val(player); let opp_val = get_val(opponent); match comp_op { 0 => my_val >= opp_val, // GE 1 => my_val <= opp_val, // LE 2 => my_val > opp_val, // GT 3 => my_val < opp_val, // LT 4 => my_val == opp_val, // EQ _ => my_val > opp_val // Default GT } }, C_HRT => { let totals = self.get_total_hearts(p_idx, db); let sum: u32 = totals.iter().sum(); sum as i32 >= val }, C_BLD => { let total = self.get_total_blades(p_idx, db); total as i32 >= val }, C_TYPE_CHECK => { // val: 1=live, 0=member if let Some(card_id) = self.get_context_card_id(ctx) { if val == 1 { db.get_live(card_id as u16).is_some() } else { db.get_member(card_id as u16).is_some() } } else { false } }, C_IS_IN_DISCARD => { let cid = ctx.source_card_id; if cid >= 0 { player.discard.contains(&(cid as u16)) } else { false } }, _ => true // Default to true if not strictly implemented } } pub fn check_cost(&self, p_idx: usize, cost: &Cost, ctx: &AbilityContext) -> bool { let player = &self.players[p_idx]; let val = cost.value as usize; match cost.cost_type { AbilityCostType::None => true, AbilityCostType::Energy => { let available = player.tapped_energy.iter().copied().filter(|&t| !t).count() as i32; available >= cost.value }, AbilityCostType::TapSelf => { if ctx.area_idx >= 0 { !player.tapped_members[ctx.area_idx as usize] } else { false } }, AbilityCostType::TapMember => { // Check if there are enough untapped members to tap let untapped_count = player.tapped_members.iter().filter(|&&t| !t).count(); untapped_count >= val }, AbilityCostType::TapEnergy => { let untapped_energy = player.tapped_energy.iter().filter(|&&t| !t).count(); untapped_energy >= val }, AbilityCostType::DiscardHand => { player.hand.len() >= val }, AbilityCostType::SacrificeSelf => { if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { player.stage[ctx.area_idx as usize] >= 0 } else { false } }, AbilityCostType::RevealHand => { player.hand.len() >= val }, AbilityCostType::SacrificeUnder => { if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { player.stage_energy[ctx.area_idx as usize].len() >= val } else { false } }, AbilityCostType::DiscardEnergy => { player.energy_zone.len() >= val }, AbilityCostType::ReturnMemberToDeck => { if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { player.stage[ctx.area_idx as usize] >= 0 } else { false } }, AbilityCostType::ReturnDiscardToDeck => { player.discard.len() >= val }, _ => true // Assume other costs executable for validation for now } } pub fn pay_cost(&mut self, db: &CardDatabase, p_idx: usize, cost: &Cost, ctx: &AbilityContext) -> bool { match cost.cost_type { AbilityCostType::None => true, AbilityCostType::Energy => { let untap_indices: Vec = self.players[p_idx].tapped_energy.iter().copied().enumerate() .filter(|item| !item.1).map(|(i, _)| i).take(cost.value as usize).collect(); if untap_indices.len() < cost.value as usize { return false; } for idx in untap_indices { self.players[p_idx].tapped_energy[idx] = true; } true } AbilityCostType::TapSelf => { if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { self.players[p_idx].tapped_members[ctx.area_idx as usize] = true; true } else { false } }, AbilityCostType::TapMember => { let player = &mut self.players[p_idx]; let mut needed = cost.value as usize; for i in 0..3 { if !player.tapped_members[i] && player.stage[i] >= 0 { player.tapped_members[i] = true; needed -= 1; if needed == 0 { break; } } } needed == 0 }, AbilityCostType::TapEnergy => { let player = &mut self.players[p_idx]; let mut needed = cost.value as usize; for i in 0..player.tapped_energy.len() { if !player.tapped_energy[i] { player.tapped_energy[i] = true; needed -= 1; if needed == 0 { break; } } } needed == 0 }, AbilityCostType::DiscardHand => { let player = &mut self.players[p_idx]; let count = cost.value as usize; if player.hand.len() < count { return false; } for _ in 0..count { if let Some(cid) = player.hand.pop() { player.discard.push(cid); } } true }, AbilityCostType::SacrificeSelf => { if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { let slot = ctx.area_idx as usize; let cid = self.players[p_idx].stage[slot]; if cid >= 0 { // Dispatch OnLeaves trigger - No mutable borrow of player active here let mut leave_ctx = ctx.clone(); leave_ctx.source_card_id = cid; leave_ctx.area_idx = ctx.area_idx; self.trigger_abilities(db, TriggerType::OnLeaves, &leave_ctx); // Now re-borrow player to finish state update let player = &mut self.players[p_idx]; player.stage[slot] = -1; player.discard.push(cid as u16); let under_cards = std::mem::take(&mut player.stage_energy[slot]); player.discard.extend(under_cards); player.stage_energy_count[slot] = 0; true } else { false } } else { false } }, AbilityCostType::RevealHand => true, AbilityCostType::SacrificeUnder => { if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { let player = &mut self.players[p_idx]; let count = cost.value as usize; let slot = ctx.area_idx as usize; if player.stage_energy[slot].len() < count { return false; } for _ in 0..count { if let Some(cid) = player.stage_energy[slot].pop() { player.discard.push(cid); } } player.stage_energy_count[slot] = player.stage_energy[slot].len() as u8; true } else { false } }, AbilityCostType::DiscardEnergy => { let player = &mut self.players[p_idx]; let count = cost.value as usize; if player.energy_zone.len() < count { return false; } for _ in 0..count { if let Some(cid) = player.energy_zone.pop() { player.discard.push(cid); } } true }, AbilityCostType::ReturnMemberToDeck => { if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { let slot = ctx.area_idx as usize; let cid = self.players[p_idx].stage[slot]; if cid >= 0 { // Dispatch OnLeaves trigger let mut leave_ctx = ctx.clone(); leave_ctx.source_card_id = cid; leave_ctx.area_idx = slot as i16; self.trigger_abilities(db, TriggerType::OnLeaves, &leave_ctx); let player = &mut self.players[p_idx]; player.stage[slot] = -1; player.deck.insert(0, cid as u16); true } else { false } } else { false } }, AbilityCostType::ReturnDiscardToDeck => { let player = &mut self.players[p_idx]; let count = cost.value as usize; if player.discard.len() < count { return false; } for _ in 0..count { if let Some(cid) = player.discard.pop() { player.deck.push(cid); } } true }, _ => false, } } // Optimization 1: Fixed-size stack pub fn resolve_bytecode(&mut self, db: &CardDatabase, bytecode: &[i32], ctx_in: &AbilityContext) { // FAST PATH: Try hardcoded native implementation first if ctx_in.program_counter == 0 { if crate::core::hardcoded::execute_hardcoded_ability(self, db, ctx_in.source_card_id as i32, ctx_in.ability_index as usize, ctx_in) { return; } } const MAX_DEPTH: usize = 8; // Stack stores (bytecode, ip, context) let mut stack: [(&[i32], usize, AbilityContext); MAX_DEPTH] = [(&[], 0, AbilityContext::default()); MAX_DEPTH]; let mut sp = 0; stack[sp] = (bytecode, ctx_in.program_counter as usize, *ctx_in); sp += 1; let mut cond = true; let mut steps = 0; while sp > 0 { sp -= 1; let (cur_bc, mut ip, mut ctx) = stack[sp]; let len = cur_bc.len(); while ip < len && steps < 1000 { steps += 1; if ip + 3 >= len { break; } let op = cur_bc[ip]; let v = cur_bc[ip+1]; let a = cur_bc[ip+2]; let s = cur_bc[ip+3]; ip += 4; // Stop execution if we paused for input if self.phase == Phase::Response { break; } if op == 0 { continue; } if op == O_RETURN { break; } let mut is_negated = false; let mut real_op = op; if real_op >= 1000 { is_negated = true; real_op -= 1000; } let mut target_slot = s; if target_slot == 10 { target_slot = ctx.target_slot as i32; } // Handle Condition Opcodes (200-255) if real_op >= 200 && real_op <= 255 { let passed = self.check_condition_opcode(db, real_op, v, a, target_slot, &ctx); cond = if is_negated { !passed } else { passed }; continue; } if real_op == O_JUMP { ip = (ip as isize + v as isize * 4) as usize; continue; } if real_op == O_JUMP_F { if !cond { ip = (ip as isize + v as isize * 4) as usize; } continue; } if real_op == O_SELECT_MODE { if ctx.choice_index == -1 { self.phase = Phase::Response; let mut p_ctx = ctx.clone(); p_ctx.program_counter = (ip as u16).saturating_sub(4); // Re-execute SELECT_MODE on resumption self.pending_ctx = Some(p_ctx); self.pending_card_id = ctx.source_card_id; self.pending_ab_idx = ctx.ability_index; self.pending_effect_opcode = O_SELECT_MODE as i16; return; } let choice = ctx.choice_index as i32; let mut jumped = false; if choice >= 0 && choice < v { let jump_ip = (ip / 4) + 1 + choice as usize; if jump_ip * 4 + 1 < len { let offset = cur_bc[jump_ip * 4 + 1]; let new_ip = (jump_ip + offset as usize) * 4; if new_ip < len { ip = new_ip; jumped = true; } } } if !jumped { ip = ip + (v as usize + 1) * 4; } continue; } if real_op >= 200 { let target_slot_cond = s & 0x0F; // Condition Logic Copied/Reused let check = match real_op { C_TR1 => self.turn == 1, C_HAS_MEMBER => { let member_id = a as u16; self.players[ctx.player_id as usize].stage.iter().filter(|&&cid| { if cid < 0 { return false; } cid as u16 == member_id }).count() as i32 >= v }, C_CLR => { let color_idx = a as usize; if color_idx > 0 && color_idx < 7 { self.players[ctx.player_id as usize].stage.iter().filter(|&&cid| cid >= 0).any(|&cid| { db.get_member(cid as u16).map_or(false, |m| m.hearts[color_idx] > 0) }) } else { false } }, C_STG => self.players[ctx.player_id as usize].stage.iter().filter(|&&c| c >= 0).count() as i32 >= v, C_HND => self.players[ctx.player_id as usize].hand.len() as i32 >= v, C_DSR => self.players[ctx.player_id as usize].discard.len() as i32 >= v, C_CTR => ctx.area_idx == 1, C_LLD => { let p = &self.players[ctx.player_id as usize]; let o = &self.players[1 - ctx.player_id as usize]; p.score > o.score }, C_GRP => { let group_id = a as u8; let p = &self.players[ctx.player_id as usize]; let count = if target_slot_cond == 1 { p.live_zone.iter().filter_map(|&cid| { if cid >= 0 { db.get_live(cid as u16) } else { None } }).filter(|l| l.groups.contains(&group_id)).count() } else { p.stage.iter().filter_map(|&cid| { if cid >= 0 { db.get_member(cid as u16) } else { None } }).filter(|m| m.groups.contains(&group_id)).count() }; count as i32 >= v }, C_GRP_FLT => { let group_id = a as u8; let p = &self.players[ctx.player_id as usize]; let count = if target_slot_cond == 1 { p.live_zone.iter().filter_map(|&cid| { if cid >= 0 { db.get_live(cid as u16) } else { None } }).filter(|l| l.groups.contains(&group_id)).count() } else { p.stage.iter().filter_map(|&cid| { if cid >= 0 { db.get_member(cid as u16) } else { None } }).filter(|m| m.groups.contains(&group_id)).count() }; count as i32 >= v }, C_OPH => self.players[1 - ctx.player_id as usize].stage.iter().filter(|&&c| c >= 0).count() as i32 >= v, C_SLF_GRP => { if ctx.area_idx >= 0 { let cid = self.players[ctx.player_id as usize].stage[ctx.area_idx as usize]; if cid >= 0 { db.get_member(cid as u16).map_or(false, |m| m.groups.contains(&(a as u8))) } else { false } } else { false } }, C_MODAL_ANSWER => ctx.choice_index == v as i16, C_ENR => self.players[ctx.player_id as usize].energy_zone.len() as i32 >= v, C_HAS_LIVE => self.players[ctx.player_id as usize].live_zone.iter().any(|&c| c >= 0), C_SCS_LIV => self.players[ctx.player_id as usize].success_lives.len() as i32 >= v, C_OPP_ENR_DIF => { let opp = &self.players[1 - ctx.player_id as usize]; let p = &self.players[ctx.player_id as usize]; (opp.energy_zone.len() as i32 - p.energy_zone.len() as i32) >= v }, C_CMP => { let p_score = self.players[ctx.player_id as usize].score; let o_score = self.players[1 - ctx.player_id as usize].score; if v > 0 { p_score as i32 >= v } else { p_score > o_score } }, C_HAS_CHOICE => true, C_OPPONENT_CHOICE => true, C_HRT => { let mut total = 0; let p = &self.players[ctx.player_id as usize]; for i in 0..3 { if p.stage[i] >= 0 { let mut h = 0; if let Some(m) = db.get_member(p.stage[i] as u16) { h += m.hearts.iter().sum::() as i32; } h += p.heart_buffs[i].iter().sum::(); total += h; } } total >= v }, C_BLD => { let mut total = 0; let p = &self.players[ctx.player_id as usize]; for i in 0..3 { if p.stage[i] >= 0 { let mut b = 0; if let Some(m) = db.get_member(p.stage[i] as u16) { b += m.blades as i32; } b += p.blade_buffs[i]; total += b; } } total >= v }, C_DK_REFR => self.players[ctx.player_id as usize].deck_refreshed_this_turn, C_HAS_MOVED => { if ctx.area_idx >= 0 && (ctx.area_idx as usize) < 3 { self.players[ctx.player_id as usize].moved_members_this_turn[ctx.area_idx as usize] } else { false } }, C_BATON => { if self.prev_card_id >= 0 { if let Some(m) = db.get_member(self.prev_card_id as u16) { m.char_id as i32 == v } else { false } } else { // Default to checking baton count if no prev card context (e.g. static check) self.players[ctx.player_id as usize].baton_touch_count as i32 >= v } }, C_TYPE_CHECK => { if let Some(card_id) = self.get_context_card_id(&ctx) { if v == 1 { db.get_live(card_id as u16).is_some() } else { db.get_member(card_id as u16).is_some() } } else { false } }, C_HND_INC => self.players[ctx.player_id as usize].hand_increased_this_turn as i32 >= v, C_LIV_ZN => self.players[ctx.player_id as usize].live_zone.iter().filter(|&&c| c >= 0).count() as i32 >= v, _ => false }; cond = if is_negated { !check } else { check }; } else if cond { let p_idx = ctx.player_id as usize; match real_op { O_DRAW => self.draw_cards(p_idx, v as u32), O_BOOST => self.players[p_idx].live_score_bonus += v, O_CHARGE => { for _ in 0..v { if let Some(cid) = self.players[p_idx].energy_deck.pop() { self.players[p_idx].energy_zone.push(cid); self.players[p_idx].tapped_energy.push(false); } } }, O_ADD_H => { if target_slot == 90 { // Source: reveal_zone (looked_cards) if !self.silent { self.log(format!(" Effect: ADD_TO_HAND from Reveal Zone ({} cards)", v)); } let count = v as usize; for _ in 0..count { if !self.players[p_idx].looked_cards.is_empty() { let cid = self.players[p_idx].looked_cards.remove(0); self.players[p_idx].hand.push(cid); self.players[p_idx].hand_increased_this_turn = self.players[p_idx].hand_increased_this_turn.saturating_add(1); if !self.silent { self.log(format!(" Added card {} to hand", cid)); } } } } else { if !self.silent { self.log(format!(" Effect: Draw {} card(s)", v)); } self.draw_cards(p_idx, v as u32) } }, O_DRAW_UNTIL => { let target_hand_size = v as usize; let current_hand_size = self.players[p_idx].hand.len(); if current_hand_size < target_hand_size { let to_draw = (target_hand_size - current_hand_size) as u32; self.draw_cards(p_idx, to_draw); } }, O_PAY_ENERGY => { let mut paid = 0; let count = v; for i in 0..self.players[p_idx].tapped_energy.len() { if paid >= count { break; } if !self.players[p_idx].tapped_energy[i] { self.players[p_idx].tapped_energy[i] = true; paid += 1; } } if !self.silent { self.log(format!(" Effect: PAID {} energy", paid)); } }, O_ACTIVATE_MEMBER => { if target_slot == 4 && ctx.area_idx >= 0 { self.players[p_idx].tapped_members[ctx.area_idx as usize] = false; } else if target_slot == 1 { for i in 0..3 { self.players[p_idx].tapped_members[i] = false; } } }, O_ACTIVATE_ENERGY => { if !self.silent { self.log(format!(" Effect: Activate {} Energy cards. State BEFORE: {:?}", v, self.players[p_idx].tapped_energy)); } let mut count = 0; for i in 0..self.players[p_idx].tapped_energy.len() { if count >= v { break; } if self.players[p_idx].tapped_energy[i] { self.players[p_idx].tapped_energy[i] = false; count += 1; } } if !self.silent { self.log(format!(" Effect: Activate Energy DONE. Count={} State AFTER: {:?}", count, self.players[p_idx].tapped_energy)); } }, O_BLADES => { if target_slot == 4 && ctx.area_idx >= 0 { let t = ctx.area_idx as usize; self.players[p_idx].blade_buffs[t] += v; if !self.silent { self.log(format!(" Effect: +{} Blades to slot {}", v, t)); } } else if target_slot == 1 { for t in 0..3 { self.players[p_idx].blade_buffs[t] += v; } if !self.silent { self.log(format!(" Effect: +{} Blades to all slots", v)); } } }, O_HEARTS => { let mut color = a as usize; if color == 0 { color = ctx.selected_color as usize; } if color < 7 { if target_slot == 4 && ctx.area_idx >= 0 { let t = ctx.area_idx as usize; self.players[p_idx].heart_buffs[t][color] += v as i32; if !self.silent { self.log(format!(" Effect: Gain {} {:?} Heart(s) in slot {}", v, color, t)); } } else if target_slot == 1 { for t in 0..3 { self.players[p_idx].heart_buffs[t][color] += v as i32; } if !self.silent { self.log(format!(" Effect: Gain {} {:?} Heart(s) in all slots", v, color)); } } } }, O_COLOR_SELECT => { if ctx.choice_index == -1 { self.phase = Phase::Response; let mut p_ctx = ctx.clone(); p_ctx.program_counter = (ip as u16).saturating_sub(4); self.pending_ctx = Some(p_ctx); self.pending_card_id = ctx.source_card_id; self.pending_ab_idx = ctx.ability_index; self.pending_effect_opcode = O_COLOR_SELECT as i16; return; } ctx.selected_color = ctx.choice_index; }, O_REDUCE_COST => self.players[p_idx].cost_reduction += v, O_RECOV_L => { for _ in 0..v { if let Some(idx) = self.players[p_idx].discard.iter().position(|&cid| db.get_live(cid).is_some()) { let cid = self.players[p_idx].discard.remove(idx); self.players[p_idx].hand.push(cid); self.players[p_idx].hand_increased_this_turn = self.players[p_idx].hand_increased_this_turn.saturating_add(1); } } }, O_RECOV_M => { for _ in 0..v { if let Some(idx) = self.players[p_idx].discard.iter().position(|&cid| db.get_member(cid).is_some()) { let cid = self.players[p_idx].discard.remove(idx); self.players[p_idx].hand.push(cid); self.players[p_idx].hand_increased_this_turn = self.players[p_idx].hand_increased_this_turn.saturating_add(1); } } }, O_SET_BLADES => { if target_slot == 4 && ctx.area_idx >= 0 { self.players[p_idx].blade_buffs[ctx.area_idx as usize] = v; } }, O_TAP_O => { if ctx.choice_index == -1 { self.phase = Phase::Response; let mut p_ctx = ctx.clone(); p_ctx.program_counter = (ip as u16).saturating_sub(4); self.pending_ctx = Some(p_ctx); self.pending_card_id = ctx.source_card_id; self.pending_ab_idx = ctx.ability_index; self.pending_effect_opcode = O_TAP_O as i16; return; } let o_idx = 1 - p_idx; let slot = ctx.choice_index as usize; if slot < 3 { self.players[o_idx].tapped_members[slot] = true; } }, O_TAP_M => { let slot = a as usize; if slot < 3 { self.players[p_idx].tapped_members[slot] = true; } }, O_SET_SCORE => { self.players[p_idx].score = v as u32; }, O_LOOK_AND_CHOOSE => { let count = v as usize; if self.players[p_idx].looked_cards.is_empty() { // Bits 12-15 of attr = Source Zone (6=Hand, 7=Discard, 8=Deck) let src_bits = (a >> 12) & 0x0F; let source_zone = if src_bits != 0 { src_bits as i32 } else { target_slot }; let deck_len = self.players[p_idx].deck.len(); let hand_len = self.players[p_idx].hand.len(); let discard_len = self.players[p_idx].discard.len(); match source_zone { 6 => { let actual = count.min(hand_len); for _ in 0..actual { if let Some(cid) = self.players[p_idx].hand.pop() { self.players[p_idx].looked_cards.push(cid); } } }, 7 => { let actual = count.min(discard_len); for _ in 0..actual { if let Some(cid) = self.players[p_idx].discard.pop() { self.players[p_idx].looked_cards.push(cid); } } }, _ => { let actual = count.min(deck_len); for _ in 0..actual { if let Some(cid) = self.players[p_idx].deck.pop() { self.players[p_idx].looked_cards.push(cid); } } } } if !self.silent { self.log(format!("Effect: LOOK_AND_CHOOSE - Populated {} cards from source {}", self.players[p_idx].looked_cards.len(), source_zone)); } } if ctx.choice_index == -1 { self.phase = Phase::Response; let mut p_ctx = ctx.clone(); p_ctx.program_counter = (ip as u16).saturating_sub(4); self.pending_ctx = Some(p_ctx); self.pending_card_id = ctx.source_card_id; self.pending_ab_idx = ctx.ability_index; self.pending_effect_opcode = O_LOOK_AND_CHOOSE as i16; self.pending_choice_type = "LOOK_AND_CHOOSE".to_string(); return; } let choice = ctx.choice_index; let mut revealed = std::mem::take(&mut self.players[p_idx].looked_cards); if choice >= 0 && (choice as usize) < revealed.len() { let chosen = revealed.remove(choice as usize); // destination based on 'a' or 'target_slot' // For LOOK_AND_CHOOSE, 'a' often encodes destination if 's' was source let destination = if a == 1 { 7 } else if a == 30 { 6 } else { target_slot }; match destination { 7 => { self.players[p_idx].discard.push(chosen); } 8 => { self.players[p_idx].deck.push(chosen); } 6 => { self.players[p_idx].hand.push(chosen); self.players[p_idx].hand_increased_this_turn = self.players[p_idx].hand_increased_this_turn.saturating_add(1); if !self.silent { self.log(format!(" Choice: Added card {} to hand", chosen)); } } _ => { self.players[p_idx].hand.push(chosen); self.players[p_idx].hand_increased_this_turn = self.players[p_idx].hand_increased_this_turn.saturating_add(1); } } } else { if !self.silent { self.log(" Choice: Passed (None selected)".to_string()); } } let dest_discard = (a & 0x01) != 0; if dest_discard { self.players[p_idx].discard.extend(revealed); } else { self.players[p_idx].deck.extend(revealed); } }, O_SEARCH_DECK => { let search_target = ctx.target_slot as usize; if search_target < self.players[p_idx].deck.len() { let cid = self.players[p_idx].deck.remove(search_target); self.players[p_idx].hand.push(cid); self.players[p_idx].hand_increased_this_turn = self.players[p_idx].hand_increased_this_turn.saturating_add(1); let mut rng = Pcg64::from_os_rng(); self.players[p_idx].deck.shuffle(&mut rng); } }, O_MOVE_TO_DECK => { let count = v as usize; for _ in 0..count { if a == 1 { if let Some(cid) = self.players[p_idx].discard.pop() { self.players[p_idx].deck.push(cid); } } else { if let Some(cid) = self.players[p_idx].hand.pop() { self.players[p_idx].deck.push(cid); } } } let mut rng = Pcg64::from_os_rng(); self.players[p_idx].deck.shuffle(&mut rng); }, O_ORDER_DECK => { // LOOK_AND_CHOOSE_ORDER: Look at top N, choose order, discard rest let count = v as usize; let deck_len = self.players[p_idx].deck.len(); // Step 1: Populate looked_cards if empty if self.players[p_idx].looked_cards.is_empty() { if deck_len > 0 && count > 0 { let actual_count = count.min(deck_len); // Remove cards from deck and store in looked_cards for _ in 0..actual_count { if let Some(cid) = self.players[p_idx].deck.pop() { self.players[p_idx].looked_cards.push(cid); } } if !self.silent { self.log(format!(" Effect: ORDER_DECK - Looking at {} cards", actual_count)); } } } // Step 2: Handle Selection or Pause if !self.players[p_idx].looked_cards.is_empty() { if ctx.choice_index == -1 { // Initial Pause self.phase = Phase::Response; let mut p_ctx = ctx.clone(); p_ctx.program_counter = (ip as u16).saturating_sub(4); self.pending_ctx = Some(p_ctx); self.pending_card_id = ctx.source_card_id; self.pending_ab_idx = ctx.ability_index; self.pending_effect_opcode = O_ORDER_DECK as i16; self.pending_choice_type = "ORDER_DECK".to_string(); return; } let choice = ctx.choice_index; let looked_len = self.players[p_idx].looked_cards.len(); if choice >= 0 && (choice as usize) < looked_len { // Chosen a card to put on TOP of deck let cid = self.players[p_idx].looked_cards.remove(choice as usize); self.players[p_idx].deck.push(cid); if !self.silent { self.log(format!(" Effect: ORDER_DECK - Placed card on top. {} remaining.", self.players[p_idx].looked_cards.len())); } // If more cards remain, PAUSE AGAIN (loop) if !self.players[p_idx].looked_cards.is_empty() { // Pausing again for next card self.phase = Phase::Response; // Important: Need to update the context stored in pending to NOT have a choice yet let mut new_ctx = ctx.clone(); new_ctx.choice_index = -1; new_ctx.program_counter = (ip as u16).saturating_sub(4); // Stay on O_ORDER_DECK self.pending_ctx = Some(new_ctx); self.pending_card_id = ctx.source_card_id; self.pending_ab_idx = ctx.ability_index; self.pending_effect_opcode = O_ORDER_DECK as i16; self.pending_choice_type = "ORDER_DECK".to_string(); return; } } else { // "Done" or Invalid choice implies handling remaining cards based on remainder mode // Attr (ip-2): 0=Discard, 1=DeckTop, 2=DeckBottom let remainder_mode = a as u8; if remainder_mode == 1 { // DeckTop if !self.silent { self.log(format!(" Effect: ORDER_DECK - Returning remaining {} cards to TOP of deck", looked_len)); } for cid in self.players[p_idx].looked_cards.drain(..) { self.players[p_idx].deck.push(cid); } } else if remainder_mode == 2 { // DeckBottom if !self.silent { self.log(format!(" Effect: ORDER_DECK - Returning remaining {} cards to BOTTOM of deck", looked_len)); } // Insert at index 0 for cid in self.players[p_idx].looked_cards.drain(..) { self.players[p_idx].deck.insert(0, cid); } } else { // Discard (0 or default) if !self.silent { self.log(format!(" Effect: ORDER_DECK - Discarding remaining {} cards", looked_len)); } for cid in self.players[p_idx].looked_cards.drain(..) { self.players[p_idx].discard.push(cid); } } } } }, O_SWAP_CARDS => { // v = count of cards to move // s = destination zone (from TargetType enum): // CARD_HAND=6, CARD_DISCARD=7, CARD_DECK_TOP=8 // Source is deck top (most common for "looked" cards) let count = v as usize; let dest = target_slot; // target_slot holds the TargetType enum value if !self.silent { self.log(format!(" Effect: SWAP_CARDS {} cards, dest={}", count, dest)); } for _ in 0..count { if let Some(cid) = self.players[p_idx].deck.pop() { match dest { 7 => { // TargetType.CARD_DISCARD = 7 self.players[p_idx].discard.push(cid); }, 8 => { // TargetType.CARD_DECK_TOP = 8 self.players[p_idx].deck.push(cid); }, 6 => { // TargetType.CARD_HAND = 6 self.players[p_idx].hand.push(cid); self.players[p_idx].hand_increased_this_turn = self.players[p_idx].hand_increased_this_turn.saturating_add(1); }, _ => { // Default: put in discard (most common destination) self.players[p_idx].discard.push(cid); } } } } }, O_META_RULE => { // a=0 -> cheer_mod (most common) // We store it as string to match Python or just use an enum later if a == 0 { // meta_rules removed as it was unused logic // self.players[p_idx].meta_rules.push("cheer_mod".to_string()); } }, O_BATON_MOD => { self.players[p_idx].baton_touch_limit = v as u8; }, O_SELECT_MODE => { // Struct: [SELECT_MODE, NumOptions, 0, 0] // Followed by NumOptions * [JUMP, Offset, 0, 0] let choice = ctx.choice_index.max(0) as usize; let num_options = v as usize; if choice < num_options { // Skip "choice" jump instructions (each 4 ints) ip += choice * 4; } else { // Fallback: Pick last or 0? // Ideally should not happen if choice is validated. // We consume the jump table if we do nothing, but here we MUST pick one path. // If we don't jump, we execute the first JUMP (Option 0). // So doing nothing = Select Option 0. } }, O_TRIGGER_REMOTE => { let target_cid = if target_slot >= 0 && target_slot < 3 { self.players[p_idx].stage[target_slot as usize] } else { -1 }; if target_cid >= 0 { if let Some(m) = db.get_member(target_cid as u16) { let ab_idx = v as usize; if ab_idx < m.abilities.len() { if sp < MAX_DEPTH { // Push current state to return to stack[sp] = (cur_bc, ip, ctx); sp += 1; // Start new execution frame for chosen ability stack[sp] = (&m.abilities[ab_idx].bytecode, 0, ctx); sp += 1; // Break from current frame loop to start new frame break; } } } } }, O_PLAY_MEMBER_FROM_HAND => { let hand_idx = ctx.choice_index as usize; let slot_idx = ctx.target_slot as usize; if hand_idx < self.players[p_idx].hand.len() && slot_idx < 3 { let card_id = self.players[p_idx].hand[hand_idx]; if db.get_member(card_id as u16).is_none() { if !self.silent { self.log(format!("[ERROR] Invalid card type for Stage (ID: {})", card_id)); } continue; } self.players[p_idx].hand.remove(hand_idx); let old_card_id = self.players[p_idx].stage[slot_idx]; if old_card_id >= 0 { // Dispatch OnLeaves trigger let mut leave_ctx = ctx.clone(); leave_ctx.source_card_id = old_card_id; leave_ctx.area_idx = slot_idx as i16; self.trigger_abilities(db, TriggerType::OnLeaves, &leave_ctx); self.players[p_idx].discard.push(old_card_id as u16); } self.players[p_idx].stage[slot_idx] = card_id as i16; self.players[p_idx].tapped_members[slot_idx] = false; self.players[p_idx].moved_members_this_turn[slot_idx] = true; if !self.silent { self.log(format!("Effect: Play card to Slot {}", slot_idx)); } let new_ctx = AbilityContext { source_card_id: card_id as i16, player_id: p_idx as u8, area_idx: slot_idx as i16, ..Default::default() }; self.trigger_abilities(db, TriggerType::OnPlay, &new_ctx); } }, O_PLAY_MEMBER_FROM_DISCARD => { if (target_slot == 4 || target_slot == -1) && ctx.choice_index == -1 { // Pause for input: Select Stage Slot self.phase = Phase::Response; let mut p_ctx = ctx.clone(); p_ctx.program_counter = (ip as u16).saturating_sub(4); // Rewind to retry this instruction self.pending_ctx = Some(p_ctx); self.pending_card_id = ctx.source_card_id; self.pending_ab_idx = ctx.ability_index; self.pending_effect_opcode = O_PLAY_MEMBER_FROM_DISCARD as i16; self.pending_choice_type = "SELECT_STAGE".to_string(); return; } let slot_idx = if target_slot == 4 || target_slot == -1 { ctx.choice_index as usize } else { target_slot as usize }; let card_id = if v == 1 { ctx.source_card_id } else { ctx.choice_index }; if slot_idx < 3 { if card_id < 0 { if !self.silent { self.log(format!("[ERROR] Invalid card ID for Stage (ID: {})", card_id)); } continue; } // Check if card is in discard if let Some(pos) = self.players[p_idx].discard.iter().position(|&cid| cid == card_id as u16) { self.players[p_idx].discard.remove(pos); let old_card_id = self.players[p_idx].stage[slot_idx]; if old_card_id >= 0 { // Dispatch OnLeaves trigger let mut leave_ctx = ctx.clone(); leave_ctx.source_card_id = old_card_id; leave_ctx.area_idx = slot_idx as i16; self.trigger_abilities(db, TriggerType::OnLeaves, &leave_ctx); self.players[p_idx].discard.push(old_card_id as u16); } self.players[p_idx].stage[slot_idx] = card_id as i16; self.players[p_idx].tapped_members[slot_idx] = false; self.players[p_idx].moved_members_this_turn[slot_idx] = true; if !self.silent { self.log(format!("Effect: Play card (ID {}) from discard to Slot {}", card_id, slot_idx)); } let new_ctx = AbilityContext { source_card_id: card_id as i16, player_id: p_idx as u8, area_idx: slot_idx as i16, ..Default::default() }; self.trigger_abilities(db, TriggerType::OnPlay, &new_ctx); } else { if !self.silent { self.log(format!("[ERROR] Card {} not found in discard", card_id)); } } } }, O_PLAY_LIVE_FROM_DISCARD => { if (target_slot == 4 || target_slot == -1) && ctx.choice_index == -1 { // Pause for input: Select Live Slot self.phase = Phase::Response; let mut p_ctx = ctx.clone(); p_ctx.program_counter = (ip as u16).saturating_sub(4); // Rewind to retry this instruction self.pending_ctx = Some(p_ctx); self.pending_card_id = ctx.source_card_id; self.pending_ab_idx = ctx.ability_index; self.pending_effect_opcode = O_PLAY_LIVE_FROM_DISCARD as i16; self.pending_choice_type = "SELECT_LIVE_SLOT".to_string(); return; } let slot_idx = if target_slot == 4 || target_slot == -1 { ctx.choice_index as usize } else { target_slot as usize }; let card_id = if v == 1 { ctx.source_card_id } else { ctx.choice_index }; if card_id < 0 { if !self.silent { self.log(format!("[ERROR] Invalid card ID for Live (ID: {})", card_id)); } continue; } // Check if card is in discard if let Some(pos) = self.players[p_idx].discard.iter().position(|&cid| cid == card_id as u16) { if slot_idx < 10 { self.players[p_idx].discard.remove(pos); let old_card_id = self.players[p_idx].live_zone[slot_idx]; if old_card_id >= 0 { // If occupied, move old live card to discard self.players[p_idx].discard.push(old_card_id as u16); } self.players[p_idx].live_zone[slot_idx] = card_id as i16; self.players[p_idx].live_zone_revealed[slot_idx] = true; // Played from discard is visible self.log(format!("Effect: Play Live card (ID {}) from discard to Live Zone slot {}", card_id, slot_idx)); } else { self.log(format!("[ERROR] Invalid live slot {}", slot_idx)); } } else { self.log(format!("[ERROR] Live Card {} not found in discard", card_id)); } } O_ADD_STAGE_ENERGY => { let slot = if target_slot == 4 && ctx.area_idx >= 0 { ctx.area_idx as usize } else { target_slot as usize }; if slot < 3 { for _ in 0..v { self.players[p_idx].stage_energy[slot].push(2000); } self.players[p_idx].stage_energy_count[slot] = self.players[p_idx].stage_energy[slot].len() as u8; } }, O_SET_TAPPED => { let slot = if target_slot == 4 && ctx.area_idx >= 0 { ctx.area_idx as usize } else { target_slot as usize }; if slot < 3 { self.players[p_idx].tapped_members[slot] = v != 0; } }, O_MOVE_MEMBER => { let src_slot = if ctx.area_idx >= 0 { ctx.area_idx as usize } else { 0 }; let dst_slot = ctx.target_slot as usize; if src_slot < 3 && dst_slot < 3 && src_slot != dst_slot { self.players[p_idx].stage.swap(src_slot, dst_slot); self.players[p_idx].tapped_members.swap(src_slot, dst_slot); self.players[p_idx].stage_energy_count.swap(src_slot, dst_slot); self.players[p_idx].stage_energy.swap(src_slot, dst_slot); self.players[p_idx].moved_members_this_turn[dst_slot] = true; self.players[p_idx].moved_members_this_turn[src_slot] = true; // Dispatch OnPositionChange triggers for &slot in &[src_slot, dst_slot] { let cid = self.players[p_idx].stage[slot]; if cid >= 0 { let mut pos_ctx = ctx.clone(); pos_ctx.source_card_id = cid; pos_ctx.area_idx = slot as i16; self.trigger_abilities(db, TriggerType::OnPositionChange, &pos_ctx); } } } }, O_PLACE_UNDER => { let slot = ctx.target_slot as usize; if slot < 3 { if a == 0 && !self.players[p_idx].hand.is_empty() { if let Some(cid) = self.players[p_idx].hand.pop() { self.players[p_idx].stage_energy[slot].push(cid); self.players[p_idx].stage_energy_count[slot] = self.players[p_idx].stage_energy[slot].len() as u8; } } else if a == 1 { if let Some(cid) = self.players[p_idx].energy_zone.pop() { self.players[p_idx].tapped_energy.pop(); self.players[p_idx].stage_energy[slot].push(cid); self.players[p_idx].stage_energy_count[slot] = self.players[p_idx].stage_energy[slot].len() as u8; } } } }, O_BUFF => { let targets = if target_slot == 4 && ctx.area_idx >= 0 { vec![ctx.area_idx as usize] } else if target_slot == 1 { vec![0, 1, 2] } else { vec![] }; for t in targets { self.players[p_idx].blade_buffs[t] += v; } }, O_TRANSFORM_COLOR => { self.players[p_idx].color_transforms.push((0, v as u8)); }, O_REDUCE_HEART_REQ => { let color = s as usize; if color < 7 { self.players[p_idx].heart_req_reductions[color] += v; } }, O_GRANT_ABILITY => { let source_cid = ctx.source_card_id; if let Some(card) = db.get_member(source_cid as u16) { // Validate ability index exists if v >= 0 && (v as usize) < card.abilities.len() { let ab_idx = v as u16; let slot_to_choose = if target_slot == 4 { ctx.area_idx } else { target_slot as i16 }; let targets = if slot_to_choose >= 0 && slot_to_choose < 3 { vec![slot_to_choose as usize] } else if target_slot == 1 { vec![0, 1, 2] } else { vec![] }; for t in targets { let target_cid = self.players[p_idx].stage[t]; if target_cid >= 0 { self.players[p_idx].granted_abilities.push((target_cid, source_cid as u16, ab_idx)); } } } } }, O_INCREASE_COST => { // Simplified: Adds a general cost modifier for now. // Conditional cost modifiers usually come from Constant abilities. self.players[p_idx].cost_modifiers.push((Condition { condition_type: ConditionType::None, value: 0, attr: 0, target_slot: 0, is_negated: false, params: serde_json::Value::Null }, v)); }, O_IMMUNITY => { self.players[p_idx].has_immunity = v != 0; }, O_ADD_CONTINUOUS => { }, O_MOVE_TO_DISCARD => { let count = v as usize; for _ in 0..count { if a == 1 { if let Some(cid) = self.players[p_idx].deck.pop() { self.players[p_idx].discard.push(cid); } } else if a == 2 { if let Some(cid) = self.players[p_idx].hand.pop() { self.players[p_idx].discard.push(cid); } } else if a == 3 { if let Some(cid) = self.players[p_idx].energy_zone.pop() { self.players[p_idx].tapped_energy.pop(); self.players[p_idx].discard.push(cid); } } } }, O_REVEAL_UNTIL => { let cond_type = v; let dest_slot = target_slot; let mut found = false; let mut revealed_count = 0; while !found { if let Some(cid) = self.players[p_idx].deck.pop() { self.players[p_idx].looked_cards.push(cid); revealed_count += 1; // Check condition based on bits in 'a' (attr) let matches = match cond_type { 33 => { // TYPE_CHECK let card_type_is_live = (a & 0x01) != 0; if card_type_is_live { db.get_live(cid as u16).is_some() } else { db.get_member(cid as u16).is_some() } }, 16 => { // COST_GE let min_cost = (a >> 1) & 0x1F; if let Some(m) = db.get_member(cid as u16) { m.cost as i32 >= min_cost as i32 } else { false } } _ => true, // Stop at 1st card if unknown }; if matches { found = true; // Move the matched card to dest if let Some(idx) = self.players[p_idx].looked_cards.iter().position(|&xcid| xcid == cid) { let matched_cid = self.players[p_idx].looked_cards.remove(idx); match dest_slot { 6 => { self.players[p_idx].hand.push(matched_cid); self.players[p_idx].hand_increased_this_turn = self.players[p_idx].hand_increased_this_turn.saturating_add(1); if !self.silent { self.log(format!(" Revealed and added card {} to hand", matched_cid)); } }, _ => { self.players[p_idx].discard.push(matched_cid); if !self.silent { self.log(format!(" Revealed and moved card {} to discard/target", matched_cid)); } } } } } } else { break; // Deck empty } } // Move rest to discard let count_rem = self.players[p_idx].looked_cards.len(); if count_rem > 0 { let rest = std::mem::take(&mut self.players[p_idx].looked_cards); if !self.silent { self.log(format!(" Discarding remaining {} revealed cards", count_rem)); } for rcid in rest { self.players[p_idx].discard.push(rcid); } } if !self.silent { self.log(format!(" Effect: REVEAL_UNTIL finished. Total revealed: {}", revealed_count)); } }, // --- Missing/Stub opcodes (log for debugging) --- O_LOOK_DECK => { let count = v as usize; // Real implementation should POP from deck to looked_cards for order/draw later self.players[p_idx].looked_cards.clear(); let deck_len = self.players[p_idx].deck.len(); if deck_len > 0 { let actual_count = count.min(deck_len); for _ in 0..actual_count { if let Some(cid) = self.players[p_idx].deck.pop() { self.players[p_idx].looked_cards.push(cid); } } if !self.silent { self.log(format!(" Effect: LOOK_DECK - Looking at {} cards", actual_count)); } } }, O_SET_HEARTS => { // Set hearts to fixed value let color = a as usize; if color < 7 { let targets = if target_slot == 4 && ctx.area_idx >= 0 { vec![ctx.area_idx as usize] } else if target_slot == 1 { vec![0, 1, 2] } else { vec![] }; for t in targets { self.players[p_idx].heart_buffs[t][color] = v as i32; } } }, O_FORMATION => { let src_slot = if target_slot == 4 { ctx.area_idx as usize } else { target_slot as usize }; let dst_slot = a as usize; if src_slot < 3 && dst_slot < 3 && src_slot != dst_slot { self.players[p_idx].stage.swap(src_slot, dst_slot); self.players[p_idx].tapped_members.swap(src_slot, dst_slot); self.players[p_idx].stage_energy_count.swap(src_slot, dst_slot); self.players[p_idx].stage_energy.swap(src_slot, dst_slot); if !self.silent { self.log(format!(" Effect: FORMATION - Swapped {} and {}", src_slot, dst_slot)); } } }, O_NEGATE => { if !self.silent { self.log(format!(" Effect: NEGATE - Suppressing trigger effects (stub)")); } }, O_SWAP_ZONE => { if !self.silent { self.log(format!(" Effect: SWAP_ZONE (stub)")); } }, O_REVEAL => { if !self.silent { self.log(format!(" Effect: REVEAL {} cards", v)); } let count = v as usize; self.players[p_idx].looked_cards.clear(); let mut revealed = Vec::new(); let deck_len = self.players[p_idx].deck.len(); if deck_len > 0 { let start = if deck_len >= count { deck_len - count } else { 0 }; for i in (start..deck_len).rev() { let cid = self.players[p_idx].deck[i]; self.players[p_idx].looked_cards.push(cid); revealed.push(cid); } } // Dispatch OnReveal triggers for cid in revealed { let mut new_ctx = ctx.clone(); new_ctx.source_card_id = cid as i16; self.trigger_abilities(db, TriggerType::OnReveal, &new_ctx); } }, O_CHEER_REVEAL => { if !self.silent { self.log(format!(" Effect: CHEER_REVEAL {} cards", v)); } let count = v as usize; self.players[p_idx].looked_cards.clear(); let mut revealed = Vec::new(); let deck_len = self.players[p_idx].deck.len(); if deck_len > 0 { let start = if deck_len >= count { deck_len - count } else { 0 }; for i in (start..deck_len).rev() { let cid = self.players[p_idx].deck[i]; self.players[p_idx].looked_cards.push(cid); revealed.push(cid); } } // Dispatch OnReveal triggers for cid in revealed { let mut new_ctx = ctx.clone(); new_ctx.source_card_id = cid as i16; self.trigger_abilities(db, TriggerType::OnReveal, &new_ctx); } }, O_REPLACE_EFFECT => { if !self.silent { self.log(format!(" Effect: REPLACE_EFFECT (stub)")); } }, O_MODIFY_SCORE_RULE => { if !self.silent { self.log(format!(" Effect: MODIFY_SCORE_RULE (stub)")); } }, _ => { #[cfg(debug_assertions)] self.log(format!(" [WARN] Unhandled opcode: {}", real_op)); } } } } } } /// Get Japanese trigger label for logging fn get_trigger_label(trigger: TriggerType) -> &'static str { match trigger { TriggerType::OnPlay => "【登場】", TriggerType::OnLiveStart => "【開始】", TriggerType::OnLiveSuccess => "【成功】", TriggerType::TurnStart => "【ターン開始】", TriggerType::TurnEnd => "【ターン終了】", TriggerType::Constant => "【常時】", TriggerType::Activated => "【起動】", TriggerType::OnLeaves => "【退場】", TriggerType::OnReveal => "【公開】", TriggerType::OnPositionChange => "【移動】", TriggerType::None => "", } } pub fn trigger_abilities(&mut self, db: &CardDatabase, trigger: TriggerType, ctx: &AbilityContext) { self.trigger_abilities_from(db, trigger, ctx, 0); } // Optimization 2: Zero-Copy pub fn trigger_abilities_from(&mut self, db: &CardDatabase, trigger: TriggerType, ctx: &AbilityContext, start_ab_idx: usize) { if self.phase == Phase::Terminal { return; } // Infinite Loop Protection if self.trigger_depth > 20 { let p_idx = self.current_player as usize; let opp_idx = 1 - p_idx; if !self.silent { self.log(format!("Rule Violation: Infinite Loop Detected for Player {}. Forfeiting game.", p_idx)); } self.players[opp_idx].score = 10000; self.players[p_idx].score = 0; self.phase = Phase::Terminal; return; } self.trigger_depth += 1; let p_idx = ctx.player_id as usize; // Store (card_id, ability_index, area_idx, is_live_card) let mut abilities_to_run: Vec<(u16, u16, i16, bool)> = Vec::new(); // 1. Collect Member Abilities from Stage for i in 0..3 { let cid = self.players[p_idx].stage[i]; if cid >= 0 { // For OnPlay, only check the slot being played to if trigger == TriggerType::OnPlay && ctx.area_idx >= 0 && i as i16 != ctx.area_idx { continue; } if let Some(card) = db.get_member(cid as u16) { for (ab_idx, ab) in card.abilities.iter().enumerate().skip(start_ab_idx as usize) { if ab.trigger == trigger { if !ab.bytecode.is_empty() { let mut actx = ctx.clone(); actx.area_idx = i as i16; if ab.conditions.iter().all(|cond| self.check_condition(db, p_idx, cond, &actx)) { if self.check_once_per_turn(p_idx, cid as u16, ab_idx as usize) { abilities_to_run.push((cid as u16, ab_idx as u16, i as i16, false)); } } } } } } } } // 1.4. Collect Source Card Abilities (even if not on stage, e.g. for OnReveal) if ctx.source_card_id >= 0 { let cid = ctx.source_card_id as u16; if let Some(card) = db.get_member(cid) { for (ab_idx, ab) in card.abilities.iter().enumerate().skip(start_ab_idx) { if ab.trigger == trigger && !ab.bytecode.is_empty() { let actx = ctx.clone(); if ab.conditions.iter().all(|cond| self.check_condition(db, p_idx, cond, &actx)) { if self.check_once_per_turn(p_idx, cid, ab_idx) { // If this card is on stage, we might have already added it above. // But usually OnReveal happens while in deck/looked zone. // Check if already queued to avoid double triggers if it's somehow both. if !abilities_to_run.iter().any(|&(added_cid, added_ab_idx, _, _)| added_cid == cid && added_ab_idx == ab_idx as u16) { abilities_to_run.push((cid, ab_idx as u16, ctx.area_idx, false)); } } } } } } if let Some(live) = db.get_live(cid) { for (ab_idx, ab) in live.abilities.iter().enumerate().skip(start_ab_idx) { if ab.trigger == trigger && !ab.bytecode.is_empty() { let actx = ctx.clone(); if ab.conditions.iter().all(|cond| self.check_condition(db, p_idx, cond, &actx)) { if self.check_once_per_turn(p_idx, cid, ab_idx) { if !abilities_to_run.iter().any(|&(added_cid, added_ab_idx, _, _)| added_cid == cid as u16 && added_ab_idx == ab_idx as u16) { abilities_to_run.push((cid as u16, ab_idx as u16, ctx.area_idx, true)); } } } } } } } // 1.5. Collect Granted Abilities // 1.5. Collect Granted Abilities // Use a temporary list to avoid borrowing issues while accessing DB let mut granted_to_run: Vec<(i32, u32, usize)> = Vec::new(); // (target_cid, source_cid, ab_idx) for &(t_cid, s_cid, ab_idx) in &self.players[p_idx].granted_abilities { if let Some(card) = db.get_member(s_cid as u16) { if let Some(ab) = card.abilities.get(ab_idx as usize) { if ab.trigger != trigger { continue; } if let Some(slot) = self.players[p_idx].stage.iter().position(|&scid| scid == t_cid) { if trigger == TriggerType::OnPlay && ctx.area_idx >= 0 && slot as i16 != ctx.area_idx { continue; } granted_to_run.push((t_cid as i32, s_cid as u32, ab_idx as usize)); } } } } for (target_cid, s_cid, ab_idx) in granted_to_run { if let Some(card) = db.get_member(s_cid as u16) { if let Some(ab) = card.abilities.get(ab_idx as usize) { if !ab.bytecode.is_empty() { if let Some(slot) = self.players[p_idx].stage.iter().position(|&scid| scid == target_cid as i16) { let mut actx = ctx.clone(); actx.source_card_id = target_cid as i16; actx.area_idx = slot as i16; actx.ability_index = ab_idx as i16; // Note: using original index if ab.conditions.iter().all(|cond| self.check_condition(db, p_idx, cond, &actx)) { if self.check_once_per_turn(p_idx, s_cid as u16, ab_idx as usize) { if !self.silent { self.log(format!("Triggering GRANTED ability from card {} on target {}", s_cid, target_cid)); } self.resolve_bytecode(db, &ab.bytecode, &actx); } } } } } } } // 2. Collect LiveCard Abilities from Live Zone (for OnLiveStart trigger) if trigger == TriggerType::OnLiveStart { for i in 0..3 { let cid = self.players[p_idx].live_zone[i]; if cid >= 0 { if let Some(live_card) = db.get_live(cid as u16) { for (ab_idx, ab) in live_card.abilities.iter().enumerate() { if ab.trigger == trigger && !ab.bytecode.is_empty() { let mut actx = ctx.clone(); actx.area_idx = i as i16; if ab.conditions.iter().all(|cond| self.check_condition(db, p_idx, cond, &actx)) { if self.check_once_per_turn(p_idx, cid as u16, ab_idx) { abilities_to_run.push((cid as u16, ab_idx as u16, i as i16, true)); } } } } } } } } // 3. Execute Bytecode (Zero-Copy) with canonical logging for frontend translation let mut is_first = true; for (cid, ab_idx, area_idx, is_live) in abilities_to_run { if is_live { if let Some(live_card) = db.get_live(cid as u16) { if let Some(ab) = live_card.abilities.get(ab_idx as usize) { let mut actx = ctx.clone(); if !is_first { actx.program_counter = 0; } actx.area_idx = area_idx; actx.ability_index = ab_idx as i16; // Canonical format [TRIGGER:ID]CardName: Pseudocode if !self.silent { let effect_desc = if ab.raw_text.is_empty() { format!("Ability {}", ab_idx) } else { ab.raw_text.lines().next().unwrap_or(&ab.raw_text).to_string() }; let trigger_label = match trigger { TriggerType::OnPlay => "[Rule 11.3] (On Play)", TriggerType::OnLiveStart => "[Rule 8.3.2] (On Live Start)", TriggerType::TurnStart => "[Turn Start]", TriggerType::TurnEnd => "[Turn End]", TriggerType::Activated => "[Activated]", _ => "[Triggered]", }; self.log(format!("{} (Live){}: {}", trigger_label, live_card.name, effect_desc)); } self.resolve_bytecode(db, &ab.bytecode, &actx); } } } else { if let Some(card) = db.get_member(cid as u16) { if let Some(ab) = card.abilities.get(ab_idx as usize) { let mut actx = ctx.clone(); if !is_first { actx.program_counter = 0; } actx.area_idx = area_idx; actx.ability_index = ab_idx as i16; // Canonical format [TRIGGER:ID]CardName: Pseudocode if !self.silent { let effect_desc = if ab.raw_text.is_empty() { format!("Ability {}", ab_idx) } else { ab.raw_text.lines().next().unwrap_or(&ab.raw_text).to_string() }; let trigger_label = match trigger { TriggerType::OnPlay => "[Rule 11.3] (On Play)", TriggerType::OnLiveStart => "[Rule 8.3.2] (On Live Start)", TriggerType::TurnStart => "[Turn Start]", TriggerType::TurnEnd => "[Turn End]", TriggerType::Activated => "[Activated]", _ => "[Triggered]", }; self.log(format!("{} {}: {}", trigger_label, card.name, effect_desc)); } self.resolve_bytecode(db, &ab.bytecode, &actx); if self.phase == Phase::Response { break; } } } } if self.phase == Phase::Response { break; } is_first = false; } self.trigger_depth -= 1; } pub fn do_yell(&mut self, db: &CardDatabase, count: u32) -> Vec { let p_idx = self.current_player as usize; let mut revealed = Vec::new(); for _ in 0..count { if self.players[p_idx].deck.is_empty() { self.resolve_deck_refresh(p_idx); } if let Some(card_id) = self.players[p_idx].deck.pop() { revealed.push(card_id); // Dispatch OnReveal trigger let mut ctx = AbilityContext::default(); ctx.player_id = p_idx as u8; ctx.source_card_id = card_id as i16; self.trigger_abilities(db, TriggerType::OnReveal, &ctx); } } revealed } fn check_once_per_turn(&self, p_idx: usize, cid: u16, ab_idx: usize) -> bool { let uid = (cid as u32) << 16 | (ab_idx as u32); !self.players[p_idx].used_abilities.contains(&uid) } fn consume_once_per_turn(&mut self, p_idx: usize, cid: u16, ab_idx: usize) { let uid = (cid as u32) << 16 | (ab_idx as u32); if !self.players[p_idx].used_abilities.contains(&uid) { self.players[p_idx].used_abilities.push(uid); } } pub fn card_matches_filter(&self, db: &CardDatabase, cid: u16, filter_attr: i32) -> bool { // Bit 2-3: Type Filter (0=None, 1=Member, 2=Live) let type_filter = (filter_attr >> 2) & 0x03; if type_filter == 1 && db.get_member(cid).is_none() { return false; } if type_filter == 2 && db.get_live(cid).is_none() { return false; } // Bit 4: Group Filter Enable if (filter_attr & 0x10) != 0 { let group_id = (filter_attr >> 5) & 0x7F; if let Some(m) = db.get_member(cid) { if !m.groups.contains(&(group_id as u8)) { return false; } } else if let Some(l) = db.get_live(cid) { if !l.groups.contains(&(group_id as u8)) { return false; } } else { return false; } } true } pub fn check_hearts_suitability(&self, have: &[u8; 7], need: &[u8; 7]) -> bool { let mut have_u32 = [0u32; 7]; let mut need_u32 = [0u32; 7]; for i in 0..7 { have_u32[i] = have[i] as u32; need_u32[i] = need[i] as u32; } let (sat, tot) = crate::core::hearts::process_hearts(&mut have_u32, &need_u32); sat >= tot } pub fn consume_hearts_from_pool(&self, pool: &mut [u8; 7], need: &[u8; 7]) { let mut pool_u32 = [0u32; 7]; let mut need_u32 = [0u32; 7]; for i in 0..7 { pool_u32[i] = pool[i] as u32; need_u32[i] = need[i] as u32; } crate::core::hearts::process_hearts(&mut pool_u32, &need_u32); for i in 0..7 { pool[i] = pool_u32[i] as u8; } } pub fn check_live_success(&self, p_idx: usize, live: &LiveCard, total_hearts: &[u8; 7]) -> bool { // Apply reductions let mut req = live.required_hearts; let reductions = self.players[p_idx].heart_req_reductions; for i in 0..7 { req[i] = (req[i] as i32 - reductions[i]).max(0) as u8; } self.check_hearts_suitability(total_hearts, &req) } pub fn do_performance_phase(&mut self, db: &CardDatabase) { let p_idx = self.current_player as usize; // 8.3.4 Flip all cards in Live Zone for i in 0..3 { self.players[p_idx].live_zone_revealed[i] = true; } // Discard non-live cards (Rule 8.3.4) BEFORE triggering OnLiveStart (Rule 11.4/8.3.8) for i in 0..3 { let cid = self.players[p_idx].live_zone[i]; if cid >= 0 && db.get_live(cid as u16).is_none() { if !self.silent { self.log(format!("Rule 8.3.4: Discarding non-live card #{} from Live Zone.", cid)); } self.players[p_idx].discard.push(cid as u16); self.players[p_idx].live_zone[i] = -1; } } // Rule 11.4 [ライブ開始時] (Live Start) for i in 0..3 { let cid = self.players[p_idx].live_zone[i]; if cid >= 0 { let ctx = AbilityContext { source_card_id: cid, player_id: p_idx as u8, area_idx: i as i16, ..Default::default() }; if !self.silent { self.log("Rule 11.4: Triggering [ライブ開始時] (Live Start) abilities.".to_string()); } self.trigger_abilities(db, TriggerType::OnLiveStart, &ctx); } } // 8.3.6 If no lives, end phase if self.players[p_idx].live_zone.iter().all(|&c| c < 0) { // Create empty performance result for consistency in logs self.performance_results.insert(p_idx as u8, json!({ "success": false, "total_hearts": [0, 0, 0, 0, 0, 0, 0], "volume_icons": 0, "yell_count": 0, "lives": [], "yell_cards": [], "breakdown": { "blades": [], "hearts": [] } })); self.advance_from_performance(); return; } // 8.3.10-11 Yell let mut total_blades = 0; for i in 0..3 { total_blades += self.get_effective_blades(p_idx, i, db); } if !self.silent { self.log(format!("Rule 8.3.11: Player {} performs Yell ({} blades).", p_idx, total_blades)); } let yelled_cards = self.do_yell(db, total_blades); if !self.silent { self.log(format!("--- PLAYER {} PERFORMANCE ---", p_idx)); self.log(format!(" Blades: {}", total_blades)); } if !self.silent { let mut yell_summary = Vec::new(); for &cid in &yelled_cards { let (name, bh, vi) = if let Some(m) = db.get_member(cid as u16) { (m.name.clone(), m.blade_hearts, m.volume_icons) } else if let Some(l) = db.get_live(cid as u16) { (l.name.clone(), l.blade_hearts, l.volume_icons) } else { ("Unknown".to_string(), [0u8; 7], 0) }; let mut contribs = Vec::new(); let h_sum: u32 = bh.iter().map(|&h| h as u32).sum(); if h_sum > 0 { contribs.push(format!("+{}H", h_sum)); } if vi > 0 { contribs.push(format!("+{}V", vi)); } let desc = if contribs.is_empty() { "Nothing".to_string() } else { contribs.join(", ") }; yell_summary.push(format!("{}({})", name, desc)); } self.log(format!(" Yells: [{}]", yell_summary.join(", "))); } // 8.3.14 Calculate Owned Hearts let mut total_hearts = [0u8; 7]; for i in 0..3 { let eff_h = self.get_effective_hearts(p_idx, i, db); for h in 0..7 { total_hearts[h] += eff_h[h]; } } let mut volume_icons = 0; // Include Icons from Stage (Rule 8.3.14) for i in 0..3 { let cid = self.players[p_idx].stage[i]; if cid >= 0 { if let Some(m) = db.get_member(cid as u16) { volume_icons += m.volume_icons; } } } for &cid in &yelled_cards { let (bh, vi) = if let Some(m) = db.get_member(cid) { (m.blade_hearts, m.volume_icons) } else if let Some(l) = db.get_live(cid) { (l.blade_hearts, l.volume_icons) } else { ([0u8; 7], 0) }; let mut adj_bh = bh; for &(src, dst) in &self.players[p_idx].color_transforms { if src == 0 && (dst as usize) < 7 { let mut sum = 0; for i in 0..7 { if i != dst as usize { sum += adj_bh[i]; adj_bh[i] = 0; } } adj_bh[dst as usize] += sum; } } for i in 0..7 { total_hearts[i] += adj_bh[i]; } volume_icons += vi; } self.players[p_idx].current_turn_volume = volume_icons; if !self.silent { self.log(format!(" Total Hearts: {:?}", total_hearts)); self.log(format!(" Volume Icons: {}", volume_icons)); } // 8.3.15-16 Check heart requirements let mut passed_flags = [false; 3]; let mut any_failed = false; // In this implementation, we consume hearts per live card (8.3.15.1.2) let mut remaining_hearts = total_hearts; for i in 0..3 { if let Some(cid) = self.players[p_idx].live_zone.get(i).copied().filter(|&c| c >= 0) { if let Some(live) = db.get_live(cid as u16) { let mut req = live.required_hearts; for h in 0..7 { req[h] = (req[h] as i32 - self.players[p_idx].heart_req_reductions[h]).max(0) as u8; } self.log(format!(" Live {}: {} - Need: {:?}, Available: {:?}", i, live.name, req, remaining_hearts)); if !any_failed && self.check_hearts_suitability(&remaining_hearts, &req) { self.consume_hearts_from_pool(&mut remaining_hearts, &req); passed_flags[i] = true; self.log(format!(" -> SUCCESS for {}", live.name)); // Dispatch OnLiveSuccess trigger: moved to LiveResult phase to ensure failure safety } else { self.log(format!(" -> FAILED hearts for {}", live.name)); any_failed = true; // Rule 8.3.16 covers this: if any fail, all are discarded later. } } } } // Rule 8.3.16 covers this: if any fail, all are discarded. // We capture IDs here so we can still report them to UI even if discarded. let live_ids_before_discard: Vec = self.players[p_idx].live_zone.to_vec(); // Rule 8.3.16: If ANY live card's requirements were not met, discard all live cards. if any_failed { self.log(" Rule 8.3.16: Performance FAILED. All live cards discarded.".to_string()); for i in 0..3 { if self.players[p_idx].live_zone[i] >= 0 { self.players[p_idx].discard.push(self.players[p_idx].live_zone[i] as u16); self.players[p_idx].live_zone[i] = -1; passed_flags[i] = false; // Ensure UI reflects failure } } } let all_met = !any_failed && live_ids_before_discard.iter().enumerate().all(|(i, &cid)| { cid < 0 || passed_flags[i] }); if all_met { for i in 0..3 { if passed_flags[i] { let cid = live_ids_before_discard[i]; if cid >= 0 { let ctx = AbilityContext { source_card_id: cid as i16, player_id: p_idx as u8, area_idx: i as i16, ..Default::default() }; self.trigger_abilities(db, TriggerType::OnLiveSuccess, &ctx); } } } } // --- Store Performance Results for UI --- let mut heart_breakdown = Vec::new(); let mut blade_breakdown = Vec::new(); // 1. Stage Contributions for i in 0..3 { let cid = self.players[p_idx].stage[i]; if cid >= 0 { if let Some(m) = db.get_member(cid as u16) { let eff_h = self.get_effective_hearts(p_idx, i, db); if eff_h.iter().any(|&h| h > 0) { heart_breakdown.push(json!({ "source": m.name, "source_id": cid, "value": eff_h, "type": "member" })); } let eff_b = self.get_effective_blades(p_idx, i, db); if eff_b > 0 { blade_breakdown.push(json!({ "source": m.name, "source_id": cid, "value": eff_b, "type": "member" })); } } // Rule 8.4.10: Participants change to Rest state self.players[p_idx].tapped_members[i] = true; } } let mut yell_cards_meta = Vec::new(); for &cid in &yelled_cards { if let Some(m) = db.get_member(cid) { yell_cards_meta.push(json!({ "id": cid, "img": m.img_path, "blade_hearts": m.blade_hearts, })); } else if let Some(l) = db.get_live(cid) { yell_cards_meta.push(json!({ "id": cid, "img": l.img_path, "blade_hearts": l.blade_hearts, })); } } let mut lives_list = Vec::new(); let mut temp_hearts_debug = total_hearts; // For simulating filling logic for i in 0..3 { let cid = live_ids_before_discard[i]; if cid >= 0 { if let Some(l) = db.get_live(cid as u16) { let mut req = l.required_hearts; for h in 0..7 { req[h] = (req[h] as i32 - self.players[p_idx].heart_req_reductions[h]).max(0) as u8; } // Calculate "filled" state for UI let mut filled = [0u8; 7]; let mut sim_have = temp_hearts_debug; // 1. Specific for ci in 0..6 { let take = sim_have[ci].min(req[ci]); filled[ci] = take; sim_have[ci] -= take; } // 2. Any let any_need = req[6]; let any_have: i32 = sim_have.iter().map(|&h| h as i32).sum(); filled[6] = (any_have as u8).min(any_need); lives_list.push(json!({ "id": cid, "name": l.name, "img": l.img_path, "passed": passed_flags[i], "score": l.score, "required": req, "filled": filled, })); // If successfully passed in sequence, permanently consume for next live card UI check if passed_flags[i] { self.consume_hearts_from_pool(&mut temp_hearts_debug, &req); } } } } self.performance_results.insert(p_idx as u8, json!({ "success": all_met, "total_hearts": total_hearts, "volume_icons": volume_icons, "yell_count": total_blades, "lives": lives_list, "yell_cards": yell_cards_meta, "breakdown": { "blades": blade_breakdown, "hearts": heart_breakdown } })); for cid in yelled_cards { self.players[p_idx].discard.push(cid); } self.advance_from_performance(); } fn advance_from_performance(&mut self) { if self.current_player == self.first_player { self.current_player = 1 - self.first_player; // Phase stays the same or moves to P2? // My enum uses PerformanceP1 and PerformanceP2. if self.phase == Phase::PerformanceP1 { self.phase = Phase::PerformanceP2; } else { self.phase = Phase::PerformanceP1; // Error path } } else { self.phase = Phase::LiveResult; self.current_player = self.first_player; } } pub fn do_live_result(&mut self, _db: &CardDatabase) { self.log("Rule 8.4: --- LIVE RESULT PHASE ---".to_string()); // Save current results to history for (pid, res) in &self.performance_results { if let serde_json::Value::Object(mut map) = res.clone() { map.insert("turn".to_string(), json!(self.turn)); map.insert("player_id".to_string(), json!(pid)); self.performance_history.push(serde_json::Value::Object(map)); } } self.last_performance_results = self.performance_results.clone(); // 0. Trigger ON_LIVE_SUCCESS for successful performances (Rule 8.3.15 sequence completion) for p in 0..2 { if let Some(res) = self.performance_results.get(&(p as u8)) { if res.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { self.log(format!("DEBUG: Live Success performance detected for player {}. Checking live zone cards for OnLiveSuccess triggers.", p)); for i in 0..3 { let cid = self.players[p].live_zone[i]; if cid >= 0 { let ctx = AbilityContext { source_card_id: cid, player_id: p as u8, area_idx: i as i16, ..Default::default() }; if !self.silent { self.log(format!("DEBUG: Triggering OnLiveSuccess for card {} in slot {}.", cid, i)); } self.trigger_abilities(_db, TriggerType::OnLiveSuccess, &ctx); // Note: If an ability is interactive, it will move to Response phase if self.phase == Phase::Response { return; } } } } } } let mut scores = [0u32; 2]; let mut has_success = [false; 2]; // 1. Judgment Phase: Calculate scores based on SUCCESSFUL lives (still in zone) for p in 0..2 { let mut live_score = 0; let mut player_has_success = false; if let Some(res) = self.performance_results.get(&(p as u8)) { // In my updated do_performance_phase, if ANY failed, ALL were discarded. // So if any cards are in live_zone, they MUST have passed. for i in 0..3 { let cid = self.players[p].live_zone[i]; if cid >= 0 { // For scoring, we lookup the score in performance results JSON // (already calculated in do_performance_phase) if let Some(lives) = res.get("lives").and_then(|l| l.as_array()) { if let Some(l_res) = lives.get(i) { if let Some(s) = l_res.get("score").and_then(|s| s.as_u64()) { live_score += s as u32; player_has_success = true; } } } } } // Add Volume Icons if at least one live was successful if player_has_success { if let Some(vol) = res.get("yell_score_bonus").and_then(|v| v.as_u64()) .or_else(|| res.get("volume_icons").and_then(|v| v.as_u64())) { live_score += vol as u32; } has_success[p] = true; } } scores[p] = live_score; } // 8.4.6 Compare let p0_wins = has_success[0] && (!has_success[1] || scores[0] >= scores[1]); let p1_wins = has_success[1] && (!has_success[0] || scores[1] >= scores[0]); let is_comparative_tie = p0_wins && p1_wins; if !self.silent { self.log(format!("Rule 8.4.6: P0 Score: {} (Success: {} wins: {})", scores[0], has_success[0], p0_wins)); self.log(format!("Rule 8.4.6: P1 Score: {} (Success: {} wins: {})", scores[1], has_success[1], p1_wins)); } // 2. Handling Winners (Rule 8.4.7) let mut choices_pending = false; for p in 0..2 { let wins = if p == 0 { p0_wins } else { p1_wins }; if wins { let my_live_count = self.players[p].live_zone.iter().filter(|&&c| c >= 0).count(); if is_comparative_tie && my_live_count >= 2 { if !self.silent { self.log(format!(" Rule 8.4.7.1: P{} Tie Penalty - {} cards remain in zone.", p, my_live_count)); } } else if my_live_count == 1 { // Auto-move single card for i in 0..3 { if self.players[p].live_zone[i] >= 0 { let cid = self.players[p].live_zone[i]; self.players[p].success_lives.push(cid as u16); self.players[p].live_zone[i] = -1; if !self.silent { self.log(format!("Rule 8.4.7: P{} obtained Success Live: Card ID {}", p, cid)); } break; } } } else if my_live_count > 1 { // SELECTION NEEDED if !choices_pending { self.current_player = p as u8; choices_pending = true; if !self.silent { self.log(format!("Rule 8.4.7.3: P{} must SELECT a success live card.", p)); } } } } } if choices_pending { // Stay in LiveResult phase, wait for 600-602 return; } // 3. Finalization (Cleanup and Turn Advance) self.finalize_live_result(); } pub fn finalize_live_result(&mut self) { // 8.4.8 Cleanup all live zones for p in 0..2 { for i in 0..3 { if self.players[p].live_zone[i] >= 0 { self.players[p].discard.push(self.players[p].live_zone[i] as u16); self.players[p].live_zone[i] = -1; } } self.players[p].current_turn_volume = 0; self.players[p].score = self.players[p].success_lives.len() as u32; } // 8.4.13 Determine next first player (Winner of judgement goes first) // Note: Simple logic for now, winner of judgement or host stays self.check_win_condition(); if self.phase != Phase::Terminal { self.turn += 1; // 8.4.13 Winner becomes next first player // For now, toggle host or keep winner. Let's toggle for balance if no winner. self.first_player = (self.first_player + 1) % 2; self.current_player = self.first_player; self.phase = Phase::Active; } self.performance_results.clear(); } pub fn do_active_phase(&mut self, db: &CardDatabase) { let p_idx = self.current_player as usize; self.setup_turn_log(); if !self.silent { self.log(format!("Rule 7.4.1: [Active Phase] Untapping all cards for Player {}.", p_idx)); } self.players[p_idx].untap_all(); let ctx = AbilityContext { source_card_id: -1, player_id: p_idx as u8, area_idx: -1, ..Default::default() }; self.trigger_abilities(db, TriggerType::TurnStart, &ctx); self.phase = Phase::Energy; } pub fn play_energy(&mut self, hand_idx: usize) { let p_idx = self.current_player as usize; if hand_idx < self.players[p_idx].hand.len() { if !self.silent { self.log(format!("Rule 9.6: Player {} played Energy from hand", p_idx)); } let cid = self.players[p_idx].hand.remove(hand_idx); if hand_idx < self.players[p_idx].hand_added_turn.len() { self.players[p_idx].hand_added_turn.remove(hand_idx); } self.players[p_idx].energy_zone.push(cid); self.players[p_idx].tapped_energy.push(false); } } pub fn do_energy_phase(&mut self) { let p_idx = self.current_player as usize; if let Some(card_id) = self.players[p_idx].energy_deck.pop() { if !self.silent { self.log(format!("Rule 7.5.2: Player {} placed Energy from Energy Deck", p_idx)); } self.players[p_idx].energy_zone.push(card_id); self.players[p_idx].tapped_energy.push(false); } self.phase = Phase::Draw; } pub fn do_draw_phase(&mut self) { let p_idx = self.current_player as usize; if !self.silent { self.log(format!("Rule 7.6.2: Player {} draws a card.", p_idx)); } self.draw_cards(p_idx, 1); self.phase = Phase::Main; } pub fn set_live_cards(&mut self, player_idx: usize, card_ids: Vec) -> Result<(), String> { if card_ids.len() > 3 { return Err("Too many lives".to_string()); } // For logging purposes, we'd ideally show card names, but for now just count if !self.silent { self.log(format!("Rule 8.2.2: Player {} sets {} cards to Live Zone.", player_idx, card_ids.len())); } for cid in card_ids { let mut placed = false; for i in 0..3 { if self.players[player_idx].live_zone[i] == -1 { self.players[player_idx].live_zone[i] = cid as i16; self.players[player_idx].live_zone_revealed[i] = false; // Rule 8.2.2: Draw same number of cards as placed self.draw_cards(player_idx, 1); placed = true; break; } } if !placed { return Err("No empty live slots".to_string()); } } Ok(()) } pub fn end_main_phase(&mut self, db: &CardDatabase) { if !self.silent { self.log(format!("Rule 7.7.3: Player {} ends Main Phase.", self.current_player)); } if self.current_player == self.first_player { self.current_player = 1 - self.first_player; self.phase = Phase::Active; self.do_active_phase(db); self.do_energy_phase(); self.do_draw_phase(); } else { self.phase = Phase::LiveSet; self.current_player = self.first_player; } } pub fn execute_mulligan(&mut self, player_idx: usize, discard_indices: Vec) { self.log(format!("Rule 6.2.1.6: Player {} finished mulligan ({} cards)", player_idx, discard_indices.len())); let mut count = 0; let mut discards = Vec::new(); let mut new_hand = SmallVec::new(); for (i, &cid) in self.players[player_idx].hand.iter().enumerate() { if discard_indices.contains(&i) { discards.push(cid); count += 1; } else { new_hand.push(cid); } } self.players[player_idx].hand = new_hand; for _ in 0..count { if self.players[player_idx].deck.is_empty() { self.resolve_deck_refresh(player_idx); } if let Some(card_id) = self.players[player_idx].deck.pop() { self.players[player_idx].hand.push(card_id); self.players[player_idx].hand_added_turn.push(self.turn); } } // If deck still empty and discard has cards, we already refreshed above or it's empty self.players[player_idx].deck.extend(discards); let mut rng = Pcg64::from_os_rng(); self.players[player_idx].deck.shuffle(&mut rng); self.resolve_deck_refresh(player_idx); if self.phase == Phase::MulliganP1 { self.current_player = 1 - self.first_player; self.phase = Phase::MulliganP2; } else if self.phase == Phase::MulliganP2 { self.current_player = self.first_player; self.phase = Phase::Active; } } pub fn play_member(&mut self, db: &CardDatabase, hand_idx: usize, slot_idx: usize) -> Result<(), String> { self.play_member_with_choice(db, hand_idx, slot_idx, -1, 0) } pub fn play_member_with_choice(&mut self, db: &CardDatabase, hand_idx: usize, slot_idx: usize, choice_idx: i32, start_ab_idx: usize) -> Result<(), String> { let p_idx = self.current_player as usize; if self.phase == Phase::Response { // RESUMPTION: Skip movement and cost payment. // ctx is passed in or reconstructed from pending_ctx to preserve program_counter let card_id = self.pending_card_id; let card = db.get_member(card_id as u16).ok_or("Card not found")?; let mut ctx = if let Some(p_ctx) = &self.pending_ctx { p_ctx.clone() } else { AbilityContext { source_card_id: card_id, player_id: p_idx as u8, area_idx: slot_idx as i16, choice_index: choice_idx as i16, ..Default::default() } }; // Ensure choice is updated ctx.choice_index = choice_idx as i16; if !self.silent { self.log_rule("Rule 11.3", &format!("Resuming [登場] (On Play) abilities for {} from idx {}.", card.name, start_ab_idx)); } // Standardize resumption: Reset phase to Main BEFORE execution. self.phase = Phase::Main; self.pending_ctx = None; self.pending_card_id = -1; self.pending_ab_idx = -1; self.pending_effect_opcode = -1; self.pending_choice_type = String::new(); self.trigger_abilities_from(db, TriggerType::OnPlay, &ctx, start_ab_idx); self.process_rule_checks(); return Ok(()); } if hand_idx >= self.players[p_idx].hand.len() { return Err("Invalid hand index".to_string()); } let card_id = self.players[p_idx].hand[hand_idx]; let card = db.get_member(card_id).ok_or("Card not found")?; if !self.silent { self.log(format!("Rule 7.7.2.2: Player {} plays {} to Slot {}", p_idx, card.name, slot_idx)); } let mut cost = self.get_member_cost(p_idx, card_id as i32, slot_idx as i32, db); let old_card_id = self.players[p_idx].stage[slot_idx]; if old_card_id >= 0 { if self.players[p_idx].moved_members_this_turn[slot_idx] { return Err("Already played/moved to this slot this turn".to_string()); } if self.players[p_idx].baton_touch_count >= self.players[p_idx].baton_touch_limit { return Err("Baton touch limit reached".to_string()); } let prev_card = db.get_member(old_card_id as u16).ok_or_else(|| format!("Prev card not found: ID {} in phase {:?}", old_card_id, self.phase))?; if !self.silent { self.log(format!("Rule 9.6.2.3.2: Baton Touch! Cost reduced by {}.", prev_card.cost)); } cost = cost.max(0); self.players[p_idx].baton_touch_count += 1; // Move old card to discard self.players[p_idx].discard.push(old_card_id as u16); } else { if self.players[p_idx].moved_members_this_turn[slot_idx] { return Err("Already played/moved to this slot this turn".to_string()); } } let untap_energy_indices: Vec = self.players[p_idx].tapped_energy.iter().enumerate() .filter(|&(_, &tapped)| !tapped).map(|(i, _)| i).take(cost as usize).collect(); if untap_energy_indices.len() < cost as usize { return Err("Not enough energy".to_string()); } for i in untap_energy_indices { self.players[p_idx].tapped_energy[i] = true; } self.players[p_idx].hand.remove(hand_idx); if hand_idx < self.players[p_idx].hand_added_turn.len() { self.players[p_idx].hand_added_turn.remove(hand_idx); } self.prev_card_id = old_card_id; self.players[p_idx].stage[slot_idx] = card_id as i16; self.players[p_idx].tapped_members[slot_idx] = false; self.players[p_idx].moved_members_this_turn[slot_idx] = true; let ctx = AbilityContext { source_card_id: card_id as i16, player_id: p_idx as u8, area_idx: slot_idx as i16, choice_index: -1, // Triggered abilities always start with no choice ability_index: -1, // Will be set specifically if paused for choice ..Default::default() }; // Rule 11.3: Trigger [登場] (On Play) abilities. // NEW: Check if we should pause for a choice. if self.phase != Phase::Response { let mut needs_choice = false; let mut ab_idx_with_choice = -1; for (i, ab) in card.abilities.iter().enumerate().skip(start_ab_idx) { if ab.trigger == TriggerType::OnPlay && bytecode_needs_early_pause(&ab.bytecode) { // Check conditions before pausing let mut actx = ctx.clone(); actx.area_idx = slot_idx as i16; if ab.conditions.iter().all(|cond| self.check_condition(db, p_idx, cond, &actx)) { needs_choice = true; ab_idx_with_choice = i as i32; break; } } } if needs_choice { if !self.silent { self.log(format!("Rule 11.3: Pausing for choice on {}.", card.name)); } let ab = &card.abilities[ab_idx_with_choice as usize]; // Identify the opcode that triggered the choice let opcode = ab.bytecode.chunks(4) .find(|chunk| !chunk.is_empty() && bytecode_has_choice(chunk)) .map(|chunk| chunk[0]) .unwrap_or(-1); self.phase = Phase::Response; self.pending_ctx = Some(ctx); self.pending_card_id = card_id as i16; self.pending_ab_idx = ab_idx_with_choice as i16; self.pending_effect_opcode = opcode as i16; self.pending_choice_type = match opcode { O_SELECT_MODE => "SELECT_MODE".to_string(), O_LOOK_AND_CHOOSE => "LOOK_AND_CHOOSE".to_string(), O_COLOR_SELECT => "COLOR_SELECT".to_string(), // O_ORDER_DECK is excluded here intentionally so it executes first to populate looked_cards _ => "".to_string(), }; return Ok(()); } } if !self.silent { self.log_rule("Rule 11.3", &format!("Triggering [登場] (On Play) abilities for {}.", card.name)); } self.trigger_abilities_from(db, TriggerType::OnPlay, &ctx, start_ab_idx); self.process_rule_checks(); // If we are in Response phase (paused for input), return immediately to wait for user if self.phase == Phase::Response { return Ok(()); } // Only set to Main if we are NOT in Response (e.g. fully resolved) if self.phase != Phase::Terminal { self.phase = Phase::Main; } Ok(()) } pub fn activate_ability(&mut self, db: &CardDatabase, slot_idx: usize, ab_idx: usize) -> Result<(), String> { self.activate_ability_with_choice(db, slot_idx, ab_idx, 0, 0) } pub fn activate_ability_with_choice(&mut self, db: &CardDatabase, slot_idx: usize, ab_idx: usize, choice_idx: i32, target_slot: i32) -> Result<(), String> { let p_idx = self.current_player as usize; let cid = if self.phase == Phase::Response { self.pending_card_id } else if slot_idx < 3 { let scid = self.players[p_idx].stage[slot_idx]; if scid >= 0 { scid } else { self.players[p_idx].live_zone[slot_idx] } } else if slot_idx >= 100 && slot_idx < 200 { let d_idx = slot_idx - 100; if d_idx < self.players[p_idx].discard.len() { self.players[p_idx].discard[d_idx] as i16 } else { -1 } } else { -1 }; if cid < 0 { return Err("No member found".to_string()); } if self.phase == Phase::Response { // RESUMPTION: Skip costs and OncePerTurn tracking. Handles both Member and Live cards. let (card_name, bytecode) = if let Some(mem) = db.get_member(cid as u16) { let ab = mem.abilities.get(ab_idx).ok_or("Ability not found on member")?; (mem.name.clone(), &ab.bytecode) } else if let Some(live) = db.get_live(cid as u16) { let ab = live.abilities.get(ab_idx).ok_or("Ability not found on live card")?; (live.name.clone(), &ab.bytecode) } else { // Standalone bytecode resumption fallback: if ab_idx is effectively 0 and we have a bytecode? // But we don't store raw bytecode in GS. So we assume it MUST be from a card. return Err("Card not found in database for resumption".to_string()); }; let mut ctx = if let Some(p_ctx) = &self.pending_ctx { p_ctx.clone() } else { AbilityContext { source_card_id: cid, player_id: p_idx as u8, area_idx: slot_idx as i16, choice_index: choice_idx as i16, target_slot: target_slot as i16, ability_index: ab_idx as i16, ..Default::default() } }; // Update choice ctx.choice_index = choice_idx as i16; if !self.silent { self.log(format!("Rule 7.7.2.1: Resuming ability of {} (Slot {}, Choice {})", card_name, slot_idx, choice_idx)); } // Standardize resumption: Reset phase to Main BEFORE execution. self.phase = Phase::Main; self.pending_ctx = None; self.pending_card_id = -1; self.pending_ab_idx = -1; self.pending_effect_opcode = -1; self.pending_choice_type = String::new(); self.resolve_bytecode(db, bytecode, &ctx); self.process_rule_checks(); return Ok(()); } let card = db.get_member(cid as u16).ok_or("Card not found")?; let ab = card.abilities.get(ab_idx).ok_or("Ability not found")?; if ab.trigger != TriggerType::Activated && self.phase != Phase::Response { return Err("Not an activated ability".to_string()); } if !self.silent { self.log(format!("Rule 7.7.2.1: Player {} activates ability of {} (Slot {}, Choice {})", p_idx, card.name, slot_idx, choice_idx)); } let ctx = AbilityContext { source_card_id: cid, player_id: p_idx as u8, area_idx: slot_idx as i16, choice_index: choice_idx as i16, target_slot: target_slot as i16, ability_index: ab_idx as i16, ..Default::default() }; // Check costs for cost in &ab.costs { if !self.check_cost(p_idx, cost, &ctx) { return Err("Cannot afford cost".to_string()); } } // Check conditions for cond in &ab.conditions { if !self.check_condition(db, p_idx, cond, &ctx) { return Err("Conditions not met".to_string()); } } // Track Once Per Turn if ab.is_once_per_turn { let ab_uid = (slot_idx as u32) << 16 | (ab_idx as u32); if self.players[p_idx].used_abilities.contains(&ab_uid) { return Err("Ability already used this turn".to_string()); } self.players[p_idx].used_abilities.push(ab_uid); } // Pay Costs for cost in &ab.costs { self.pay_cost(db, p_idx, cost, &ctx); } // NEW: Check if we should pause for a choice AFTER costs are paid if self.phase != Phase::Response && bytecode_needs_early_pause(&ab.bytecode) { if !self.silent { self.log(format!("Rule 7.7.2.1: Pausing for choice on {}.", card.name)); } self.phase = Phase::Response; self.pending_ctx = Some(ctx); self.pending_card_id = cid; self.pending_ab_idx = ab_idx as i16; self.pending_effect_opcode = bytecode_needs_early_pause_opcode(&ab.bytecode) as i16; return Ok(()); } // Run self.consume_once_per_turn(p_idx, cid as u16, ab_idx); self.resolve_bytecode(db, &ab.bytecode, &ctx); self.process_rule_checks(); // If we are in Response phase (paused for input), return immediately if self.phase == Phase::Response { return Ok(()); } // If not paused, and not terminal, ensure we are in Main phase (cleanup for single-shot) if self.phase != Phase::Terminal { self.phase = Phase::Main; } Ok(()) } pub fn step(&mut self, db: &CardDatabase, action: i32) -> Result<(), String> { // eprintln!("TRACE: step phase={:?} action={}", self.phase, action); match self.phase { Phase::MulliganP1 | Phase::MulliganP2 => { let p_idx = self.current_player as usize; if action == 0 { let mut discards = Vec::new(); for i in 0..60 { if (self.players[p_idx].mulligan_selection >> i) & 1 == 1 { discards.push(i as usize); } } self.execute_mulligan(p_idx, discards); } else if action >= 300 && action <= 359 { let card_idx = (action - 300) as u16; if (card_idx as usize) < self.players[p_idx].hand.len() { self.players[p_idx].mulligan_selection ^= 1u64 << card_idx; } } }, Phase::Active => { self.do_active_phase(db); }, Phase::Energy => { if action >= 100 && action <= 159 { self.play_energy((action - 100) as usize); } else { self.do_energy_phase(); } }, Phase::Draw => { self.do_draw_phase(); }, Phase::Main => { if action == 0 { self.end_main_phase(db); } else if action >= 1 && action <= 180 { let adj = action - 1; self.play_member(db, (adj / 3) as usize, (adj % 3) as usize)?; } else if action >= 200 && action < 230 { // Simple ability activation (no choice) let adj = action - 200; self.activate_ability(db, (adj / 10) as usize, (adj % 10) as usize)?; } else if action >= 550 && action < 850 { // Ability with choice: 550 + slot*100 + ab_idx*10 + choice let adj = action - 550; let slot_idx = (adj / 100) as usize; let ab_idx = ((adj % 100) / 10) as usize; let choice_idx = (adj % 10) as i32; self.activate_ability_with_choice(db, slot_idx, ab_idx, choice_idx, 0)?; } else if action >= 1000 && action < 2000 { // Play with choice: 1000 + hand*100 + slot*10 + choice let adj = (action - 1000) as usize; let hand_idx = adj / 100; let rem = adj % 100; let slot_idx = rem / 10; let choice_idx = (rem % 10) as i32; self.play_member_with_choice(db, hand_idx, slot_idx, choice_idx, 0)?; } else if action >= 2000 && action < 3000 { // Activate from discard: 2000 + discard_idx * 10 + ab_idx let adj = action - 2000; let discard_idx = (adj / 10) as usize; let ab_idx = (adj % 10) as usize; self.activate_ability_with_choice(db, 100 + discard_idx, ab_idx, -1, -1)?; } }, Phase::LiveSet => { let p_idx = self.current_player as usize; if action == 0 { let draws = self.live_set_pending_draws[p_idx]; if draws > 0 { if !self.silent { self.log(format!("Rule 8.2.2: Live Set End: Player {} draws {} cards.", p_idx, draws)); } self.draw_cards(p_idx, draws as u32); self.live_set_pending_draws[p_idx] = 0; } if self.current_player == self.first_player { self.current_player = 1 - self.first_player; } else { self.phase = Phase::PerformanceP1; self.current_player = self.first_player; } } else if action >= 400 && action < 500 { let hand_idx = (action - 400) as usize; if hand_idx < self.players[p_idx].hand.len() { let cid = self.players[p_idx].hand.remove(hand_idx); for i in 0..3 { if self.players[p_idx].live_zone[i] == -1 { self.players[p_idx].live_zone[i] = cid as i16; self.players[p_idx].live_zone_revealed[i] = false; self.live_set_pending_draws[p_idx] += 1; if !self.silent { self.log(format!("Rule 8.2.2: Player {} sets live card. Draw pending (+1).", p_idx)); } break; } } } } }, Phase::PerformanceP1 | Phase::PerformanceP2 => { self.do_performance_phase(db); // Transition handled in advance_from_performance }, Phase::LiveResult => { if action == 0 { self.do_live_result(db); } else if action >= 600 && action <= 602 { let p_idx = self.current_player as usize; let slot_idx = (action - 600) as usize; let cid = self.players[p_idx].live_zone[slot_idx]; if cid >= 0 { self.players[p_idx].success_lives.push(cid as u16); self.players[p_idx].live_zone[slot_idx] = -1; if !self.silent { self.log(format!("Player {} SELECTED Success Live: Card ID {}", p_idx, cid)); } self.finalize_live_result(); } } }, Phase::Response => { if let Some(ctx) = self.pending_ctx.clone() { // Unified choice index extraction from action let choice_idx = if action == 0 { if self.pending_effect_opcode == O_ORDER_DECK as i16 || self.pending_effect_opcode == O_LOOK_AND_CHOOSE as i16 { 99 // "Done/Discard Rest" signal } else { -1 } } else if action >= 560 && action <= 569 { action - 560 } else if action >= 580 && action <= 585 { action - 580 } else if action >= 550 { // Context-encoded: 550 + slot*100 + ab*10 + choice let adj = action - 550; let slot = adj / 100; let ab = (adj % 100) / 10; // Optional: Validate context matches pending (prevents illegal cross-ability actions) if slot as usize == ctx.area_idx as usize && ab as usize == self.pending_ab_idx.max(0) as usize { (adj % 10) as i32 } else { // Fallback for legacy 600+ if not using high format, or mismatch if action >= 600 && action <= 719 { (action - 600) as i32 } else { -1 } } } else { -1 }; if self.pending_effect_opcode == O_ORDER_DECK as i16 || self.pending_effect_opcode == O_LOOK_AND_CHOOSE as i16 { let ab_idx = self.pending_ab_idx; if ab_idx < 0 { self.play_member_with_choice(db, 0, ctx.area_idx as usize, choice_idx, 0)?; } else { self.activate_ability_with_choice(db, ctx.area_idx as usize, ab_idx as usize, choice_idx, 0)?; } } else if action >= 600 && action <= 602 && self.pending_effect_opcode == O_TAP_O as i16 { let target_slot = choice_idx; let ab_idx = self.pending_ab_idx.max(0) as usize; self.activate_ability_with_choice(db, ctx.area_idx as usize, ab_idx, 0, target_slot)?; } else if (action >= 580 && action <= 585) && self.pending_effect_opcode == O_COLOR_SELECT as i16 { let start_idx = self.pending_ab_idx.max(0) as usize; self.play_member_with_choice(db, 0, ctx.area_idx as usize, choice_idx, start_idx)?; } else if action >= 560 && action <= 562 { // Stage slot selection (for PLAY_MEMBER_FROM_DISCARD etc.) let ab_idx = self.pending_ab_idx.max(0) as usize; self.activate_ability_with_choice(db, ctx.area_idx as usize, ab_idx, choice_idx, 0)?; } else if action >= 550 { // Generic list selection fallback (context-aware range) let ab_idx = self.pending_ab_idx.max(0) as usize; self.activate_ability_with_choice(db, ctx.area_idx as usize, ab_idx, choice_idx, 0)?; } } else { self.phase = Phase::Main; } }, _ => {} } Ok(()) } pub fn get_legal_action_ids(&self, db: &CardDatabase) -> Vec { let mut actions = Vec::with_capacity(32); self.generate_legal_actions(db, &mut actions); actions } pub fn get_legal_actions(&self, db: &CardDatabase) -> Vec { let mut mask = vec![false; ACTION_SPACE]; self.generate_legal_actions(db, mask.as_mut_slice()); mask } pub fn get_legal_actions_into(&self, db: &CardDatabase, mask: &mut [bool]) { self.generate_legal_actions(db, mask); } pub fn generate_legal_actions(&self, db: &CardDatabase, receiver: &mut R) { receiver.reset(); let p_idx = self.current_player as usize; let player = &self.players[p_idx]; match self.phase { Phase::Main => { receiver.add_action(0); // Optimization 3: Loop Hoisting let available_energy = player.tapped_energy.iter().filter(|&&t| !t).count() as i32; // Pre-calculate stage slot costs let mut slot_costs = [0; 3]; for s in 0..3 { if player.stage[s] >= 0 { if let Some(prev) = db.get_member(player.stage[s] as u16) { slot_costs[s] = prev.cost as i32; } } } // 1. Play Member from Hand for (hand_idx, &cid) in player.hand.iter().enumerate().take(60) { if let Some(card) = db.get_member(cid) { let base_cost = (card.cost as i32 - player.cost_reduction).max(0); for slot_idx in 0..3 { if player.moved_members_this_turn[slot_idx] { continue; } let mut cost = base_cost; if player.stage[slot_idx] >= 0 { cost = (cost - slot_costs[slot_idx]).max(0); if player.baton_touch_count >= player.baton_touch_limit { continue; } } if cost <= available_energy { // Check for OnPlay choices (Limit to first 10 cards to stay within Action ID space) let mut has_choice_on_play = false; if hand_idx < 10 { for ab in &card.abilities { if ab.trigger == TriggerType::OnPlay { // OPTIMIZATION: Use pre-computed flags let has_look_choose = (ab.choice_flags & CHOICE_FLAG_LOOK) != 0; let has_select_mode = (ab.choice_flags & CHOICE_FLAG_MODE) != 0; let has_color_select = (ab.choice_flags & CHOICE_FLAG_COLOR) != 0; let has_order_deck = (ab.choice_flags & CHOICE_FLAG_ORDER) != 0; if has_look_choose || has_order_deck { has_choice_on_play = true; let count = ab.choice_count as i32; for c in 0..count { let choice_aid = 1000 + hand_idx * 100 + slot_idx * 10 + (c as usize); receiver.add_action(choice_aid); } } else if has_color_select { has_choice_on_play = true; for c in 0..6 { let choice_aid = 1000 + hand_idx * 100 + slot_idx * 10 + (c as usize); receiver.add_action(choice_aid); } } else if has_select_mode { has_choice_on_play = true; let count = ab.choice_count as i32; for c in 0..count { let choice_aid = 1000 + hand_idx * 100 + slot_idx * 10 + (c as usize); receiver.add_action(choice_aid); } } } } } if !has_choice_on_play { // Simple ability - no choice needed let aid = 1 + hand_idx * 3 + slot_idx; receiver.add_action(aid); } else { // NEW: We still allow playing choice cards via standard action 1-180 // but it will pause for choice. let aid = 1 + hand_idx * 3 + slot_idx; receiver.add_action(aid); } } } } } // 2. Activate Stage Ability - audit for choice requirements for slot_idx in 0..3 { if let Some(cid) = player.stage.get(slot_idx).filter(|&&c| c >= 0) { if let Some(card) = db.get_member(*cid as u16) { for (ab_idx, ab) in card.abilities.iter().enumerate().take(10) { if ab.trigger == TriggerType::Activated { let ctx = AbilityContext { player_id: self.current_player, area_idx: slot_idx as i16, ..Default::default() }; let cond_ok = ab.conditions.iter().all(|c| self.check_condition(db, p_idx, c, &ctx)); let cost_ok = ab.costs.iter().all(|c| self.check_cost(p_idx, c, &ctx)); if cond_ok && cost_ok { // Check if ability needs choices (has O_LOOK_AND_CHOOSE or O_SELECT_MODE) // OPTIMIZATION: Use pre-computed flags let has_look_choose = (ab.choice_flags & CHOICE_FLAG_LOOK) != 0; let has_select_mode = (ab.choice_flags & CHOICE_FLAG_MODE) != 0; let has_color_select = (ab.choice_flags & CHOICE_FLAG_COLOR) != 0; if has_look_choose { let count = ab.choice_count as i32; for choice in 0..count { let ab_aid = 550 + slot_idx * 100 + ab_idx * 10 + (choice as usize); receiver.add_action(ab_aid); } } else if has_select_mode { let count = ab.choice_count as i32; for choice in 0..count { let ab_aid = 550 + slot_idx * 100 + ab_idx * 10 + (choice as usize); receiver.add_action(ab_aid); } } else if has_color_select { for choice in 0..6 { let ab_aid = 550 + slot_idx * 100 + ab_idx * 10 + (choice as usize); receiver.add_action(ab_aid); } } else { let ab_aid = 200 + slot_idx * 10 + ab_idx; receiver.add_action(ab_aid); } } } } } } } // 3. Activate Discard Ability for discard_idx in 0..self.players[p_idx].discard.len().min(60) { let cid = self.players[p_idx].discard[discard_idx]; if let Some(card) = db.get_member(cid) { for (ab_idx, ab) in card.abilities.iter().enumerate().take(10) { if ab.trigger == TriggerType::Activated { let ctx = AbilityContext { player_id: self.current_player, source_card_id: cid as i16, area_idx: -1, ..Default::default() }; let cond_ok = ab.conditions.iter().all(|c| self.check_condition(db, p_idx, c, &ctx)); let cost_ok = ab.costs.iter().all(|c| self.check_cost(p_idx, c, &ctx)); if cond_ok && cost_ok { if self.check_once_per_turn(p_idx, cid as u16, ab_idx) { // Discard activations use 2000 + idx * 10 + ab_idx let ab_aid = 2000 + discard_idx * 10 + ab_idx; receiver.add_action(ab_aid); } } } } } } } Phase::LiveSet => { receiver.add_action(0); if player.live_zone.iter().any(|&cid| cid == -1) { for i in 0..player.hand.len().min(60) { receiver.add_action(400 + i); } } } Phase::PerformanceP1 | Phase::PerformanceP2 => { receiver.add_action(0); } Phase::MulliganP1 | Phase::MulliganP2 => { // Action 0: Confirm receiver.add_action(0); // Actions 300-359: Toggle (One-way for AI to avoid loops) let hand_len = player.hand.len().min(60); for i in 0..hand_len { // Only legal if NOT already selected if (player.mulligan_selection >> i) & 1 == 0 { receiver.add_action(300 + i); } } } Phase::Active | Phase::Draw | Phase::LiveResult => { receiver.add_action(0); if self.phase == Phase::LiveResult { // Selection choices (600-602) for i in 0..3 { if player.live_zone[i] >= 0 { receiver.add_action(600 + i); } } } } Phase::Energy => { receiver.add_action(0); // Pass/Auto-draw from energy deck let hand_len = player.hand.len().min(60); for i in 0..hand_len { receiver.add_action(100 + i); } } Phase::Response => { // Always allow action 0 as a fallback/wait action receiver.add_action(0); // We only show choices for the pending ability if let Some(ctx) = self.pending_ctx { // Fix: Only generate choices if current player owns the pending ability if ctx.player_id as usize != p_idx { return; } let card_id = self.pending_card_id; let ab_idx = self.pending_ab_idx; let member = db.get_member(card_id as u16); let live = db.get_live(card_id as u16); let abilities = if let Some(m) = member { Some(&m.abilities) } else { live.map(|l| &l.abilities) }; // NEW: Explicitly check pending_effect_opcode first to avoid bytecode sniffing failures if self.pending_effect_opcode == O_TAP_O as i16 { for c in 600..603 { receiver.add_action(c); } return; } if self.pending_effect_opcode == O_ORDER_DECK as i16 { // Selection choice for ORDER_DECK: Choose a card index to put on top let ctx_player = ctx.player_id as usize; let count = self.players[ctx_player].looked_cards.len(); if count > 0 { let base = 550 + (ctx.area_idx as i32 * 100) + (self.pending_ab_idx.max(0) as i32 * 10); for c in 0..=count { let aid = (base + c as i32) as usize; receiver.add_action(aid); } } return; } if self.pending_effect_opcode == O_COLOR_SELECT as i16 { for c in 580..586 { receiver.add_action(c); } return; } if self.pending_effect_opcode == O_LOOK_AND_CHOOSE as i16 { let ctx_player = ctx.player_id as usize; let looked = &self.players[ctx_player].looked_cards; let mut filter_attr = 0; if let Some(abs) = abilities { let ab_idx_real = if ab_idx == -1 { // OPTIMIZATION: Use pre-computed flags abs.iter().position(|ab| (ab.choice_flags & (CHOICE_FLAG_LOOK | CHOICE_FLAG_MODE | CHOICE_FLAG_COLOR | CHOICE_FLAG_ORDER)) != 0).unwrap_or(0) } else { ab_idx as usize }; if let Some(ab) = abs.get(ab_idx_real) { if let Some(chunk) = ab.bytecode.chunks(4).find(|ch| ch[0] == O_LOOK_AND_CHOOSE) { filter_attr = chunk[2]; } } } let base = 550 + (ctx.area_idx as i32 * 100) + (self.pending_ab_idx.max(0) as i32 * 10); for (i, &cid) in looked.iter().enumerate() { if self.card_matches_filter(db, cid, filter_attr) { let aid = (base + i as i32) as usize; receiver.add_action(aid); } } return; } if self.pending_effect_opcode == O_PLAY_MEMBER_FROM_DISCARD as i16 || self.pending_effect_opcode == O_PLAY_MEMBER_FROM_HAND as i16 { for s in 0..3 { if !player.moved_members_this_turn[s] { let aid = 560 + s; receiver.add_action(aid); } } return; } // Fallback to sniffing for other opcodes if let Some(abs) = abilities { let ab_idx_real = if ab_idx == -1 { abs.iter().position(|ab| ab.trigger == TriggerType::OnPlay && bytecode_has_choice(&ab.bytecode)).unwrap_or(0) } else { ab_idx as usize }; if let Some(ab) = abs.get(ab_idx_real) { let mut count = 2; if let Some(chunk) = ab.bytecode.chunks(4).find(|chunk| !chunk.is_empty() && chunk[0] == O_LOOK_AND_CHOOSE) { count = chunk[1].max(1).min(10) as usize; } else if ab.bytecode.chunks(4).any(|chunk| !chunk.is_empty() && chunk[0] == O_SELECT_MODE) { if let Some(chunk) = ab.bytecode.chunks(4).find(|chunk| !chunk.is_empty() && chunk[0] == O_SELECT_MODE) { count = chunk[1].max(1).min(10) as usize; } } let base = 550 + (ctx.area_idx as i32 * 100) + (ab_idx_real as i32 * 10); for c in 0..count { let aid = (base + c as i32) as usize; receiver.add_action(aid); } } } } } _ => { receiver.add_action(0); } } } pub fn integrated_step(&mut self, db: &CardDatabase, action: i32, opp_mode: u8, mcts_sims: usize, enable_rollout: bool) -> (f32, bool) { let prev_score = self.players[0].score as f32; // 1. Agent Step (if interactive) let is_interactive = match self.phase { Phase::Main | Phase::LiveSet | Phase::MulliganP1 | Phase::MulliganP2 | Phase::Response | Phase::LiveResult | Phase::Energy => true, _ => false, }; if self.current_player == 0 && is_interactive { let _ = self.step(db, action); } else { // Auto-progress or illegal call - pass let _ = self.step(db, 0); } // 2. Loop until Agent Turn or Terminal while self.phase != Phase::Terminal { let is_int = match self.phase { Phase::Main | Phase::LiveSet | Phase::MulliganP1 | Phase::MulliganP2 | Phase::Response | Phase::LiveResult | Phase::Energy => true, _ => false, }; if self.current_player == 0 && is_int { break; // Agent's turn reached } if is_int { // Interactive turn for opponent (Player 1) if opp_mode == 1 { // Default to Original Heuristic for normal integrated step // Default to Original Heuristic for normal integrated step // In-lined to support enable_rollout let sims = if mcts_sims > 0 { mcts_sims } else { 1000 }; let mut mcts = MCTS::new(); let heuristic = OriginalHeuristic; let (stats, _) = mcts.search_custom(self, db, sims, 0.0, SearchHorizon::TurnEnd, &heuristic, false, enable_rollout); let best_action = if !stats.is_empty() { stats[0].0 } else { 0 }; let _ = self.step(db, best_action); } else { self.step_opponent(db); } } else { // Non-interactive phase (Energy, Draw, LiveResult, etc.) let _ = self.step(db, 0); } } let current_score = self.players[0].score as f32; let mut reward = current_score - prev_score; // Add win/loss reward if self.phase == Phase::Terminal { let winner = self.get_winner(); if winner == 0 { reward += 100.0; } else if winner == 1 { reward -= 100.0; } } (reward, self.phase == Phase::Terminal) } } // End GameState // ======================================================================================= // Action Receiver Trait (Sparse Generation) // ======================================================================================= pub trait ActionReceiver { fn add_action(&mut self, action_id: usize); fn reset(&mut self); } impl ActionReceiver for [bool] { fn add_action(&mut self, action_id: usize) { if action_id < self.len() { self[action_id] = true; } } fn reset(&mut self) { self.fill(false); } } impl ActionReceiver for Vec { fn add_action(&mut self, action_id: usize) { self.push(action_id); } fn reset(&mut self) { self.clear(); } } impl ActionReceiver for Vec { fn add_action(&mut self, action_id: usize) { self.push(action_id as i32); } fn reset(&mut self) { self.clear(); } } impl GameState { pub fn step_opponent(&mut self, db: &CardDatabase) { // Simple Random Opponent Logic let legal = self.get_legal_actions(db); let legal_indices: Vec = legal.iter().enumerate().filter(|(_, l)| **l).map(|(i, _)| i as i32).collect(); let action = if !legal_indices.is_empty() { let mut rng = Pcg64::from_os_rng(); *legal_indices.choose(&mut rng).unwrap() } else { 0 }; let _ = self.step(db, action); } pub fn step_opponent_mcts(&mut self, db: &CardDatabase, sims: usize, _heuristic: &dyn Heuristic) { let mcts = MCTS::new(); // Default to Blind mode for fair play let stats = mcts.search_parallel_mode(self, db, sims, 0.0, crate::core::mcts::SearchHorizon::GameEnd, EvalMode::Blind); if !stats.is_empty() { let action = stats[0].0; let _ = self.step(db, action); } } pub fn step_opponent_greedy(&mut self, db: &CardDatabase, heuristic: &dyn Heuristic) { let action = self.get_greedy_action(db, heuristic); let _ = self.step(db, action); } pub fn get_mcts_suggestions(&self, db: &CardDatabase, sims: usize, horizon: SearchHorizon, eval_mode: EvalMode) -> Vec<(i32, f32, u32)> { let mcts = MCTS::new(); // Uses the specified eval_mode, which defaults to Blind in bindings mcts.search_parallel_mode(self, db, sims, 0.0, horizon, eval_mode) } pub fn get_greedy_action(&mut self, db: &CardDatabase, heuristic: &dyn Heuristic) -> i32 { let legal = self.get_legal_actions(db); let legal_indices: Vec = legal.iter().enumerate().filter(|(_, l)| **l).map(|(i, _)| i as i32).collect(); if legal_indices.is_empty() { return 0; } let mut best_action = 0; let mut best_score = f32::NEG_INFINITY; // Simple 1-ply search for &action in &legal_indices { let mut state = self.clone(); state.silent = true; // Handle determinization? // For a greedy agent, we assume perfect information or just use current state? // "Not using MCTS" implies we don't do rollouts or determinization usually, // OR we do determinization once? // Let's do determinization to be fair (IS-Greedy). let me = self.current_player as usize; let opp = 1 - me; let opp_hand_len = state.players[opp].hand.len(); let mut unseen: Vec = state.players[opp].hand.iter().cloned().collect(); unseen.extend(state.players[opp].deck.iter().cloned()); let mut rng = Pcg64::from_os_rng(); unseen.shuffle(&mut rng); state.players[opp].hand = unseen.drain(0..opp_hand_len).collect(); state.players[opp].deck = SmallVec::from_vec(unseen); let _ = state.step(db, action); // Eval from perspective of current player (self.current_player) // But state.step might change current_player? // Heuristic evaluate takes baselines. let score = heuristic.evaluate(&state, db, self.players[0].score, self.players[1].score); // Heuristic returns p0 win probability (0.0 to 1.0) // If I am p0, I want to Maximize score. // If I am p1, I want to Minimize score (make it close to 0.0). let my_utility = if me == 0 { score } else { 1.0 - score }; if my_utility > best_score { best_score = my_utility; best_action = action; } } best_action } pub fn play_asymmetric_match(&mut self, db: &CardDatabase, p0_sims: usize, p1_sims: usize, p0_heuristic_id: i32, p1_heuristic_id: i32, horizon: SearchHorizon, p0_rollout: bool, p1_rollout: bool) -> (i32, u32) { let h0: Box = match p0_heuristic_id { 1 => Box::new(SimpleHeuristic), _ => Box::new(OriginalHeuristic), }; let h1: Box = match p1_heuristic_id { 1 => Box::new(SimpleHeuristic), _ => Box::new(OriginalHeuristic), }; let mut loop_count = 0; while self.phase != Phase::Terminal && loop_count < 2000 { loop_count += 1; // Determine who needs to make a decision let acting_player = match self.phase { Phase::Response => { if let Some(ctx) = self.pending_ctx { ctx.player_id } else { self.current_player } }, _ => self.current_player, }; let is_interactive = match self.phase { Phase::Main | Phase::LiveSet | Phase::MulliganP1 | Phase::MulliganP2 | Phase::LiveResult | Phase::Energy | Phase::Response => true, _ => false, }; if is_interactive { let p_idx = acting_player as usize; let sims = if p_idx == 0 { p0_sims } else { p1_sims }; let rollout = if p_idx == 0 { p0_rollout } else { p1_rollout }; let heuristic = if p_idx == 0 { h0.as_ref() } else { h1.as_ref() }; let action = if sims > 0 { let mut mcts = MCTS::new(); let (stats, _) = mcts.search_custom(self, db, sims, 0.0, horizon, heuristic, false, rollout); if !stats.is_empty() { stats[0].0 } else { 0 } } else { self.get_greedy_action(db, heuristic) }; let _ = self.step(db, action); } else { let _ = self.step(db, 0); } } (self.get_winner(), self.turn as u32) } pub fn play_mirror_match(&mut self, db: &CardDatabase, p0_sims: usize, p1_sims: usize, p0_heuristic_id: i32, p1_heuristic_id: i32, horizon: SearchHorizon, enable_rollout: bool) -> (i32, u32) { self.play_asymmetric_match(db, p0_sims, p1_sims, p0_heuristic_id, p1_heuristic_id, horizon, enable_rollout, enable_rollout) } // Optimization 2: Zero Allocation Write pub fn write_observation(&self, db: &CardDatabase, out: &mut [f32]) { let obs = self.encode_state(db); let len = obs.len().min(out.len()); out[..len].copy_from_slice(&obs[..len]); if out.len() > len { for i in len..out.len() { out[i] = 0.0; } } } pub fn get_observation(&self, db: &CardDatabase) -> Vec { self.encode_state(db) } pub fn is_terminal(&self) -> bool { self.phase == Phase::Terminal } pub fn get_winner(&self) -> i32 { if self.phase != Phase::Terminal { return -1; } let s0 = self.players[0].score; let s1 = self.players[1].score; if s0 > s1 { 0 } else if s1 > s0 { 1 } else { 2 } // Draw } }