enjine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +201 -0
- data/SUPPORTED.md +387 -0
- data/docs/ARCHITECTURE.md +141 -0
- data/docs/DESIGN.md +139 -0
- data/docs/MECHANICS_COVERAGE.md +670 -0
- data/docs/guides/adding-a-mechanic.md +60 -0
- data/docs/guides/api.md +614 -0
- data/docs/guides/card-data-format.md +1472 -0
- data/docs/guides/deck-builder.md +157 -0
- data/docs/plans/standard.md +371 -0
- data/docs/reference/abilities.md +1680 -0
- data/docs/reference/costs.md +339 -0
- data/docs/reference/counters.md +121 -0
- data/docs/reference/decisions.md +102 -0
- data/docs/reference/effects.md +2340 -0
- data/docs/reference/filters.md +172 -0
- data/docs/reference/keywords.md +647 -0
- data/docs/reference/subtypes.md +169 -0
- data/docs/reference/supertypes.md +31 -0
- data/docs/reference/triggers.md +797 -0
- data/docs/reference/types.md +216 -0
- data/exe/enjine +7 -0
- data/lib/enjine/actions/activate_mana_ability.rb +24 -0
- data/lib/enjine/actions/cast.rb +57 -0
- data/lib/enjine/actions/pass.rb +18 -0
- data/lib/enjine/actions/play_land.rb +52 -0
- data/lib/enjine/actions/special_action.rb +37 -0
- data/lib/enjine/actions.rb +20 -0
- data/lib/enjine/agents/busy.rb +27 -0
- data/lib/enjine/agents/random.rb +28 -0
- data/lib/enjine/card_definition.rb +433 -0
- data/lib/enjine/card_faces.rb +116 -0
- data/lib/enjine/catalog.rb +56 -0
- data/lib/enjine/characteristics.rb +39 -0
- data/lib/enjine/cli/text.rb +295 -0
- data/lib/enjine/cli.rb +312 -0
- data/lib/enjine/decisions/bottom.rb +29 -0
- data/lib/enjine/decisions/casting.rb +188 -0
- data/lib/enjine/decisions/combat.rb +75 -0
- data/lib/enjine/decisions/discard.rb +29 -0
- data/lib/enjine/decisions/effect_choice.rb +21 -0
- data/lib/enjine/decisions/enter_choice.rb +26 -0
- data/lib/enjine/decisions/legend_rule.rb +21 -0
- data/lib/enjine/decisions/mulligan.rb +26 -0
- data/lib/enjine/decisions/order_triggers.rb +21 -0
- data/lib/enjine/decisions/play_or_draw.rb +19 -0
- data/lib/enjine/decisions/priority.rb +22 -0
- data/lib/enjine/decisions/untap_choice.rb +35 -0
- data/lib/enjine/decisions.rb +22 -0
- data/lib/enjine/deck.rb +151 -0
- data/lib/enjine/deck_builder.rb +454 -0
- data/lib/enjine/docs.rb +90 -0
- data/lib/enjine/engine/ability_layers.rb +118 -0
- data/lib/enjine/engine/attachments.rb +86 -0
- data/lib/enjine/engine/casting/alt_casting.rb +254 -0
- data/lib/enjine/engine/casting/cast_choices.rb +292 -0
- data/lib/enjine/engine/casting/casting.rb +326 -0
- data/lib/enjine/engine/casting/cost_options.rb +99 -0
- data/lib/enjine/engine/casting/costs.rb +117 -0
- data/lib/enjine/engine/casting/payment.rb +353 -0
- data/lib/enjine/engine/casting/permissions.rb +412 -0
- data/lib/enjine/engine/casting/target_counts.rb +113 -0
- data/lib/enjine/engine/casting/waterbend.rb +90 -0
- data/lib/enjine/engine/characteristics.rb +379 -0
- data/lib/enjine/engine/class_levels.rb +25 -0
- data/lib/enjine/engine/combat.rb +315 -0
- data/lib/enjine/engine/copies.rb +208 -0
- data/lib/enjine/engine/damage.rb +196 -0
- data/lib/enjine/engine/designations.rb +46 -0
- data/lib/enjine/engine/emblems.rb +37 -0
- data/lib/enjine/engine/entering.rb +156 -0
- data/lib/enjine/engine/face_down.rb +30 -0
- data/lib/enjine/engine/faces.rb +222 -0
- data/lib/enjine/engine/follow_ups.rb +27 -0
- data/lib/enjine/engine/mana_sources.rb +230 -0
- data/lib/enjine/engine/phasing.rb +74 -0
- data/lib/enjine/engine/pregame.rb +95 -0
- data/lib/enjine/engine/priority.rb +47 -0
- data/lib/enjine/engine/resolution.rb +168 -0
- data/lib/enjine/engine/rules.rb +287 -0
- data/lib/enjine/engine/sagas.rb +41 -0
- data/lib/enjine/engine/speed.rb +104 -0
- data/lib/enjine/engine/spell_grants.rb +46 -0
- data/lib/enjine/engine/state_based_actions.rb +157 -0
- data/lib/enjine/engine/triggers.rb +364 -0
- data/lib/enjine/engine/turn_history.rb +240 -0
- data/lib/enjine/engine/turn_structure.rb +224 -0
- data/lib/enjine/engine/zones.rb +278 -0
- data/lib/enjine/engine.rb +55 -0
- data/lib/enjine/errors.rb +62 -0
- data/lib/enjine/events.rb +161 -0
- data/lib/enjine/frames.rb +84 -0
- data/lib/enjine/fuzz.rb +112 -0
- data/lib/enjine/game.rb +204 -0
- data/lib/enjine/history.rb +26 -0
- data/lib/enjine/mana.rb +142 -0
- data/lib/enjine/mechanics/abilities/additional_triggers.rb +86 -0
- data/lib/enjine/mechanics/abilities/chosen_values.rb +213 -0
- data/lib/enjine/mechanics/abilities/cost_changes.rb +154 -0
- data/lib/enjine/mechanics/abilities/damage_replacements.rb +569 -0
- data/lib/enjine/mechanics/abilities/double_counters.rb +71 -0
- data/lib/enjine/mechanics/abilities/entering_choices.rb +91 -0
- data/lib/enjine/mechanics/abilities/entering_copy.rb +69 -0
- data/lib/enjine/mechanics/abilities/enters_tapped.rb +159 -0
- data/lib/enjine/mechanics/abilities/enters_with_counters.rb +116 -0
- data/lib/enjine/mechanics/abilities/mana_ability.rb +209 -0
- data/lib/enjine/mechanics/abilities/play_permissions.rb +527 -0
- data/lib/enjine/mechanics/abilities/player_rules.rb +169 -0
- data/lib/enjine/mechanics/abilities/replacement_effects.rb +368 -0
- data/lib/enjine/mechanics/abilities/restrictions.rb +144 -0
- data/lib/enjine/mechanics/abilities/spell_keywords.rb +55 -0
- data/lib/enjine/mechanics/abilities/stack_abilities.rb +542 -0
- data/lib/enjine/mechanics/abilities/static.rb +365 -0
- data/lib/enjine/mechanics/abilities/tapping_power.rb +71 -0
- data/lib/enjine/mechanics/abilities/turn_rules.rb +49 -0
- data/lib/enjine/mechanics/card_spec.rb +177 -0
- data/lib/enjine/mechanics/copy_except.rb +73 -0
- data/lib/enjine/mechanics/costs/base.rb +115 -0
- data/lib/enjine/mechanics/costs/behold.rb +76 -0
- data/lib/enjine/mechanics/costs/discard.rb +65 -0
- data/lib/enjine/mechanics/costs/exert.rb +29 -0
- data/lib/enjine/mechanics/costs/exile_cards.rb +39 -0
- data/lib/enjine/mechanics/costs/from_graveyard.rb +45 -0
- data/lib/enjine/mechanics/costs/get_counters.rb +39 -0
- data/lib/enjine/mechanics/costs/mana.rb +37 -0
- data/lib/enjine/mechanics/costs/one_of.rb +49 -0
- data/lib/enjine/mechanics/costs/pay_life.rb +50 -0
- data/lib/enjine/mechanics/costs/remove_counters.rb +51 -0
- data/lib/enjine/mechanics/costs/sacrifice.rb +59 -0
- data/lib/enjine/mechanics/costs/tap.rb +56 -0
- data/lib/enjine/mechanics/costs/waterbend.rb +29 -0
- data/lib/enjine/mechanics/counters/kinds.rb +190 -0
- data/lib/enjine/mechanics/effects/add_subtypes.rb +30 -0
- data/lib/enjine/mechanics/effects/airbend.rb +41 -0
- data/lib/enjine/mechanics/effects/amass.rb +124 -0
- data/lib/enjine/mechanics/effects/assimilate.rb +72 -0
- data/lib/enjine/mechanics/effects/base.rb +148 -0
- data/lib/enjine/mechanics/effects/become.rb +182 -0
- data/lib/enjine/mechanics/effects/behold.rb +48 -0
- data/lib/enjine/mechanics/effects/blight.rb +94 -0
- data/lib/enjine/mechanics/effects/choose_cards.rb +168 -0
- data/lib/enjine/mechanics/effects/copy.rb +370 -0
- data/lib/enjine/mechanics/effects/counter_spell.rb +21 -0
- data/lib/enjine/mechanics/effects/counters.rb +161 -0
- data/lib/enjine/mechanics/effects/damage.rb +72 -0
- data/lib/enjine/mechanics/effects/delayed_triggers.rb +112 -0
- data/lib/enjine/mechanics/effects/destroy.rb +22 -0
- data/lib/enjine/mechanics/effects/discard.rb +86 -0
- data/lib/enjine/mechanics/effects/draw.rb +16 -0
- data/lib/enjine/mechanics/effects/earthbend.rb +70 -0
- data/lib/enjine/mechanics/effects/emblem.rb +52 -0
- data/lib/enjine/mechanics/effects/endure.rb +63 -0
- data/lib/enjine/mechanics/effects/exile.rb +49 -0
- data/lib/enjine/mechanics/effects/extra_turn.rb +122 -0
- data/lib/enjine/mechanics/effects/faces.rb +115 -0
- data/lib/enjine/mechanics/effects/fight.rb +76 -0
- data/lib/enjine/mechanics/effects/flip_coins.rb +97 -0
- data/lib/enjine/mechanics/effects/gain_control.rb +94 -0
- data/lib/enjine/mechanics/effects/game_end.rb +42 -0
- data/lib/enjine/mechanics/effects/library.rb +327 -0
- data/lib/enjine/mechanics/effects/life.rb +83 -0
- data/lib/enjine/mechanics/effects/look_at_hand.rb +17 -0
- data/lib/enjine/mechanics/effects/loot.rb +118 -0
- data/lib/enjine/mechanics/effects/mana.rb +96 -0
- data/lib/enjine/mechanics/effects/may.rb +82 -0
- data/lib/enjine/mechanics/effects/meld.rb +77 -0
- data/lib/enjine/mechanics/effects/mill.rb +22 -0
- data/lib/enjine/mechanics/effects/pay.rb +231 -0
- data/lib/enjine/mechanics/effects/phasing.rb +51 -0
- data/lib/enjine/mechanics/effects/proliferate.rb +72 -0
- data/lib/enjine/mechanics/effects/pump.rb +60 -0
- data/lib/enjine/mechanics/effects/put_in_library.rb +98 -0
- data/lib/enjine/mechanics/effects/return_choice.rb +29 -0
- data/lib/enjine/mechanics/effects/return_to_battlefield.rb +80 -0
- data/lib/enjine/mechanics/effects/return_to_hand.rb +20 -0
- data/lib/enjine/mechanics/effects/roll_dice.rb +45 -0
- data/lib/enjine/mechanics/effects/sacrifice.rb +118 -0
- data/lib/enjine/mechanics/effects/scry_and_surveil.rb +74 -0
- data/lib/enjine/mechanics/effects/search.rb +156 -0
- data/lib/enjine/mechanics/effects/tap_untap.rb +23 -0
- data/lib/enjine/mechanics/effects/tokens.rb +76 -0
- data/lib/enjine/mechanics/filters/any_target.rb +15 -0
- data/lib/enjine/mechanics/filters/base.rb +386 -0
- data/lib/enjine/mechanics/filters/graveyard.rb +23 -0
- data/lib/enjine/mechanics/filters/one_of.rb +55 -0
- data/lib/enjine/mechanics/filters/permanents.rb +52 -0
- data/lib/enjine/mechanics/filters/players.rb +20 -0
- data/lib/enjine/mechanics/filters/stack.rb +97 -0
- data/lib/enjine/mechanics/follow_up.rb +46 -0
- data/lib/enjine/mechanics/keywords/alternative_costs.rb +361 -0
- data/lib/enjine/mechanics/keywords/beam_me_up.rb +78 -0
- data/lib/enjine/mechanics/keywords/changeling.rb +58 -0
- data/lib/enjine/mechanics/keywords/convoke.rb +77 -0
- data/lib/enjine/mechanics/keywords/crew.rb +155 -0
- data/lib/enjine/mechanics/keywords/cycling.rb +58 -0
- data/lib/enjine/mechanics/keywords/decayed.rb +25 -0
- data/lib/enjine/mechanics/keywords/devour.rb +61 -0
- data/lib/enjine/mechanics/keywords/evergreen.rb +54 -0
- data/lib/enjine/mechanics/keywords/extort.rb +26 -0
- data/lib/enjine/mechanics/keywords/flags.rb +46 -0
- data/lib/enjine/mechanics/keywords/foretell.rb +45 -0
- data/lib/enjine/mechanics/keywords/gift.rb +50 -0
- data/lib/enjine/mechanics/keywords/hideaway.rb +28 -0
- data/lib/enjine/mechanics/keywords/increment.rb +31 -0
- data/lib/enjine/mechanics/keywords/job_select.rb +29 -0
- data/lib/enjine/mechanics/keywords/kicker.rb +92 -0
- data/lib/enjine/mechanics/keywords/mobilize_and_firebending.rb +84 -0
- data/lib/enjine/mechanics/keywords/paradigm.rb +91 -0
- data/lib/enjine/mechanics/keywords/power_up.rb +48 -0
- data/lib/enjine/mechanics/keywords/protection.rb +111 -0
- data/lib/enjine/mechanics/keywords/prowess.rb +30 -0
- data/lib/enjine/mechanics/keywords/riot.rb +35 -0
- data/lib/enjine/mechanics/keywords/saddle.rb +115 -0
- data/lib/enjine/mechanics/keywords/speed.rb +89 -0
- data/lib/enjine/mechanics/keywords/station.rb +163 -0
- data/lib/enjine/mechanics/keywords/storied.rb +41 -0
- data/lib/enjine/mechanics/keywords/storm_and_cascade.rb +86 -0
- data/lib/enjine/mechanics/keywords/sunburst.rb +29 -0
- data/lib/enjine/mechanics/keywords/suspend.rb +191 -0
- data/lib/enjine/mechanics/keywords/teamwork.rb +45 -0
- data/lib/enjine/mechanics/keywords/ward.rb +67 -0
- data/lib/enjine/mechanics/linked.rb +67 -0
- data/lib/enjine/mechanics/mana_spent.rb +99 -0
- data/lib/enjine/mechanics/selector.rb +207 -0
- data/lib/enjine/mechanics/subtypes/auras_and_equipment.rb +176 -0
- data/lib/enjine/mechanics/subtypes/basic_land_types.rb +36 -0
- data/lib/enjine/mechanics/subtypes/class_levels.rb +102 -0
- data/lib/enjine/mechanics/subtypes/saga.rb +89 -0
- data/lib/enjine/mechanics/supertypes/basic.rb +17 -0
- data/lib/enjine/mechanics/supertypes/legendary.rb +18 -0
- data/lib/enjine/mechanics/triggers/ability_triggers.rb +51 -0
- data/lib/enjine/mechanics/triggers/any_of.rb +43 -0
- data/lib/enjine/mechanics/triggers/base.rb +228 -0
- data/lib/enjine/mechanics/triggers/casting.rb +219 -0
- data/lib/enjine/mechanics/triggers/combat.rb +123 -0
- data/lib/enjine/mechanics/triggers/damage.rb +130 -0
- data/lib/enjine/mechanics/triggers/dilemma.rb +37 -0
- data/lib/enjine/mechanics/triggers/draw_and_discard.rb +85 -0
- data/lib/enjine/mechanics/triggers/keyword_actions.rb +55 -0
- data/lib/enjine/mechanics/triggers/life.rb +35 -0
- data/lib/enjine/mechanics/triggers/steps.rb +39 -0
- data/lib/enjine/mechanics/triggers/tap_and_counters.rb +111 -0
- data/lib/enjine/mechanics/triggers/zone_changes.rb +249 -0
- data/lib/enjine/mechanics/types/kindred.rb +30 -0
- data/lib/enjine/mechanics/types/land.rb +24 -0
- data/lib/enjine/mechanics/types/permanent_types.rb +67 -0
- data/lib/enjine/mechanics/types/spell_types.rb +38 -0
- data/lib/enjine/mechanics/values.rb +675 -0
- data/lib/enjine/mechanics.rb +74 -0
- data/lib/enjine/player.rb +30 -0
- data/lib/enjine/primitive.rb +61 -0
- data/lib/enjine/replacements.rb +191 -0
- data/lib/enjine/rng.rb +57 -0
- data/lib/enjine/rules_monitor.rb +404 -0
- data/lib/enjine/setup.rb +45 -0
- data/lib/enjine/snapshot/player_checks.rb +86 -0
- data/lib/enjine/snapshot/turn_checks.rb +91 -0
- data/lib/enjine/snapshot.rb +307 -0
- data/lib/enjine/state.rb +173 -0
- data/lib/enjine/steps.rb +117 -0
- data/lib/enjine/tokens.rb +25 -0
- data/lib/enjine/verify.rb +233 -0
- data/lib/enjine/version.rb +7 -0
- data/lib/enjine/view.rb +162 -0
- data/lib/enjine.rb +46 -0
- metadata +315 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Triggers
|
|
6
|
+
# "When ~ enters" / "Whenever a creature you control enters" (landfall: lands you control).
|
|
7
|
+
# `untapped: true` is "when this land enters untapped": the status it entered with (CR 614.12).
|
|
8
|
+
# `from`: only a permanent that entered from that zone ("enters from exile", CR 603.6a).
|
|
9
|
+
class Enters < Trigger
|
|
10
|
+
FROM = %w[hand graveyard exile library stack outside].freeze
|
|
11
|
+
|
|
12
|
+
primitive :trigger, :enters,
|
|
13
|
+
description: "The subject enters the battlefield. `untapped: true`: only if it entered untapped. " \
|
|
14
|
+
"`from`: only if it entered from that zone (\"enters from exile\").",
|
|
15
|
+
rules: %w[603.6a 603.2 614.12],
|
|
16
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, \"controller\": you|opponent|any, \"other\": true}",
|
|
17
|
+
"untapped" => "true | false", "from" => FROM.join(" | ") },
|
|
18
|
+
example: { "enters" => { "subject" => { "each" => "land", "controller" => "you" } } }
|
|
19
|
+
|
|
20
|
+
def self.keys = %w[subject untapped from]
|
|
21
|
+
def event_class = Events::ZoneChanged
|
|
22
|
+
|
|
23
|
+
def setup(params, errors, path)
|
|
24
|
+
@untapped = params.fetch("untapped", false)
|
|
25
|
+
errors << "#{path}.untapped: must be true or false" unless [true, false].include?(@untapped)
|
|
26
|
+
@from = params["from"]
|
|
27
|
+
errors << "#{path}.from: must be one of #{FROM.join(", ")}" unless @from.nil? || FROM.include?(@from)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def match?(engine, event, source)
|
|
31
|
+
return false unless event.to == "battlefield" && (@from.nil? || event.from == @from)
|
|
32
|
+
|
|
33
|
+
obj = engine.state.objects[event.new_id] or return false
|
|
34
|
+
return false if @untapped && obj.tapped
|
|
35
|
+
|
|
36
|
+
@subject.match?(engine, event.new_id, obj.controller, event.card, source, types: engine.chars(obj).types)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s = "When #{@subject} enters#{" untapped" if @untapped}#{" from #{@from}" if @from}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# "Whenever you play a land" (CR 305.1): a player plays a land, from their hand or, when an effect allows it,
|
|
43
|
+
# from another zone (`from` / `not_from`: "from exile", "from anywhere other than your hand"). Putting a land
|
|
44
|
+
# onto the battlefield with an effect isn't playing it.
|
|
45
|
+
class PlaysLand < PlayerTrigger
|
|
46
|
+
primitive :trigger, :plays_land,
|
|
47
|
+
description: "A player (`controller`: you (default) | opponent | any) plays a land. `from` / " \
|
|
48
|
+
"`not_from`: only a land played from (not from) that zone: hand | graveyard | exile | " \
|
|
49
|
+
"library | outside. Lands put onto the battlefield by effects aren't played.",
|
|
50
|
+
rules: %w[305.1 603.2 601.2a],
|
|
51
|
+
params: { "controller" => PLAYERS.join(" | "), "from" => "zone", "not_from" => "zone" },
|
|
52
|
+
example: { "plays_land" => { "not_from" => "hand" } }
|
|
53
|
+
|
|
54
|
+
def self.keys = %w[controller from not_from]
|
|
55
|
+
def event_class = Events::ZoneChanged
|
|
56
|
+
|
|
57
|
+
def setup(params, errors, path)
|
|
58
|
+
super
|
|
59
|
+
@zones = CastFrom.build(params, errors, path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def match?(engine, event, source)
|
|
63
|
+
return false unless event.reason == "play_land" && event.to == "battlefield" && @zones.match?(event.from)
|
|
64
|
+
|
|
65
|
+
player = engine.state.objects[event.new_id]&.controller || engine.names.index(event.owner)
|
|
66
|
+
player_ok?(engine, engine.name(player), source)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_s = "Whenever #{who} play#{"s" unless @controller == "you"} a land#{@zones}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# "When ~ dies" / "Whenever another creature you control dies".
|
|
73
|
+
class Dies < Trigger
|
|
74
|
+
primitive :trigger, :dies,
|
|
75
|
+
description: "The subject is put into a graveyard from the battlefield. Looks back in time (CR 603.10a).",
|
|
76
|
+
rules: %w[603.6c 603.10a 700.4],
|
|
77
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
|
|
78
|
+
example: { "dies" => { "subject" => "self" } }
|
|
79
|
+
|
|
80
|
+
def event_class = Events::ZoneChanged
|
|
81
|
+
def leaves_battlefield? = true
|
|
82
|
+
|
|
83
|
+
def match?(engine, event, source)
|
|
84
|
+
return false unless event.from == "battlefield" && event.to == "graveyard"
|
|
85
|
+
|
|
86
|
+
lki = engine.last_known(event.old_id) or return false
|
|
87
|
+
@subject.match?(engine, event.old_id, lki["controller"], event.card, source, types: lki["types"])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_s = "When #{@subject} dies"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# "When ~ leaves the battlefield" / "Whenever another creature you control leaves the battlefield"
|
|
94
|
+
# (CR 603.6c): a permanent moves from the battlefield to any other zone (`to`: only that zone). It
|
|
95
|
+
# looks back in time (CR 603.10a): the permanent is matched as it last existed, and its own ability
|
|
96
|
+
# triggers although it has left.
|
|
97
|
+
class LeavesBattlefield < Trigger
|
|
98
|
+
ZONES = %w[graveyard exile hand library].freeze
|
|
99
|
+
|
|
100
|
+
primitive :trigger, :leaves_battlefield,
|
|
101
|
+
description: "The subject leaves the battlefield (for any zone, or only `to` that zone). Looks back " \
|
|
102
|
+
"in time: the permanent is matched as it last existed (types, controller, counters).",
|
|
103
|
+
rules: %w[603.6c 603.10a 400.7],
|
|
104
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, \"controller\": ..., \"other\": true}",
|
|
105
|
+
"to" => ZONES.join(" | ") },
|
|
106
|
+
example: { "leaves_battlefield" => { "subject" => { "each" => "creature", "controller" => "you",
|
|
107
|
+
"other" => true } } }
|
|
108
|
+
|
|
109
|
+
def self.keys = %w[subject to]
|
|
110
|
+
def event_class = Events::ZoneChanged
|
|
111
|
+
def leaves_battlefield? = true
|
|
112
|
+
|
|
113
|
+
def setup(params, errors, path)
|
|
114
|
+
@to = params["to"]
|
|
115
|
+
errors << "#{path}.to: must be one of #{ZONES.join(", ")}" unless @to.nil? || ZONES.include?(@to)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def match?(engine, event, source)
|
|
119
|
+
return false unless event.from == "battlefield" && (@to.nil? || event.to == @to)
|
|
120
|
+
|
|
121
|
+
lki = engine.last_known(event.old_id) or return false
|
|
122
|
+
@subject.match?(engine, event.old_id, lki["controller"], event.card, source, types: lki["types"])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def to_s
|
|
126
|
+
return "When #{@subject} leaves the battlefield" unless @to
|
|
127
|
+
|
|
128
|
+
"When #{@subject} is put into #{"a " unless @to == "exile"}#{@to} from the battlefield"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# "Whenever you sacrifice another permanent" / "a Food" (CR 701.21a): only its controller can sacrifice
|
|
133
|
+
# a permanent, so the subject's `controller` is who sacrificed it. Looks back in time (CR 603.10a).
|
|
134
|
+
class Sacrifices < Trigger
|
|
135
|
+
include Batched
|
|
136
|
+
|
|
137
|
+
primitive :trigger, :sacrifices,
|
|
138
|
+
description: "The subject is sacrificed (`controller` of the subject: who sacrificed it). Looks back " \
|
|
139
|
+
"in time, like leaves_battlefield. `batched: true`: \"one or more\", once per event batch.",
|
|
140
|
+
rules: %w[701.21a 603.10a 603.2c],
|
|
141
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, \"controller\": you, \"other\": true}",
|
|
142
|
+
"batched" => "true | false" },
|
|
143
|
+
example: { "sacrifices" => { "subject" => { "each" => "artifact", "controller" => "you" } } }
|
|
144
|
+
|
|
145
|
+
def self.keys = %w[subject batched]
|
|
146
|
+
def event_class = Events::ZoneChanged
|
|
147
|
+
def leaves_battlefield? = true
|
|
148
|
+
def setup(params, errors, path) = setup_batched(params, errors, path)
|
|
149
|
+
|
|
150
|
+
def match?(engine, event, source)
|
|
151
|
+
return false unless event.from == "battlefield" && event.reason == "sacrifice"
|
|
152
|
+
|
|
153
|
+
lki = engine.last_known(event.old_id) or return false
|
|
154
|
+
@subject.match?(engine, event.old_id, lki["controller"], event.card, source, types: lki["types"])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def to_s = "Whenever #{@subject} is sacrificed"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# "Whenever one or more creature cards are put into your graveyard from anywhere" / "cards are put into
|
|
161
|
+
# exile from graveyards and/or the battlefield". `controller`: whose graveyard or exile, i.e. the card's
|
|
162
|
+
# owner. CR 603.6c: a "from anywhere" trigger is never a leaves-the-battlefield ability.
|
|
163
|
+
class PutInto < PlayerTrigger
|
|
164
|
+
include Batched
|
|
165
|
+
include CardMatch
|
|
166
|
+
|
|
167
|
+
ZONES = %w[graveyard exile].freeze
|
|
168
|
+
FROM = %w[battlefield graveyard hand library stack exile].freeze
|
|
169
|
+
|
|
170
|
+
primitive :trigger, :put_into,
|
|
171
|
+
description: "A card is put into `zone` (graveyard | exile) from anywhere, or only `from` the listed " \
|
|
172
|
+
"zones. `controller`: the card's owner (you (default) | opponent | any); optional `card` " \
|
|
173
|
+
"filter on the card. `batched: true`: \"one or more cards\", once per event batch. With " \
|
|
174
|
+
"\"battlefield\" among `from` it looks back in time like leaves_battlefield.",
|
|
175
|
+
rules: %w[603.6c 603.2c 400.3],
|
|
176
|
+
params: { "zone" => ZONES.join(" | "), "from" => "list of #{FROM.join(", ")} (default: anywhere)",
|
|
177
|
+
"controller" => PLAYERS.join(" | "), "card" => "card filter", "batched" => "true | false" },
|
|
178
|
+
example: { "put_into" => { "zone" => "graveyard", "card" => "creature", "batched" => true } }
|
|
179
|
+
|
|
180
|
+
def self.keys = %w[zone from controller card batched]
|
|
181
|
+
def event_class = Events::ZoneChanged
|
|
182
|
+
# CR 603.6c: "put into a graveyard from the battlefield" is a leaves-the-battlefield ability; "from anywhere"
|
|
183
|
+
# never is.
|
|
184
|
+
def leaves_battlefield? = @from&.include?("battlefield") || false
|
|
185
|
+
|
|
186
|
+
def setup(params, errors, path)
|
|
187
|
+
super
|
|
188
|
+
setup_batched(params, errors, path)
|
|
189
|
+
setup_card(params, errors, path)
|
|
190
|
+
@zone = params["zone"]
|
|
191
|
+
errors << "#{path}.zone: must be one of #{ZONES.join(", ")}" unless ZONES.include?(@zone)
|
|
192
|
+
@from = params["from"]
|
|
193
|
+
return if @from.nil? || (@from.is_a?(Array) && !@from.empty? && (@from - FROM).empty?)
|
|
194
|
+
|
|
195
|
+
errors << "#{path}.from: must be a list of #{FROM.join(", ")}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def match?(engine, event, source)
|
|
199
|
+
event.to == @zone && (@from.nil? || @from.include?(event.from)) && player_ok?(engine, event.owner, source) &&
|
|
200
|
+
card_ok?(engine, event.card)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def to_s
|
|
204
|
+
into = @zone == "exile" ? "exile" : "#{{ "you" => "your", "opponent" => "an opponent's" }.fetch(@controller, "a")} graveyard"
|
|
205
|
+
from = @from ? " from #{@from.join(" or ")}" : " from anywhere"
|
|
206
|
+
"Whenever #{@batched ? "one or more #{card_words}s are" : "a #{card_words} is"} put into #{into}#{from}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# "Whenever a card leaves your graveyard" / "whenever one or more cards leave your graveyard" (`batched`).
|
|
211
|
+
# Looks back in time (CR 603.10a): the card is matched as it was in the graveyard.
|
|
212
|
+
class LeavesGraveyard < PlayerTrigger
|
|
213
|
+
primitive :trigger, :leaves_graveyard,
|
|
214
|
+
description: "A card leaves a graveyard (whose: `controller` you (default) | opponent | any). Optional " \
|
|
215
|
+
"`card` filter on its card types. `batched: true` is \"one or more cards leave\": once per " \
|
|
216
|
+
"event that moves cards (one effect, cost or action), however many cards.",
|
|
217
|
+
rules: %w[603.10a 603.2c 404.1],
|
|
218
|
+
params: { "controller" => PLAYERS.join(" | "), "card" => "card type filter, e.g. \"creature\"",
|
|
219
|
+
"batched" => "true | false" },
|
|
220
|
+
example: { "leaves_graveyard" => { "controller" => "you", "batched" => true } }
|
|
221
|
+
|
|
222
|
+
def self.keys = %w[controller card batched]
|
|
223
|
+
def event_class = Events::ZoneChanged
|
|
224
|
+
def batched? = @batched
|
|
225
|
+
|
|
226
|
+
def setup(params, errors, path)
|
|
227
|
+
super
|
|
228
|
+
@batched = params.fetch("batched", false)
|
|
229
|
+
errors << "#{path}.batched: must be true or false" unless [true, false].include?(@batched)
|
|
230
|
+
@card = params["card"] && Mechanics.build_entry(:filter, params["card"], errors, "#{path}.card")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def match?(engine, event, source)
|
|
234
|
+
return false unless event.from == "graveyard" && player_ok?(engine, event.owner, source)
|
|
235
|
+
return true unless @card
|
|
236
|
+
|
|
237
|
+
defn = engine.catalog.fetch(event.card)
|
|
238
|
+
@card.matches_facts?(Filters::Facts.new(chars: defn.printed_characteristics, counters: {}, token: defn.token?))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def to_s
|
|
242
|
+
what = @card ? Triggers.filter_words(@card) : "card"
|
|
243
|
+
whose = { "you" => "your", "opponent" => "an opponent's" }.fetch(@controller, "a")
|
|
244
|
+
"Whenever #{@batched ? "one or more #{what}s leave" : "a #{what} leaves"} #{whose} graveyard"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Types
|
|
6
|
+
# Kindred (CR 308). A kindred card always has another card type, and is cast and resolves by that
|
|
7
|
+
# type's rules (CR 308.1): a "Kindred Instant" is an instant. Its subtypes are creature types
|
|
8
|
+
# (CR 308.2, 205.3m), so "Goblin spells" and "the number of creature types" see them.
|
|
9
|
+
class Kindred < Primitive
|
|
10
|
+
primitive :type, :kindred,
|
|
11
|
+
description: "A card type that goes with another one (\"Kindred Sorcery — Goblin\"): the card is " \
|
|
12
|
+
"cast and resolves as its other type, and its subtypes are creature types.",
|
|
13
|
+
rules: %w[308.1 308.2 205.3m],
|
|
14
|
+
example: { "name" => "Example Goblin Gambit", "mana_cost" => "{R}", "types" => %w[kindred sorcery],
|
|
15
|
+
"subtypes" => ["goblin"],
|
|
16
|
+
"abilities" => [{ "spell" => { "targets" => ["any_target"],
|
|
17
|
+
"effects" => [{ "deal_damage" => { "amount" => 1,
|
|
18
|
+
"to" => { "target" => 0 } } }] } }] }
|
|
19
|
+
|
|
20
|
+
def self.permanent? = false
|
|
21
|
+
def self.instant_speed? = false
|
|
22
|
+
|
|
23
|
+
# CR 308.1: each kindred card has another card type.
|
|
24
|
+
def self.validate(card, errors)
|
|
25
|
+
errors << "types: a kindred card needs another card type (CR 308.1)" if (Array(card["types"]).map(&:to_s) - ["kindred"]).empty?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Types
|
|
6
|
+
# Land (CR 305). Played as a special action, never cast; one per turn.
|
|
7
|
+
class Land < Primitive
|
|
8
|
+
primitive :type, :land,
|
|
9
|
+
description: "A permanent played with the special action \"play a land\" (one per turn, " \
|
|
10
|
+
"main phase, empty stack). It has no mana cost.",
|
|
11
|
+
rules: %w[305.1 305.2 305.3 202.1a],
|
|
12
|
+
example: { "name" => "Forest", "types" => ["land"], "supertypes" => ["basic"],
|
|
13
|
+
"subtypes" => ["forest"] }
|
|
14
|
+
|
|
15
|
+
def self.permanent? = true
|
|
16
|
+
def self.instant_speed? = false
|
|
17
|
+
|
|
18
|
+
def self.validate(card, errors)
|
|
19
|
+
errors << "mana_cost: lands have no mana cost (CR 202.1a)" if card.key?("mana_cost")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Types
|
|
6
|
+
# Shared rules of permanent card types that are cast (artifact, creature,
|
|
7
|
+
# enchantment, planeswalker): sorcery timing unless something grants flash (CR 307.1 analogues).
|
|
8
|
+
class PermanentType < Primitive
|
|
9
|
+
def self.permanent? = true
|
|
10
|
+
def self.instant_speed? = false
|
|
11
|
+
def self.validate(_card, _errors) = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Creature (CR 302). Has power and toughness; can attack and block; summoning sickness (302.6).
|
|
15
|
+
class Creature < PermanentType
|
|
16
|
+
primitive :type, :creature,
|
|
17
|
+
description: "A permanent with power and toughness that can attack and block. Cast at sorcery " \
|
|
18
|
+
"speed. It can't attack or use {T} abilities unless controlled continuously since " \
|
|
19
|
+
"its controller's most recent turn began (summoning sickness).",
|
|
20
|
+
rules: %w[302.1 302.4 302.6 307.1],
|
|
21
|
+
example: { "name" => "Grizzly Bears", "mana_cost" => "{1}{G}", "types" => ["creature"],
|
|
22
|
+
"subtypes" => ["bear"], "power" => 2, "toughness" => 2 }
|
|
23
|
+
|
|
24
|
+
def self.validate(card, errors)
|
|
25
|
+
%w[power toughness].each { |k| errors << "#{k}: creatures need an integer #{k} (CR 302.4)" unless card.key?(k) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Artifact (CR 301).
|
|
30
|
+
class Artifact < PermanentType
|
|
31
|
+
primitive :type, :artifact, description: "A permanent cast at sorcery speed.", rules: %w[301.1],
|
|
32
|
+
example: { "name" => "Example Relic", "mana_cost" => "{2}", "types" => ["artifact"] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Enchantment (CR 303).
|
|
36
|
+
class Enchantment < PermanentType
|
|
37
|
+
primitive :type, :enchantment, description: "A permanent cast at sorcery speed.", rules: %w[303.1],
|
|
38
|
+
example: { "name" => "Example Aura", "mana_cost" => "{1}{W}",
|
|
39
|
+
"types" => ["enchantment"] }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Planeswalker (CR 306). Enters with loyalty counters; one loyalty ability per turn.
|
|
43
|
+
class Planeswalker < PermanentType
|
|
44
|
+
primitive :type, :planeswalker,
|
|
45
|
+
description: "A permanent that enters with loyalty counters equal to its printed loyalty. " \
|
|
46
|
+
"Its loyalty abilities can be activated at sorcery speed, one per turn. Damage dealt " \
|
|
47
|
+
"to it removes loyalty counters; at 0 loyalty it is put into its owner's graveyard.",
|
|
48
|
+
rules: %w[306.5b 306.8 120.3c 606.3 704.5i],
|
|
49
|
+
example: { "name" => "Vessa, Rally Captain", "mana_cost" => "{2}{W}{W}", "types" => ["planeswalker"],
|
|
50
|
+
"loyalty" => 3,
|
|
51
|
+
"abilities" => [{ "loyalty" => { "cost" => 1, "effects" => [
|
|
52
|
+
{ "pump" => { "power" => 1, "toughness" => 1,
|
|
53
|
+
"to" => { "each" => "creature", "controller" => "you" } } }
|
|
54
|
+
] } }] }
|
|
55
|
+
|
|
56
|
+
def self.validate(card, errors)
|
|
57
|
+
errors << "loyalty: planeswalkers need an integer loyalty (CR 306.5b)" unless card["loyalty"].is_a?(Integer)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# CR 306.5b: a planeswalker enters with loyalty counters equal to its printed loyalty.
|
|
61
|
+
def self.entering(definition, enters)
|
|
62
|
+
enters["counters"]["loyalty"] = definition.loyalty if definition.loyalty
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Types
|
|
6
|
+
# Instants and sorceries: never permanents; they go to the graveyard on resolution (CR 608.2n).
|
|
7
|
+
class SpellType < Primitive
|
|
8
|
+
def self.permanent? = false
|
|
9
|
+
def self.validate(_card, _errors) = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Instant (CR 304). Cast any time you have priority.
|
|
13
|
+
class Instant < SpellType
|
|
14
|
+
primitive :type, :instant,
|
|
15
|
+
description: "A spell cast any time its controller has priority. Its spell ability resolves, " \
|
|
16
|
+
"then it goes to the graveyard.",
|
|
17
|
+
rules: %w[304.1 304.2 608.2n],
|
|
18
|
+
example: { "name" => "Lightning Bolt", "mana_cost" => "{R}", "types" => ["instant"],
|
|
19
|
+
"abilities" => [{ "spell" => { "targets" => ["any_target"],
|
|
20
|
+
"effects" => [{ "deal_damage" => { "amount" => 3,
|
|
21
|
+
"to" => { "target" => 0 } } }] } }] }
|
|
22
|
+
|
|
23
|
+
def self.instant_speed? = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Sorcery (CR 307). Main phase, empty stack, own turn.
|
|
27
|
+
class Sorcery < SpellType
|
|
28
|
+
primitive :type, :sorcery,
|
|
29
|
+
description: "A spell cast only in its controller's main phase while the stack is empty.",
|
|
30
|
+
rules: %w[307.1 307.2 608.2n],
|
|
31
|
+
example: { "name" => "Divination", "mana_cost" => "{2}{U}", "types" => ["sorcery"],
|
|
32
|
+
"abilities" => [{ "spell" => { "effects" => [{ "draw" => { "amount" => 2 } }] } }] }
|
|
33
|
+
|
|
34
|
+
def self.instant_speed? = false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|