Skip to content

Commit

Permalink
Rework counters + Mazemind Tome State Triggered Ability
Browse files Browse the repository at this point in the history
  • Loading branch information
radar committed Feb 10, 2024
1 parent fa198e4 commit 619cced
Show file tree
Hide file tree
Showing 25 changed files with 112 additions and 53 deletions.
4 changes: 4 additions & 0 deletions lib/magic/activated_ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/magic/cards/academy_elite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/magic/cards/aron_benalias_ruin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/magic/cards/basri_ket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/magic/cards/basris_solidarity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/magic/cards/feat_of_resistance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/magic/cards/light_of_promise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/magic/cards/makeshift_batallion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 22 additions & 11 deletions lib/magic/cards/mazemind_tome.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions lib/magic/cards/nine_lives.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion lib/magic/cards/scavenging_ooze.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/magic/costs/add_counter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions lib/magic/costs/remove_counter.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 8 additions & 3 deletions lib/magic/effects/add_counter.rb
Original file line number Diff line number Diff line change
@@ -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
"#<Effects::AddCounter source:#{source} counter_type:#{counter_type} amount:#{amount} target:#{target}>"
end

def resolve!
target.add_counter(counter_type)
target.add_counter(counter_type:, amount:)
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/magic/effects/exile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ class Exile < TargetedEffect
def resolve!
target.exile!
end

def inspect
"#<Effects::Exile source:#{source.name} target:#{target.name}>"
end
end
end
end
13 changes: 9 additions & 4 deletions lib/magic/events/counter_added_to_permanent.rb
Original file line number Diff line number Diff line change
@@ -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
"#<Events::CounterAdded permanent: #{permanent.name}, type: #{counter_type}, amount: #{amount}>"
"#<Events::CounterAdded target: #{target.name}, counter_type: #{counter_type}, amount: #{amount}>"
end
alias_method :to_s, :inspect

def permanent
target
end
end
end
Expand Down
12 changes: 11 additions & 1 deletion lib/magic/game.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion lib/magic/game/turn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 24 additions & 10 deletions lib/magic/permanent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion lib/magic/player.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/magic/stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion spec/cards/academy_elite_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions spec/cards/basris_lieutenant_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion spec/cards/mazemind_tome_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 619cced

Please sign in to comment.