From 619cced831ece5289d165cd5b57bcb976e65958b Mon Sep 17 00:00:00 2001 From: Ryan Bigg Date: Sun, 11 Feb 2024 08:12:02 +1100 Subject: [PATCH] Rework counters + Mazemind Tome State Triggered Ability --- lib/magic/activated_ability.rb | 4 +++ lib/magic/cards/academy_elite.rb | 2 +- lib/magic/cards/aron_benalias_ruin.rb | 2 +- lib/magic/cards/basri_ket.rb | 4 +-- lib/magic/cards/basris_solidarity.rb | 2 +- lib/magic/cards/feat_of_resistance.rb | 2 +- lib/magic/cards/light_of_promise.rb | 2 +- lib/magic/cards/makeshift_batallion.rb | 2 +- lib/magic/cards/mazemind_tome.rb | 33 ++++++++++++------ lib/magic/cards/nine_lives.rb | 3 +- lib/magic/cards/scavenging_ooze.rb | 2 +- lib/magic/costs/add_counter.rb | 2 +- lib/magic/costs/remove_counter.rb | 10 +++--- lib/magic/effects/add_counter.rb | 11 ++++-- lib/magic/effects/exile.rb | 4 +++ .../events/counter_added_to_permanent.rb | 13 ++++--- lib/magic/game.rb | 12 ++++++- lib/magic/game/turn.rb | 3 +- lib/magic/permanent.rb | 34 +++++++++++++------ lib/magic/player.rb | 2 +- lib/magic/stack.rb | 1 + spec/cards/academy_elite_spec.rb | 2 +- spec/cards/basris_lieutenant_spec.rb | 4 +-- spec/cards/mazemind_tome_spec.rb | 7 +++- spec/cards/tempered_veteran_spec.rb | 2 +- 25 files changed, 112 insertions(+), 53 deletions(-) diff --git a/lib/magic/activated_ability.rb b/lib/magic/activated_ability.rb index 812714f..a4ec351 100644 --- a/lib/magic/activated_ability.rb +++ b/lib/magic/activated_ability.rb @@ -6,6 +6,10 @@ def self.costs(costs) end end + def name + self.class.name + end + def valid_targets?(*targets) targets.all? { target_choices.include?(_1) } end diff --git a/lib/magic/cards/academy_elite.rb b/lib/magic/cards/academy_elite.rb index 4d82626..fda9306 100644 --- a/lib/magic/cards/academy_elite.rb +++ b/lib/magic/cards/academy_elite.rb @@ -11,7 +11,7 @@ class AcademyElite < Creature class ETB < TriggeredAbility::EnterTheBattlefield def perform counters = game.graveyard_cards.by_any_type(T::Instant, T::Sorcery).count - source.add_counter(Counters::Plus1Plus1, amount: counters) + source.trigger_effect(:add_counter, target: source, counter_type: Counters::Plus1Plus1, amount: counters) end end diff --git a/lib/magic/cards/aron_benalias_ruin.rb b/lib/magic/cards/aron_benalias_ruin.rb index 73f0c8f..1cd20f5 100644 --- a/lib/magic/cards/aron_benalias_ruin.rb +++ b/lib/magic/cards/aron_benalias_ruin.rb @@ -13,7 +13,7 @@ class ActivatedAbility < Magic::ActivatedAbility def resolve! creatures_you_control.each do |creature| - creature.add_counter(Counters::Plus1Plus1) + creature.add_counter(counter_type: Counters::Plus1Plus1) end end end diff --git a/lib/magic/cards/basri_ket.rb b/lib/magic/cards/basri_ket.rb index 271a0dd..929640d 100644 --- a/lib/magic/cards/basri_ket.rb +++ b/lib/magic/cards/basri_ket.rb @@ -15,7 +15,7 @@ def receive_event(event) owner.create_token(token_class: SoldierToken) owner.creatures.each do |creature| - creature.add_counter(Counters::Plus1Plus1) + creature.add_counter(counter_type: Counters::Plus1Plus1) end end end @@ -36,7 +36,7 @@ def single_target? end def resolve!(target:) - target.add_counter(Counters::Plus1Plus1) + target.add_counter(counter_type: Counters::Plus1Plus1) target.grant_keyword(Keywords::INDESTRUCTIBLE, until_eot: true) end end diff --git a/lib/magic/cards/basris_solidarity.rb b/lib/magic/cards/basris_solidarity.rb index eaf65e4..0142551 100644 --- a/lib/magic/cards/basris_solidarity.rb +++ b/lib/magic/cards/basris_solidarity.rb @@ -7,7 +7,7 @@ module Cards class BasrisSolidarity < Sorcery def resolve! controller.creatures.each do |creature| - creature.add_counter(Counters::Plus1Plus1) + creature.add_counter(counter_type: Counters::Plus1Plus1) end super diff --git a/lib/magic/cards/feat_of_resistance.rb b/lib/magic/cards/feat_of_resistance.rb index eded4ae..da440bf 100644 --- a/lib/magic/cards/feat_of_resistance.rb +++ b/lib/magic/cards/feat_of_resistance.rb @@ -26,7 +26,7 @@ def resolve!(color:) end def resolve!(target:) - target.add_counter(Counters::Plus1Plus1) + target.add_counter(counter_type: Counters::Plus1Plus1) game.choices.add(Choice.new(source: self, target: target)) end end diff --git a/lib/magic/cards/light_of_promise.rb b/lib/magic/cards/light_of_promise.rb index 66be1e2..3768070 100644 --- a/lib/magic/cards/light_of_promise.rb +++ b/lib/magic/cards/light_of_promise.rb @@ -13,7 +13,7 @@ def event_handlers # Whenever you gain life, put that many +1/+1 counters on this creature.” Events::LifeGain => -> (receiver, event) do return unless event.player == receiver.controller - receiver.attached_to.add_counter(Counters::Plus1Plus1, amount: event.life) + receiver.attached_to.add_counter(counter_type: Counters::Plus1Plus1, amount: event.life) end } end diff --git a/lib/magic/cards/makeshift_batallion.rb b/lib/magic/cards/makeshift_batallion.rb index b11af59..dcb3685 100644 --- a/lib/magic/cards/makeshift_batallion.rb +++ b/lib/magic/cards/makeshift_batallion.rb @@ -16,7 +16,7 @@ def event_handlers return if attacks.none? { |event| event.attacker == receiver } return if attacks.count < 3 - receiver.add_counter(Counters::Plus1Plus1) + receiver.add_counter(counter_type: Counters::Plus1Plus1) end } end diff --git a/lib/magic/cards/mazemind_tome.rb b/lib/magic/cards/mazemind_tome.rb index 400dcfc..5fa6cd6 100644 --- a/lib/magic/cards/mazemind_tome.rb +++ b/lib/magic/cards/mazemind_tome.rb @@ -34,17 +34,28 @@ def resolve! def activated_abilities = [ActivatedAbility, ActivatedAbility2] - def event_handlers - { - Events::CounterAddedToPermanent => ->(receiver, event) { - return unless event.permanent == receiver - - if receiver.counters.of_type(Magic::Counters::Page).count >= 4 - receiver.trigger_effect(:exile, target: receiver) - receiver.trigger_effect(:gain_life, life: 4) - end - } - } + def state_triggered_abilities = [StateTriggeredAbility] + + class StateTriggeredAbility + attr_reader :source + def initialize(source:) + @source = source + end + + def name + self.class + end + + def condition_met? + source.counters.of_type(Magic::Counters::Page).count >= 4 + end + + def resolve! + if source.zone.battlefield? + source.trigger_effect(:exile, target: source) + source.trigger_effect(:gain_life, life: 4) + end + end end end end diff --git a/lib/magic/cards/nine_lives.rb b/lib/magic/cards/nine_lives.rb index 80744fd..a0569b7 100644 --- a/lib/magic/cards/nine_lives.rb +++ b/lib/magic/cards/nine_lives.rb @@ -9,8 +9,7 @@ class NineLives < Enchantment def replacement_effects { Events::LifeLoss => -> (receiver, event) do - receiver.trigger_effect( - :add_counter, + Events::CounterAddedToPermanent.new( source: receiver, counter_type: Counters::Incarnation, target: receiver, diff --git a/lib/magic/cards/scavenging_ooze.rb b/lib/magic/cards/scavenging_ooze.rb index 6c793b5..0111732 100644 --- a/lib/magic/cards/scavenging_ooze.rb +++ b/lib/magic/cards/scavenging_ooze.rb @@ -20,7 +20,7 @@ def target_choices def resolve!(target:) trigger_effect(:exile, target: target) if target.creature? - source.add_counter(Counters::Plus1Plus1) + source.add_counter(counter_type: Counters::Plus1Plus1) source.controller.gain_life(1) end end diff --git a/lib/magic/costs/add_counter.rb b/lib/magic/costs/add_counter.rb index a75a29f..13719d4 100644 --- a/lib/magic/costs/add_counter.rb +++ b/lib/magic/costs/add_counter.rb @@ -9,7 +9,7 @@ def initialize(counter_type:, target:) end def finalize!(_player) - target.add_counter(counter_type) + target.add_counter(counter_type: counter_type) end end end diff --git a/lib/magic/costs/remove_counter.rb b/lib/magic/costs/remove_counter.rb index f1d17ea..ce05cfa 100644 --- a/lib/magic/costs/remove_counter.rb +++ b/lib/magic/costs/remove_counter.rb @@ -1,19 +1,19 @@ module Magic module Costs class RemoveCounter - attr_reader :source, :counter_class + attr_reader :source, :counter_type - def initialize(source, counter_class) + def initialize(source, counter_type) @source = source - @counter_class = counter_class + @counter_type = counter_type end def can_pay? - source.counters.count(counter_class) > 0 + source.counters.count(counter_type) > 0 end def finalize!(_player) - source.remove_counter(counter_class) + source.remove_counter(counter_type: counter_type) end end end diff --git a/lib/magic/effects/add_counter.rb b/lib/magic/effects/add_counter.rb index 62815b0..5a0f0f7 100644 --- a/lib/magic/effects/add_counter.rb +++ b/lib/magic/effects/add_counter.rb @@ -1,15 +1,20 @@ module Magic module Effects class AddCounter < TargetedEffect - attr_reader :counter_type + attr_reader :counter_type, :amount - def initialize(counter_type:, **args) + def initialize(counter_type:, amount: 1, **args) + @amount = amount @counter_type = counter_type super(**args) end + def inspect + "#" + end + def resolve! - target.add_counter(counter_type) + target.add_counter(counter_type:, amount:) end end end diff --git a/lib/magic/effects/exile.rb b/lib/magic/effects/exile.rb index 777ac27..974c03c 100644 --- a/lib/magic/effects/exile.rb +++ b/lib/magic/effects/exile.rb @@ -4,6 +4,10 @@ class Exile < TargetedEffect def resolve! target.exile! end + + def inspect + "#" + end end end end diff --git a/lib/magic/events/counter_added_to_permanent.rb b/lib/magic/events/counter_added_to_permanent.rb index 99a8eba..60c5ef1 100644 --- a/lib/magic/events/counter_added_to_permanent.rb +++ b/lib/magic/events/counter_added_to_permanent.rb @@ -1,16 +1,21 @@ module Magic module Events class CounterAddedToPermanent - attr_reader :permanent, :counter_type, :amount + attr_reader :source, :target, :counter_type, :amount - def initialize(permanent: nil, counter_type:, amount:) - @permanent = permanent + def initialize(source:, target:, counter_type:, amount: 1) + @target = target @counter_type = counter_type @amount = amount end def inspect - "#" + "#" + end + alias_method :to_s, :inspect + + def permanent + target end end end diff --git a/lib/magic/game.rb b/lib/magic/game.rb index 2abae60..3ac89a5 100644 --- a/lib/magic/game.rb +++ b/lib/magic/game.rb @@ -101,12 +101,22 @@ def tick! move_dead_creatures_to_graveyard end + # Rule 603.8 + def check_for_state_triggered_abilities + abilities = battlefield.flat_map(&:state_triggered_abilities).select(&:condition_met?) + # Sub rule: A state-triggered ability doesn't trigger again until the ability has resolved, has been countered, or has otherwise left the stack. + abilities = abilities.reject { |ability| stack.include?(ability) } + abilities.each do + stack.add(_1) + end + end + def graveyard_cards CardList.new(players.flat_map { _1.graveyard.cards }) end def move_dead_creatures_to_graveyard battlefield.creatures.dead.each(&:destroy!) - end\ + end end end diff --git a/lib/magic/game/turn.rb b/lib/magic/game/turn.rb index c348d58..00bbbc7 100644 --- a/lib/magic/game/turn.rb +++ b/lib/magic/game/turn.rb @@ -194,8 +194,9 @@ def notify!(*events) replacement_sources = replacement_effect_sources(event) # TODO: Handle multiple replacement effects -- player gets to choose which one to pick if replacement_sources.any? - logger.debug " EVENT REPLACED! Replaced by: #{replacement_sources.first}" event = replacement_sources.first.handle_replacement_effect(event) + logger.debug " EVENT REPLACED! Replaced by: #{replacement_sources.first}" + logger.debug " New Event -> #{event}" end if event diff --git a/lib/magic/permanent.rb b/lib/magic/permanent.rb index 71c64e7..25f0710 100644 --- a/lib/magic/permanent.rb +++ b/lib/magic/permanent.rb @@ -8,7 +8,22 @@ class Permanent include Types extend Forwardable - attr_reader :game, :owner, :controller, :card, :types, :delayed_responses, :attachments, :protections, :modifiers, :counters, :keywords, :keyword_grants, :activated_abilities, :exiled_cards, :cannot_untap_next_turn + attr_reader :game, + :owner, + :controller, + :card, + :types, + :delayed_responses, + :attachments, + :protections, + :modifiers, + :counters, + :keywords, + :keyword_grants, + :activated_abilities, + :state_triggered_abilities, + :exiled_cards, + :cannot_untap_next_turn def_delegators :@card, :name, :cmc, :mana_value, :colors, :colorless?, :opponents def_delegators :@game, :logger @@ -75,6 +90,10 @@ def activated_abilities @activated_abilities ||= card.activated_abilities.map { |ability| ability.new(source: self) } end + def state_triggered_abilities + @state_triggered_abilities ||= card.state_triggered_abilities.map { |ability| ability.new(source: self) } + end + alias_method :to_s, :inspect def controller?(other_controller) @@ -123,6 +142,8 @@ def receive_notification(event) trigger_delayed_response(event) case event + when Events::CounterAddedToPermanent + add_counter(counter_type: event.counter_type, amount: event.amount) if event.permanent == self when Events::CreatureDied died! if event.permanent == self when Events::PermanentLeavingZone @@ -269,18 +290,11 @@ def cleanup! remove_until_eot_modifiers! end - def add_counter(counter_type, amount: 1) + def add_counter(counter_type:, amount: 1) @counters = Counters::Collection.new(@counters + [counter_type.new] * amount) - counter_added = Events::CounterAddedToPermanent.new( - permanent: self, - counter_type: counter_type, - amount: amount - ) - - game.notify!(counter_added) end - def remove_counter(counter_type, amount: 1) + def remove_counter(counter_type:, amount: 1) removable_counters = @counters.select { |counter| counter.is_a?(counter_type) }.first(amount) if removable_counters.count < amount raise "Not enough #{counter_type} counters to remove" diff --git a/lib/magic/player.rb b/lib/magic/player.rb index 832eec4..802bd0c 100644 --- a/lib/magic/player.rb +++ b/lib/magic/player.rb @@ -292,7 +292,7 @@ def protected_from?(card) permanents.flat_map { |card| card.protections.player }.any? { |protection| protection.protected_from?(card) } end - def add_counter(counter_type, amount: 1) + def add_counter(counter_type:, amount: 1) @counters = Counters::Collection.new(@counters + [counter_type.new] * amount) counter_added = Events::CounterAddedToPlayer.new( player: self, diff --git a/lib/magic/stack.rb b/lib/magic/stack.rb index 8768457..3f8c92a 100644 --- a/lib/magic/stack.rb +++ b/lib/magic/stack.rb @@ -37,6 +37,7 @@ def initialize(logger: Logger.new($stdout), stack: [], effects: [], choices: []) end def add(item) + logger.debug "Item added to stack: #{item}" @stack.unshift(item) end diff --git a/spec/cards/academy_elite_spec.rb b/spec/cards/academy_elite_spec.rb index 74f3f00..aeca554 100644 --- a/spec/cards/academy_elite_spec.rb +++ b/spec/cards/academy_elite_spec.rb @@ -58,7 +58,7 @@ let(:academy_elite) { ResolvePermanent("Academy Elite", owner: p1) } before do - academy_elite.add_counter(Magic::Counters::Plus1Plus1) + academy_elite.add_counter(counter_type: Magic::Counters::Plus1Plus1) end it "removes a +1/+1 counter, draws a card and discards a card" do diff --git a/spec/cards/basris_lieutenant_spec.rb b/spec/cards/basris_lieutenant_spec.rb index 065b353..4d1df83 100644 --- a/spec/cards/basris_lieutenant_spec.rb +++ b/spec/cards/basris_lieutenant_spec.rb @@ -25,7 +25,7 @@ context "on death -- when basri's lieutenant is on the battlefield" do context "when the death is this card and it had a counter" do before do - subject.add_counter(Magic::Counters::Plus1Plus1) + subject.add_counter(counter_type: Magic::Counters::Plus1Plus1) end it "creates a 2/2 white knight creature token with vigilance" do @@ -43,7 +43,7 @@ let(:elves) { ResolvePermanent("Wood Elves", owner: p1) } before do subject - elves.add_counter(Magic::Counters::Plus1Plus1) + elves.add_counter(counter_type: Magic::Counters::Plus1Plus1) end it "creates a 2/2 white knight creature token with vigilance" do diff --git a/spec/cards/mazemind_tome_spec.rb b/spec/cards/mazemind_tome_spec.rb index 0634485..0c5564e 100644 --- a/spec/cards/mazemind_tome_spec.rb +++ b/spec/cards/mazemind_tome_spec.rb @@ -36,7 +36,12 @@ end it "when there are four or more page counters, exile it. Gain 4 life." do - subject.add_counter(Magic::Counters::Page, amount: 4) + subject.trigger_effect(:add_counter, counter_type: Magic::Counters::Page, target: subject, amount: 4) + + game.check_for_state_triggered_abilities + expect(game.stack).to include(subject.state_triggered_abilities.first) + + game.stack.resolve! expect(subject.card.zone).to be_exile expect(p1.life).to eq(24) diff --git a/spec/cards/tempered_veteran_spec.rb b/spec/cards/tempered_veteran_spec.rb index f39c1d1..7eea5ce 100644 --- a/spec/cards/tempered_veteran_spec.rb +++ b/spec/cards/tempered_veteran_spec.rb @@ -11,7 +11,7 @@ it "adds a counter to a creature with a counter" do p1.add_mana(white: 2) - wood_elves.add_counter(Magic::Counters::Plus1Plus1) + wood_elves.add_counter(counter_type: Magic::Counters::Plus1Plus1) p1.activate_ability(ability: ability) do _1.targeting(wood_elves).pay_mana(white: 1) end