From c3675274644cf0fe2545a8c674ff2e4721d391c1 Mon Sep 17 00:00:00 2001 From: manbo Date: Wed, 1 Apr 2026 14:14:26 +0800 Subject: [PATCH] Add card_game_dsl_example.rs --- card_game_dsl_example.rs | 506 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 card_game_dsl_example.rs diff --git a/card_game_dsl_example.rs b/card_game_dsl_example.rs new file mode 100644 index 0000000..416e80d --- /dev/null +++ b/card_game_dsl_example.rs @@ -0,0 +1,506 @@ +// stack data structure +#[derive(Component, Clone, Debug)] +pub struct TriggerStack { + /// LIFO queue of pending triggers + pub stack: Vec, + /// Current player with priority + pub priority_player: PlayerId, + /// Players who have passed priority + pub passed_priority: HashSet, + /// State snapshot for rollback (AI simulation, undo) + pub snapshot_history: Vec, +} + +#[derive(Clone, Debug)] +pub struct TriggerObject { + pub id: TriggerId, + pub source_entity: Entity, + pub trigger_type: TriggerType, + pub controller: PlayerId, + pub priority: TriggerPriority, + pub parameters: TriggerParams, + pub state: TriggerState, // Pending, Resolving, Resolved, Counteracted +} + +#[derive(Clone, Debug, PartialEq)] +pub enum TriggerType { + /// "When you play a card" - Slay the Spire + OnCardPlayed(CardId), + /// "When a creature enters battlefield" - MTG + OnEntersBattlefield(EntityType), + /// "When damage is dealt" - Hearthstone + OnDamageDealt { source: Entity, amount: u32 }, + /// "At start of turn" - All games + OnTurnStart(PlayerId), + /// "When health drops below X" - Conditional + OnHealthThreshold { entity: Entity, threshold: u32 }, + /// Custom game-specific triggers + Custom(String), +} + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub enum TriggerPriority { + /// State-based actions (checked before priority) + StateBased = 0, + /// Replacement effects (modify what happens) + Replacement = 1, + /// Triggered abilities (go on stack) + Triggered = 2, + /// Activated abilities (player chooses) + Activated = 3, + /// Spells (highest level) + Spell = 4, +} + + + +// Stack Resolution System, APNAP Order +pub fn resolve_trigger_stack( + stack: &mut TriggerStack, + game_state: &mut GameState, +) { + loop { + // Check state-based actions first (don't use stack) + check_state_based_actions(game_state); + + // If all players passed priority and stack is empty → next phase + if stack.stack.is_empty() && stack.passed_priority.len() == game_state.player_count { + advance_phase(game_state); + return; + } + + // If all players passed priority, resolve top of stack + if stack.passed_priority.len() == game_state.player_count { + if let Some(trigger) = stack.stack.pop() { + // Clear passed priority for next resolution + stack.passed_priority.clear(); + stack.priority_player = trigger.controller; + + // Execute trigger, may add new triggers to stack + execute_trigger(trigger, stack, game_state); + } + continue; + } + + // Current player with priority can: + // 1. Add spell/ability to stack + // 2. Pass priority + // For AI/simulation, we auto-resolve + if game_state.is_ai_game() { + auto_resolve_priority(stack, game_state); + } else { + // Wait for player input (network message in multiplayer) + return; + } + } +} + +// APNAP Order for simultaneous triggers +pub fn add_simultaneous_triggers( + stack: &mut TriggerStack, + triggers: Vec, + active_player: PlayerId, + player_order: &[PlayerId], +) { + // Group by controller + let mut by_controller: HashMap> = HashMap::new(); + for trigger in triggers { + by_controller.entry(trigger.controller).or_default().push(trigger); + } + + // APNAP: Active Player first (lowest on stack), then Non-Active in turn order + // Last added resolves first (LIFO) + let mut ordered = Vec::new(); + + // Active player's triggers go on stack first (bottom) + if let Some(ap_triggers) = by_controller.remove(&active_player) { + ordered.extend(ap_triggers); + } + + // Non-active players in turn order + for player in player_order { + if *player != active_player { + if let Some(nap_triggers) = by_controller.remove(player) { + ordered.extend(nap_triggers); + } + } + } + + // Add to stack (last in = first to resolve) + stack.stack.extend(ordered); +} + + + +// mbf +#[derive(Component, Clone, Debug)] +pub struct ModifierStack { + /// All active modifiers on this entity + pub modifiers: Vec, + /// Cached computed values (for performance) + pub cached_values: HashMap, + /// Dirty flag for recalculation + pub dirty: bool, +} + +#[derive(Clone, Debug)] +pub struct Modifier { + pub id: ModifierId, + pub source_entity: Entity, + pub modifier_type: ModifierType, + pub value: i32, + pub priority: u32, // Higher priority applies first + pub layer: ModifierLayer, // For same-type modifiers + pub timestamp: u64, // For timestamp order within layer + pub duration: Option, // Turns/rounds remaining + pub is_cumulative: bool, // Stack with same modifiers? +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ModifierType { + Attack, + Health, + MaxHealth, + Cost, + Damage, + Healing, + DrawCount, + CardTargetCount, + // Game-specific + SpellPower, // Hearthstone + Power, // MTG + Strength, // Yu-Gi-Oh + EnergyCost, // Slay the Spire +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ModifierLayer { + /// Base value (card's printed stats) + Base = 0, + /// Characteristic-defining abilities (MTG) + CharacteristicDefining = 1, + /// Control-changing effects + Control = 2, + /// All other effects (timestamp order) + General = 3, + /// Setting to specific value (overwrites all) + Set = 4, + /// Multiplying/dividing + Multiply = 5, + /// Additive (most common) + Additive = 6, +} + +// mbf resolution system +pub fn compute_modified_value( + entity: Entity, + modifier_type: ModifierType, + base_value: i32, + modifiers: &ModifierStack, + game_state: &GameState, +) -> i32 { + if !modifiers.dirty { + return *modifiers.cached_values.get(&modifier_type).unwrap_or(&base_value); + } + + let mut value = base_value; + + // Get all modifiers of this type, sorted by priority → layer → timestamp + let mut applicable: Vec<&Modifier> = modifiers + .modifiers + .iter() + .filter(|m| m.modifier_type == modifier_type) + .collect(); + + applicable.sort_by(|a, b| { + a.priority.cmp(&b.priority) + .then(a.layer.cmp(&b.layer)) + .then(a.timestamp.cmp(&b.timestamp)) + }); + + // Apply in order + for modifier in applicable { + value = match modifier.layer { + ModifierLayer::Set => modifier.value, + ModifierLayer::Multiply => value * modifier.value, + ModifierLayer::Additive => value + modifier.value, + _ => value + modifier.value, // Default to additive + }; + } + + // Cache result + modifiers.cached_values.insert(modifier_type, value); + modifiers.dirty = false; + + value +} + +// Example: "Give all minions +2/+2" (Hearthstone) +pub fn apply_buff( + commands: &mut Commands, + target_query: &Query>, + source: Entity, + attack_buff: i32, + health_buff: i32, + duration: Option, +) { + let timestamp = get_game_timestamp(); + + for entity in target_query.iter() { + commands.entity(entity).with_modifiers(|mods: &mut ModifierStack| { + mods.modifiers.push(Modifier { + id: generate_modifier_id(), + source_entity: source, + modifier_type: ModifierType::Attack, + value: attack_buff, + priority: 100, + layer: ModifierLayer::Additive, + timestamp, + duration, + is_cumulative: true, + }); + mods.modifiers.push(Modifier { + id: generate_modifier_id(), + source_entity: source, + modifier_type: ModifierType::Health, + value: health_buff, + priority: 100, + layer: ModifierLayer::Additive, + timestamp, + duration, + is_cumulative: true, + }); + mods.dirty = true; + }); + } +} + + + + +// ecs core components +#[derive(Component, Clone, Debug)] +pub struct CardComponent { + pub card_id: CardId, + pub card_type: CardType, + pub cost: u32, + pub owner: PlayerId, + pub controller: PlayerId, + pub zone: CardZone, // Hand, Deck, Battlefield, Graveyard, Exile +} + +#[derive(Component, Clone, Debug)] +pub struct StatsComponent { + pub base_attack: i32, + pub base_health: i32, + pub current_attack: i32, // Computed from MBF + pub current_health: i32, // Computed from MBF + pub damage_taken: i32, +} + +#[derive(Component, Clone, Debug)] +pub struct TriggerComponent { + pub triggers: Vec, +} + +#[derive(Clone, Debug)] +pub struct TriggerDefinition { + pub event_type: TriggerType, + pub condition: Option, + pub effects: Vec, + pub is_mandatory: bool, + pub once_per_turn: bool, +} + +// ============ PLAYER COMPONENTS ============ +#[derive(Component, Clone, Debug)] +pub struct PlayerComponent { + pub player_id: PlayerId, + pub hand: Vec, + pub deck: Vec, + pub graveyard: Vec, + pub exile: Vec, + pub health: i32, + pub max_health: i32, + pub resource: u32, // Mana/Energy + pub max_resource: u32, +} + +// ============ GAME STATE ============ +#[derive(Resource, Clone, Debug)] +pub struct GameState { + pub current_player: PlayerId, + pub phase: GamePhase, + pub turn_number: u32, + pub player_order: Vec, + pub stack: TriggerStack, + pub is_multiplayer: bool, + pub authority: AuthorityType, // Server/Client +} + + + +// dsl atomic commands +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum EffectCommand { + // Damage/Healing + DealDamage { amount: u32, target: TargetSelector }, + Heal { amount: u32, target: TargetSelector }, + + // Card Operations + DrawCards { count: u32, player: PlayerId }, + DiscardCards { count: u32, player: PlayerId }, + AddCardToHand { card_id: CardId, player: PlayerId }, + + // Resource + GainResource { amount: u32, player: PlayerId }, + SpendResource { amount: u32, player: PlayerId }, + + // Modifiers + ApplyModifier { modifier: Modifier, target: TargetSelector }, + RemoveModifier { modifier_id: ModifierId, target: TargetSelector }, + + // Movement + MoveCard { entity: Entity, from: CardZone, to: CardZone }, + SummonMinion { card_id: CardId, player: PlayerId, position: BoardPosition }, + + // Trigger Management + AddTrigger { trigger: TriggerDefinition, target: Entity }, + RemoveTrigger { trigger_id: TriggerId, target: Entity }, + + // Game State + SetPhase { phase: GamePhase }, + EndTurn, + WinGame { player: PlayerId }, + LoseGame { player: PlayerId }, + + // Conditional + IfCondition { + condition: EffectCondition, + then_effects: Vec, + else_effects: Option>, + }, + + // Repeat + ForEach { + targets: TargetSelector, + effects: Vec, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum TargetSelector { + Self, + RandomEnemy, + AllEnemies, + AllAllies, + SpecificEntity(Entity), + LowestHealthEnemy, + HighestAttackEnemy, + // MTG-style + TargetCreature, + TargetPlayer, + TargetArtifact, + // Yu-Gi-Oh-style + TargetMonster, + TargetSpellTrap, +} + + + + +/// Examples +/// Slay the Spire +// "When you play a card, draw 1 card" +TriggerDefinition { + event_type: TriggerType::OnCardPlayed(CardId::Any), + condition: Some(TriggerCondition::PlayerControlled), + effects: vec![EffectCommand::DrawCards { count: 1, player: TriggerSource }], + is_mandatory: true, + once_per_turn: false, +} + +// "When you take damage, gain 1 block" +TriggerDefinition { + event_type: TriggerType::OnDamageTaken { min_amount: 1 }, + condition: None, + effects: vec![EffectCommand::ApplyModifier { + modifier: Modifier::new_block(1), + target: TargetSelector::Self, + }], + is_mandatory: true, + once_per_turn: false, +} + + + +/// Magic: The Gathering +// "Whenever a creature enters the battlefield, draw a card" +TriggerDefinition { + event_type: TriggerType::OnEntersBattlefield(EntityType::Creature), + condition: Some(TriggerCondition::ControllerIsSelf), + effects: vec![EffectCommand::DrawCards { count: 1, player: TriggerSource }], + is_mandatory: true, + once_per_turn: false, +} + +// "At the beginning of your upkeep, sacrifice a creature" +TriggerDefinition { + event_type: TriggerType::OnTurnStart(TriggerSource), + condition: Some(TriggerCondition::Phase(Phase::Upkeep)), + effects: vec![EffectCommand::SelectAndSacrifice { + selector: TargetSelector::TargetCreature, + count: 1, + }], + is_mandatory: true, + once_per_turn: true, +} + + + +/// Hearthstone +// "Whenever you summon a minion, deal 1 damage to all enemies" +TriggerDefinition { + event_type: TriggerType::OnSummon(EntityType::Minion), + condition: Some(TriggerCondition::ControllerIsSelf), + effects: vec![EffectCommand::DealDamage { + amount: 1, + target: TargetSelector::AllEnemies, + }], + is_mandatory: true, + once_per_turn: false, +} + +// "Deathrattle: Draw a card" +TriggerDefinition { + event_type: TriggerType::OnDeath, + condition: None, + effects: vec![EffectCommand::DrawCards { count: 1, player: Owner }], + is_mandatory: true, + once_per_turn: false, +} + + + +/// Yu-Gi-Oh! +// "When this card is Normal Summoned: You can add 1 Spell from deck to hand" +TriggerDefinition { + event_type: TriggerType::OnNormalSummon, + condition: Some(TriggerCondition::ThisCard), + effects: vec![EffectCommand::SearchDeck { + card_type: CardType::Spell, + count: 1, + to: Zone::Hand, + }], + is_mandatory: false, // Optional trigger + once_per_turn: true, +} + +// "When a monster declares an attack: Negate the attack" +TriggerDefinition { + event_type: TriggerType::OnAttackDeclared, + condition: Some(TriggerCondition::FacingThisCard), + effects: vec![EffectCommand::NegateAttack], + is_mandatory: true, + once_per_turn: false, +}