Add card_game_dsl_example.rs

This commit is contained in:
2026-04-01 14:14:26 +08:00
parent e8a0b491ea
commit c367527464

506
card_game_dsl_example.rs Normal file
View File

@@ -0,0 +1,506 @@
// stack data structure
#[derive(Component, Clone, Debug)]
pub struct TriggerStack {
/// LIFO queue of pending triggers
pub stack: Vec<TriggerObject>,
/// Current player with priority
pub priority_player: PlayerId,
/// Players who have passed priority
pub passed_priority: HashSet<PlayerId>,
/// State snapshot for rollback (AI simulation, undo)
pub snapshot_history: Vec<GameStateSnapshot>,
}
#[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<TriggerObject>,
active_player: PlayerId,
player_order: &[PlayerId],
) {
// Group by controller
let mut by_controller: HashMap<PlayerId, Vec<TriggerObject>> = 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<Modifier>,
/// Cached computed values (for performance)
pub cached_values: HashMap<ModifierType, i32>,
/// 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<u32>, // 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<Entity, With<MinionComponent>>,
source: Entity,
attack_buff: i32,
health_buff: i32,
duration: Option<u32>,
) {
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<TriggerDefinition>,
}
#[derive(Clone, Debug)]
pub struct TriggerDefinition {
pub event_type: TriggerType,
pub condition: Option<TriggerCondition>,
pub effects: Vec<EffectDefinition>,
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<Entity>,
pub deck: Vec<Entity>,
pub graveyard: Vec<Entity>,
pub exile: Vec<Entity>,
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<PlayerId>,
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<EffectCommand>,
else_effects: Option<Vec<EffectCommand>>,
},
// Repeat
ForEach {
targets: TargetSelector,
effects: Vec<EffectCommand>,
},
}
#[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,
}