trioskosmos's picture
Upload folder using huggingface_hub
88d4171 verified
raw
history blame
241 kB
#![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<Effect>,
#[serde(default)]
pub conditions: Vec<Condition>,
#[serde(default)]
pub costs: Vec<Cost>,
#[serde(default)]
pub is_once_per_turn: bool,
#[serde(default)]
pub bytecode: Vec<i32>,
#[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<u8>,
pub units: Vec<u8>,
pub abilities: Vec<Ability>,
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<Ability>,
pub groups: Vec<u8>,
pub units: Vec<u8>,
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<String>, // 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<String>,
#[serde(skip)]
pub performance_results: HashMap<u8, serde_json::Value>,
#[serde(skip)]
pub last_performance_results: HashMap<u8, serde_json::Value>,
#[serde(default)]
#[serde(skip)]
pub performance_history: Vec<serde_json::Value>,
#[serde(default)]
pub trigger_depth: u16,
#[serde(default)]
pub live_set_pending_draws: [u8; 2],
#[serde(default)]
pub pending_ctx: Option<AbilityContext>,
#[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<u16, MemberCard>,
pub lives: HashMap<u16, LiveCard>,
// Optimization 1: Fast Lookup Vectors
pub members_vec: Vec<Option<MemberCard>>,
pub lives_vec: Vec<Option<LiveCard>>,
}
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<Self> {
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::<MemberCard>(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::<LiveCard>(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<u16>, p1_deck: Vec<u16>, p0_energy: Vec<u16>, p1_energy: Vec<u16>, p0_lives: Vec<u16>, p1_lives: Vec<u16>) {
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<u16>, p1_deck: Vec<u16>, p0_energy: Vec<u16>, p1_energy: Vec<u16>, p0_lives: Vec<u16>, p1_lives: Vec<u16>, seed: Option<u64>) {
// 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::<Vec<_>>();
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<f32> {
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<f32>, 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<f32>, 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<f32>, 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<u16> = 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<i32> {
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<usize> = 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::<u8>() as i32;
}
h += p.heart_buffs[i].iter().sum::<i32>();
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<u16> {
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<i16> = 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<u32>) -> 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<usize>) {
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<usize> = 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<i32> {
let mut actions = Vec::with_capacity(32);
self.generate_legal_actions(db, &mut actions);
actions
}
pub fn get_legal_actions(&self, db: &CardDatabase) -> Vec<bool> {
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<R: ActionReceiver + ?Sized>(&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<usize> {
fn add_action(&mut self, action_id: usize) {
self.push(action_id);
}
fn reset(&mut self) {
self.clear();
}
}
impl ActionReceiver for Vec<i32> {
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<i32> = 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<i32> = 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<u16> = 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<dyn Heuristic> = match p0_heuristic_id {
1 => Box::new(SimpleHeuristic),
_ => Box::new(OriginalHeuristic),
};
let h1: Box<dyn Heuristic> = 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<f32> {
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
}
}