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,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "entering_choices"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
# "Choose a color", "choose a creature type", "choose a card type" and "choose Khans or Dragons"
|
|
8
|
+
# (CR 105.4, 205.3m, 205.2a, 607.2d, 607.2f). The choice is remembered in an object's memory under a
|
|
9
|
+
# `chosen_*` key, where filters ("of the chosen type", Filters::Restricted), conditions ({"chosen": word})
|
|
10
|
+
# and linked abilities read it (CR 607.2d). Made as a permanent enters (an ability, CR 614.12) or while
|
|
11
|
+
# a spell or ability resolves (an effect, remembered on the resolving object).
|
|
12
|
+
module Chosen
|
|
13
|
+
# What is chosen -> [memory key, option key].
|
|
14
|
+
MEMORY = { "color" => "chosen_color", "creature_type" => "chosen_type", "card_type" => "chosen_card_type",
|
|
15
|
+
"basic_land_type" => "chosen_type", "option" => "chosen_option" }.freeze
|
|
16
|
+
# CR 105.4: one of the five colors.
|
|
17
|
+
COLORS = %w[W U B R G].freeze
|
|
18
|
+
# CR 205.2a: the card types of traditional Magic cards.
|
|
19
|
+
CARD_TYPES = %w[artifact battle creature enchantment instant kindred land planeswalker sorcery].freeze
|
|
20
|
+
# CR 305.6: the basic land types.
|
|
21
|
+
BASIC_LAND_TYPES = %w[plains island swamp mountain forest].freeze
|
|
22
|
+
WORD = /\A[a-z][a-z0-9_]*\z/
|
|
23
|
+
|
|
24
|
+
# The values a player may choose: every color, creature type (CR 205.3m) or card type, or the named options.
|
|
25
|
+
def self.values(kind, words)
|
|
26
|
+
case kind
|
|
27
|
+
when "color" then COLORS
|
|
28
|
+
when "creature_type" then Keywords::Changeling::CREATURE_TYPES.map(&:to_s)
|
|
29
|
+
when "card_type" then CARD_TYPES
|
|
30
|
+
when "basic_land_type" then BASIC_LAND_TYPES
|
|
31
|
+
else words
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# `"options": ["khans", "dragons"]` for "option": two or more lowercase words (CR 607.2f).
|
|
36
|
+
def self.words(kind, params, errors, path)
|
|
37
|
+
return [].freeze unless kind == "option"
|
|
38
|
+
|
|
39
|
+
list = Mechanics.fetch_param(params, :options)
|
|
40
|
+
ok = list.is_a?(Array) && list.size >= 2 && list.all? { WORD.match?(it.to_s) } && list.uniq.size == list.size
|
|
41
|
+
errors << "#{path}.options: must list two or more different lowercase words" unless ok
|
|
42
|
+
ok ? list.map(&:to_s).freeze : [].freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.memory(kind, value) = { MEMORY.fetch(kind) => value }
|
|
46
|
+
def self.words_text(kind, words) = kind == "option" ? words.join(" or ") : "a #{kind.tr("_", " ")}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module Abilities
|
|
50
|
+
# "As this enters, choose a color / creature type / card type / one of the words" (CR 614.12, 607.2d).
|
|
51
|
+
class EnteringChosen < EnteringChoice
|
|
52
|
+
def self.kind_name = raise(NotImplementedError)
|
|
53
|
+
|
|
54
|
+
def self.build(params, errors, path, **)
|
|
55
|
+
Mechanics.check_keys(params, kind_name == "option" ? %w[options] : [], errors, path)
|
|
56
|
+
new(Chosen.words(kind_name, params, errors, path))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(words)
|
|
60
|
+
super()
|
|
61
|
+
@words = words
|
|
62
|
+
freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_s = "as this enters, choose #{Chosen.words_text(self.class.kind_name, @words)}"
|
|
66
|
+
|
|
67
|
+
def options(_engine, _controller)
|
|
68
|
+
kind = self.class.kind_name
|
|
69
|
+
Chosen.values(kind, @words).map { { type: kind, kind.to_sym => it } }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def enters_for(answer) = { "memory" => Chosen.memory(self.class.kind_name, answer[self.class.kind_name.to_sym]) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# "As this enters, choose a color."
|
|
76
|
+
class ChooseColor < EnteringChosen
|
|
77
|
+
primitive :ability, :choose_color,
|
|
78
|
+
description: "As this permanent enters, its controller chooses a color; filters with \"colors\": " \
|
|
79
|
+
"\"chosen\" (and other abilities of it) use it.",
|
|
80
|
+
rules: %w[614.12 105.4 607.2d], example: { "choose_color" => {} }
|
|
81
|
+
def self.kind_name = "color"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# "As this enters, choose a creature type."
|
|
85
|
+
class ChooseCreatureType < EnteringChosen
|
|
86
|
+
primitive :ability, :choose_creature_type,
|
|
87
|
+
description: "As this permanent enters, its controller chooses a creature type; filters with " \
|
|
88
|
+
"\"subtypes\": \"chosen\" use it.",
|
|
89
|
+
rules: %w[614.12 205.3m 607.2d], example: { "choose_creature_type" => {} }
|
|
90
|
+
def self.kind_name = "creature_type"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# "As this enters, choose a card type."
|
|
94
|
+
class ChooseCardType < EnteringChosen
|
|
95
|
+
primitive :ability, :choose_card_type,
|
|
96
|
+
description: "As this permanent enters, its controller chooses a card type; filters with \"types\": " \
|
|
97
|
+
"\"chosen\" (or \"not\": \"chosen\") use it.",
|
|
98
|
+
rules: %w[614.12 205.2a 607.2d], example: { "choose_card_type" => {} }
|
|
99
|
+
def self.kind_name = "card_type"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# "As this enters, choose a basic land type." (Multiversal Passage: "This land is the chosen type.")
|
|
103
|
+
class ChooseBasicLandType < EnteringChosen
|
|
104
|
+
primitive :ability, :choose_basic_land_type,
|
|
105
|
+
description: "As this permanent enters, its controller chooses a basic land type; filters with " \
|
|
106
|
+
"\"subtypes\": \"chosen\" and a static ability's `\"subtypes\": \"chosen\"` use it " \
|
|
107
|
+
"(\"this land is the chosen type\").",
|
|
108
|
+
rules: %w[614.12 305.6 607.2d], example: { "choose_basic_land_type" => {} }
|
|
109
|
+
def self.kind_name = "basic_land_type"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# "As this enters, choose Khans or Dragons." Abilities of it check the word with {"chosen": word}.
|
|
113
|
+
class ChooseOption < EnteringChosen
|
|
114
|
+
primitive :ability, :choose_option,
|
|
115
|
+
description: "As this permanent enters, its controller chooses one of the `options` (words with no " \
|
|
116
|
+
"rules meaning); a condition {\"chosen\": word} is true when that word was chosen.",
|
|
117
|
+
rules: %w[614.12 607.2f 614.12b],
|
|
118
|
+
params: { "options" => "list of two or more lowercase words" },
|
|
119
|
+
example: { "choose_option" => { "options" => %w[khans dragons] } }
|
|
120
|
+
def self.kind_name = "option"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
module Effects
|
|
125
|
+
# "Choose a creature type." while resolving: the choice is remembered on the resolving spell or
|
|
126
|
+
# ability, so its later effects' filters ("of the chosen type") and conditions read it (CR 607.2d).
|
|
127
|
+
class ResolvingChosen < ChoiceEffect
|
|
128
|
+
def self.kind_name = raise(NotImplementedError)
|
|
129
|
+
def self.default_to = "you"
|
|
130
|
+
def self.keys = kind_name == "option" ? %w[options to if] : %w[to if]
|
|
131
|
+
|
|
132
|
+
def setup(params, errors, path) = @words = Chosen.words(self.class.kind_name, params, errors, path)
|
|
133
|
+
|
|
134
|
+
def to_s = "choose #{Chosen.words_text(self.class.kind_name, @words)}"
|
|
135
|
+
|
|
136
|
+
def run(engine, ctx, locals)
|
|
137
|
+
return :done unless locals["asked"] || condition.nil? || condition.true?(engine, ctx)
|
|
138
|
+
|
|
139
|
+
kind = self.class.kind_name
|
|
140
|
+
answer = answers(locals).first
|
|
141
|
+
unless answer
|
|
142
|
+
chooser = players(engine, ctx).first || ctx.controller
|
|
143
|
+
return ask(engine, locals, chooser, "Choose #{Chosen.words_text(kind, @words)}",
|
|
144
|
+
Chosen.values(kind, @words).map { { "type" => kind, kind => it } })
|
|
145
|
+
end
|
|
146
|
+
engine.state.objects[ctx.object]&.memory&.merge!(Chosen.memory(kind, answer[kind]))
|
|
147
|
+
:done
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# "Choose a color."
|
|
152
|
+
class ChooseColorEffect < ResolvingChosen
|
|
153
|
+
primitive :effect, :choose_color,
|
|
154
|
+
description: "The player (`to`, default you) chooses a color; later effects of this spell or ability " \
|
|
155
|
+
"read it with \"colors\": \"chosen\".",
|
|
156
|
+
rules: %w[105.4 607.2d 608.2c], params: { "to" => "player selector (default you)" },
|
|
157
|
+
example: { "choose_color" => {} }
|
|
158
|
+
def self.kind_name = "color"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# "Choose a creature type."
|
|
162
|
+
class ChooseCreatureTypeEffect < ResolvingChosen
|
|
163
|
+
primitive :effect, :choose_creature_type,
|
|
164
|
+
description: "The player (`to`, default you) chooses a creature type; later effects of this spell or " \
|
|
165
|
+
"ability read it with \"subtypes\": \"chosen\".",
|
|
166
|
+
rules: %w[205.3m 607.2d 608.2c], params: { "to" => "player selector (default you)" },
|
|
167
|
+
example: { "choose_creature_type" => {} }
|
|
168
|
+
def self.kind_name = "creature_type"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# "Choose a card type."
|
|
172
|
+
class ChooseCardTypeEffect < ResolvingChosen
|
|
173
|
+
primitive :effect, :choose_card_type,
|
|
174
|
+
description: "The player (`to`, default you) chooses a card type; later effects of this spell or " \
|
|
175
|
+
"ability read it with \"types\": \"chosen\".",
|
|
176
|
+
rules: %w[205.2a 607.2d 608.2c], params: { "to" => "player selector (default you)" },
|
|
177
|
+
example: { "choose_card_type" => {} }
|
|
178
|
+
def self.kind_name = "card_type"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# "Choose odd or even." Later effects check the word with {"if": {"chosen": word}}.
|
|
182
|
+
class ChooseOptionEffect < ResolvingChosen
|
|
183
|
+
primitive :effect, :choose_option,
|
|
184
|
+
description: "The player (`to`, default you) chooses one of the `options` (words); later effects of this " \
|
|
185
|
+
"spell or ability check it with the condition {\"chosen\": word}.",
|
|
186
|
+
rules: %w[607.2f 608.2c],
|
|
187
|
+
params: { "options" => "list of two or more lowercase words", "to" => "player selector (default you)" },
|
|
188
|
+
example: { "choose_option" => { "options" => %w[odd even] } }
|
|
189
|
+
def self.kind_name = "option"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
class Condition
|
|
194
|
+
# {"chosen": "khans"}: that word was chosen for this object (CR 607.2f), by its "as this enters"
|
|
195
|
+
# choice or earlier while this spell or ability resolved.
|
|
196
|
+
class ChosenIs
|
|
197
|
+
def self.build(data, errors, path)
|
|
198
|
+
Mechanics.check_keys(data, %w[chosen], errors, path)
|
|
199
|
+
errors << "#{path}.chosen: must be a lowercase word" unless Chosen::WORD.match?(data["chosen"].to_s)
|
|
200
|
+
new(data["chosen"].to_s)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def initialize(word)
|
|
204
|
+
@word = word
|
|
205
|
+
freeze
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def true?(engine, ctx) = Filters.chosen_memory(engine, ctx.source, ctx)["chosen_option"] == @word
|
|
209
|
+
def to_s = "#{@word} was chosen"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Abilities
|
|
6
|
+
# "costs {N} less" (CR 601.2f, 118.7a): an amount of generic mana, fixed or computed (Amount),
|
|
7
|
+
# optionally only while a condition holds. Shared by the spell and activated-ability forms.
|
|
8
|
+
class Reduction
|
|
9
|
+
attr_reader :amount, :condition
|
|
10
|
+
|
|
11
|
+
def self.build(data, errors, path)
|
|
12
|
+
unless data.is_a?(Hash)
|
|
13
|
+
errors << "#{path}: must be {\"amount\": n, \"if\": condition}"
|
|
14
|
+
return new(Amount::Fixed.new(0), nil)
|
|
15
|
+
end
|
|
16
|
+
data = data.transform_keys(&:to_s)
|
|
17
|
+
Mechanics.check_keys(data, %w[amount if], errors, path)
|
|
18
|
+
amount = if data.key?("amount")
|
|
19
|
+
Amount.build(data["amount"], errors, "#{path}.amount", targets_count: 0)
|
|
20
|
+
else
|
|
21
|
+
(errors << "#{path}.amount: required") && Amount::Fixed.new(0)
|
|
22
|
+
end
|
|
23
|
+
condition = data["if"] && Condition.build(data["if"], errors, "#{path}.if")
|
|
24
|
+
new(amount, condition)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(amount, condition)
|
|
28
|
+
@amount = amount
|
|
29
|
+
@condition = condition
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The generic mana it takes off now (0 when the condition doesn't hold).
|
|
34
|
+
def reduction(engine, ctx) = condition.nil? || condition.true?(engine, ctx) ? amount.value(engine, ctx) : 0
|
|
35
|
+
|
|
36
|
+
def to_s = "costs {#{amount}} less#{" if #{condition}" if condition}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# "This spell costs {N} less to cast [if ... / for each ...]" (CR 601.2f). It functions while
|
|
40
|
+
# the card is being cast; the condition sees the chosen targets ("if it targets ...").
|
|
41
|
+
class CostReduction < Primitive
|
|
42
|
+
primitive :ability, :cost_reduction,
|
|
43
|
+
description: "This spell costs `amount` generic mana less to cast, if the optional `if` condition " \
|
|
44
|
+
"holds when its total cost is determined (after targets are chosen). `amount` may be " \
|
|
45
|
+
"computed, e.g. {\"count\": {\"each\": \"artifact\", \"controller\": \"you\"}} for " \
|
|
46
|
+
"affinity for artifacts (CR 702.41a). It can't reduce colored mana or go below {0}.",
|
|
47
|
+
rules: %w[601.2f 118.7a 702.41a],
|
|
48
|
+
params: { "amount" => "amount (integer or computed)", "if" => "condition (optional)" },
|
|
49
|
+
example: { "cost_reduction" => { "amount" => 1, "if" => { "attacked" => true } } }
|
|
50
|
+
|
|
51
|
+
attr_reader :rule
|
|
52
|
+
|
|
53
|
+
def self.build(params, errors, path, **)
|
|
54
|
+
new(Reduction.build(params, errors, path))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def initialize(rule)
|
|
58
|
+
super()
|
|
59
|
+
@rule = rule
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reduction(engine, ctx) = rule.reduction(engine, ctx)
|
|
64
|
+
def kind = :cost_reduction
|
|
65
|
+
def mana_ability? = false
|
|
66
|
+
def allowed_types = nil
|
|
67
|
+
def try_effects = []
|
|
68
|
+
def to_s = "This spell #{rule}."
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# "Spells you cast cost {1} less" / "Noncreature spells your opponents cast cost {1} more"
|
|
72
|
+
# (CR 601.2f): a static ability of a permanent that changes the total cost of matching spells.
|
|
73
|
+
class CostModifier < Primitive
|
|
74
|
+
TYPES = %w[artifact creature enchantment land planeswalker instant sorcery].freeze
|
|
75
|
+
CASTERS = %w[you opponent any].freeze
|
|
76
|
+
|
|
77
|
+
primitive :ability, :cost_modifier,
|
|
78
|
+
description: "While on the battlefield: spells cast by `caster` (you, opponent or any; default you) " \
|
|
79
|
+
"whose card types match `spells` ({\"any_of\": [types], \"not\": [types]}, default all " \
|
|
80
|
+
"spells) cost `reduce` less or `increase` more generic mana. `from` / `not_from`: only " \
|
|
81
|
+
"spells cast from (not from) that zone (hand | graveyard | exile | library | outside).",
|
|
82
|
+
rules: %w[601.2f 118.7a 604.1 601.2a],
|
|
83
|
+
params: { "spells" => "{\"any_of\": [types], \"not\": [types]} (optional)",
|
|
84
|
+
"caster" => "you | opponent | any", "reduce" => "amount", "increase" => "amount",
|
|
85
|
+
"from" => "zone", "not_from" => "zone" },
|
|
86
|
+
example: { "cost_modifier" => { "spells" => { "not" => ["creature"] }, "reduce" => 1 } }
|
|
87
|
+
|
|
88
|
+
attr_reader :caster, :any_of, :not_types, :amount, :sign
|
|
89
|
+
|
|
90
|
+
def self.build(params, errors, path, **)
|
|
91
|
+
Mechanics.check_keys(params, %w[spells caster reduce increase] + CastFrom::KEYS, errors, path)
|
|
92
|
+
spells = Mechanics.fetch_param(params, :spells) || {}
|
|
93
|
+
unless spells.is_a?(Hash)
|
|
94
|
+
errors << "#{path}.spells: must be an object"
|
|
95
|
+
spells = {}
|
|
96
|
+
end
|
|
97
|
+
Mechanics.check_keys(spells, %w[any_of not], errors, "#{path}.spells")
|
|
98
|
+
lists = %w[any_of not].to_h do |key|
|
|
99
|
+
list = Array(spells.transform_keys(&:to_s)[key]).map(&:to_s)
|
|
100
|
+
errors << "#{path}.spells.#{key}: must be card types (#{TYPES.join(", ")})" unless (list - TYPES).empty?
|
|
101
|
+
[key, list.map(&:to_sym)]
|
|
102
|
+
end
|
|
103
|
+
caster = Mechanics.fetch_param(params, :caster) || "you"
|
|
104
|
+
errors << "#{path}.caster: must be one of #{CASTERS.join(", ")}" unless CASTERS.include?(caster)
|
|
105
|
+
new(caster, lists["any_of"], lists["not"], *build_amount(params, errors, path),
|
|
106
|
+
zones: CastFrom.build(params, errors, path))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.build_amount(params, errors, path)
|
|
110
|
+
keys = %w[reduce increase].select { params.key?(it) }
|
|
111
|
+
unless keys.size == 1
|
|
112
|
+
errors << "#{path}: needs exactly one of reduce, increase"
|
|
113
|
+
return [Amount::Fixed.new(0), -1]
|
|
114
|
+
end
|
|
115
|
+
[Amount.build(params[keys.first], errors, "#{path}.#{keys.first}", targets_count: 0), keys.first == "reduce" ? -1 : 1]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def initialize(caster, any_of, not_types, amount, sign, zones: CastFrom.new(nil, nil))
|
|
119
|
+
super()
|
|
120
|
+
@caster = caster
|
|
121
|
+
@any_of = any_of.freeze
|
|
122
|
+
@not_types = not_types.freeze
|
|
123
|
+
@amount = amount
|
|
124
|
+
@sign = sign
|
|
125
|
+
@zones = zones
|
|
126
|
+
freeze
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def kind = :cost_modifier
|
|
130
|
+
def mana_ability? = false
|
|
131
|
+
def allowed_types = nil
|
|
132
|
+
def try_effects = []
|
|
133
|
+
|
|
134
|
+
# The generic change this permanent makes to `spell`, cast by player `caster_index`.
|
|
135
|
+
def change_for(engine, permanent, spell, caster_index)
|
|
136
|
+
return 0 unless caster == "any" || (caster == "you") == (caster_index == permanent.controller)
|
|
137
|
+
|
|
138
|
+
types = engine.chars(spell).types
|
|
139
|
+
return 0 if types.intersect?(not_types) || !(any_of.empty? || types.intersect?(any_of))
|
|
140
|
+
return 0 unless @zones.match?(engine.cast_origin(spell))
|
|
141
|
+
|
|
142
|
+
sign * amount.value(engine, EffectContext.new(permanent.controller, permanent.id, permanent.id, []))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def to_s
|
|
146
|
+
kinds = [not_types.map { "non#{it}" }, any_of.join(" or ")].flatten.reject(&:empty?).join(" ")
|
|
147
|
+
whose = { "you" => "you cast", "opponent" => "your opponents cast", "any" => "" }[caster]
|
|
148
|
+
"#{[kinds, "spells", whose].reject(&:empty?).join(" ")}#{@zones} cost {#{amount}} " \
|
|
149
|
+
"#{sign.negative? ? "less" : "more"}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|