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,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Transform permanents (CR 701.27): turn each referenced double-faced permanent over (see
|
|
7
|
+
# Engine::Faces#transform!).
|
|
8
|
+
class Transform < Effect
|
|
9
|
+
primitive :effect, :transform,
|
|
10
|
+
description: "Transform each referenced permanent (default: this one): turn it over so its other " \
|
|
11
|
+
"face is up. It stays the same object, and effects on it keep applying. Nothing " \
|
|
12
|
+
"happens to a permanent that isn't a double-faced card or token, or whose other face " \
|
|
13
|
+
"is an instant or sorcery. An ability of the permanent transforms it only if it hasn't " \
|
|
14
|
+
"transformed since the ability was put on the stack.",
|
|
15
|
+
rules: %w[701.27a 701.27c 701.27d 701.27e 701.27g 712.15],
|
|
16
|
+
params: { "to" => "selector (default \"self\")" },
|
|
17
|
+
example: { "transform" => { "to" => "self" } }
|
|
18
|
+
|
|
19
|
+
def self.keys = %w[to if]
|
|
20
|
+
def self.default_to = "self"
|
|
21
|
+
|
|
22
|
+
def apply(engine, ctx)
|
|
23
|
+
objects(engine, ctx).each do |id|
|
|
24
|
+
obj = engine.object(id)
|
|
25
|
+
engine.transform!(obj) if engine.transform_still_allowed?(obj, ctx)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The prepared designation (CR 723.3a-b).
|
|
31
|
+
class Prepare < Effect
|
|
32
|
+
primitive :effect, :prepare,
|
|
33
|
+
description: "Each referenced permanent (default: this one) becomes prepared, if it has a prepare " \
|
|
34
|
+
"spell and isn't prepared already: its controller creates a copy of the prepare spell " \
|
|
35
|
+
"in exile and may cast that copy while the permanent stays prepared. Casting it " \
|
|
36
|
+
"unprepares the permanent.",
|
|
37
|
+
rules: %w[723.3a 723.3c],
|
|
38
|
+
params: { "to" => "selector (default \"self\")" },
|
|
39
|
+
example: { "prepare" => { "to" => "self" } }
|
|
40
|
+
|
|
41
|
+
def self.keys = %w[to if]
|
|
42
|
+
def self.default_to = "self"
|
|
43
|
+
|
|
44
|
+
def apply(engine, ctx) = objects(engine, ctx).each { engine.prepare!(engine.object(it)) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# CR 723.3b: "becomes unprepared".
|
|
48
|
+
class Unprepare < Effect
|
|
49
|
+
primitive :effect, :unprepare,
|
|
50
|
+
description: "Each referenced permanent (default: this one) loses the prepared designation; the " \
|
|
51
|
+
"copy of its prepare spell in exile ceases to exist.",
|
|
52
|
+
rules: %w[723.3b 723.3c],
|
|
53
|
+
params: { "to" => "selector (default \"self\")" },
|
|
54
|
+
example: { "unprepare" => { "to" => "self" } }
|
|
55
|
+
|
|
56
|
+
def self.keys = %w[to if]
|
|
57
|
+
def self.default_to = "self"
|
|
58
|
+
|
|
59
|
+
def apply(engine, ctx) = objects(engine, ctx).each { engine.unprepare!(engine.object(it)) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module Abilities
|
|
64
|
+
# "This creature enters prepared." (CR 723.3a): a self-replacement effect on its own entering.
|
|
65
|
+
class EntersPrepared < Primitive
|
|
66
|
+
primitive :ability, :enters_prepared,
|
|
67
|
+
description: "This permanent enters the battlefield prepared (it needs a prepare spell, see " \
|
|
68
|
+
"`layout: \"prepare\"`).",
|
|
69
|
+
rules: %w[723.3a 614.1c],
|
|
70
|
+
example: "enters_prepared"
|
|
71
|
+
|
|
72
|
+
INSTANCE = new.freeze
|
|
73
|
+
|
|
74
|
+
def self.build(params, errors, path, **)
|
|
75
|
+
Mechanics.check_keys(params, [], errors, path)
|
|
76
|
+
INSTANCE
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def kind = :enters_prepared
|
|
80
|
+
def mana_ability? = false
|
|
81
|
+
def allowed_types = nil
|
|
82
|
+
def to_s = "enters prepared"
|
|
83
|
+
def try_effects = []
|
|
84
|
+
|
|
85
|
+
def validate_definition(defn, errors, path)
|
|
86
|
+
errors << "#{path}: only a card with a prepare spell (layout \"prepare\") enters prepared" unless defn.layout == "prepare"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def modify_entering(enters) = enters["prepared"] = true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
module Triggers
|
|
94
|
+
# "Whenever this creature transforms into [this face]" (CR 701.27f): a self trigger is an ability of the
|
|
95
|
+
# face it transformed into, so it triggers only on transforming into that face.
|
|
96
|
+
class Transforms < Trigger
|
|
97
|
+
primitive :trigger, :transforms,
|
|
98
|
+
description: "The subject transforms. For the default subject (this permanent), the ability is on " \
|
|
99
|
+
"the face that's up afterward, so it means \"whenever this transforms into [this face]\".",
|
|
100
|
+
rules: %w[701.27f 603.2],
|
|
101
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
|
|
102
|
+
example: { "transforms" => {} }
|
|
103
|
+
|
|
104
|
+
def event_class = Events::Transformed
|
|
105
|
+
|
|
106
|
+
def match?(engine, event, source)
|
|
107
|
+
obj = engine.state.objects[event.object] or return false
|
|
108
|
+
@subject.match?(engine, event.object, obj.controller, obj.card, source, types: engine.chars(obj).types)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_s = "whenever #{@subject} transforms"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Base for effects with a second selector besides `to` (`with` / `from`).
|
|
7
|
+
class PairEffect < Effect
|
|
8
|
+
def self.other_key = raise(NotImplementedError)
|
|
9
|
+
def self.keys = ["to", other_key, "if"]
|
|
10
|
+
|
|
11
|
+
attr_reader :other
|
|
12
|
+
|
|
13
|
+
def setup(params, errors, path)
|
|
14
|
+
key = self.class.other_key
|
|
15
|
+
raw = Mechanics.fetch_param(params, key)
|
|
16
|
+
@other = if raw.nil?
|
|
17
|
+
(errors << "#{path}.#{key}: required") && nil
|
|
18
|
+
else
|
|
19
|
+
Selector.build(raw, errors, "#{path}.#{key}", targets_count: @targets_count, results_count: @results_count)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def other_creatures(engine, ctx)
|
|
24
|
+
other.resolve(engine, ctx).select { engine.state.battlefield.include?(it) && engine.chars(it).creature? }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def creatures(engine, ctx) = objects(engine, ctx).select { engine.chars(it).creature? }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# "Target creature you control fights target creature an opponent controls." (CR 701.14)
|
|
31
|
+
class Fight < PairEffect
|
|
32
|
+
primitive :effect, :fight,
|
|
33
|
+
description: "The creature referenced by `to` fights the one referenced by `with`: each deals " \
|
|
34
|
+
"damage equal to its power to the other at the same time (not combat damage). If " \
|
|
35
|
+
"either is gone, no longer a creature or an illegal target, neither deals damage. " \
|
|
36
|
+
"{\"this_way\": n} naming it is the excess damage dealt to the `with` creature.",
|
|
37
|
+
rules: %w[701.14a 701.14b 701.14c 701.14d 120.4a],
|
|
38
|
+
params: { "to" => "selector (one creature)", "with" => "selector (one creature)" },
|
|
39
|
+
example: { "fight" => { "to" => { "target" => 0 }, "with" => { "target" => 1 } } }
|
|
40
|
+
|
|
41
|
+
def self.other_key = "with"
|
|
42
|
+
|
|
43
|
+
def apply(engine, ctx)
|
|
44
|
+
one = refs(engine, ctx).first
|
|
45
|
+
other_one = other.resolve(engine, ctx).first
|
|
46
|
+
# Nothing happens unless both are creatures on the battlefield. The excess damage dealt to the
|
|
47
|
+
# `with` creature is what {"this_way": n} reads (CR 120.4a).
|
|
48
|
+
engine.record_amount!(engine.fight!(one, other_one))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_s = "#{to} fights #{other}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# "Target creatures you control each deal damage equal to their power to target creature." ("bite")
|
|
55
|
+
class Bite < PairEffect
|
|
56
|
+
primitive :effect, :bite,
|
|
57
|
+
description: "Each creature referenced by `from` deals damage equal to its power to each creature, " \
|
|
58
|
+
"planeswalker or player referenced by `to` (one damage event per source; not combat " \
|
|
59
|
+
"damage, and nothing is dealt back).",
|
|
60
|
+
rules: %w[120.3 120.4 608.2b],
|
|
61
|
+
params: { "from" => "selector (the creatures dealing damage)", "to" => "selector" },
|
|
62
|
+
example: { "bite" => { "from" => { "target" => 0 }, "to" => { "target" => 1 } } }
|
|
63
|
+
|
|
64
|
+
def self.other_key = "from"
|
|
65
|
+
|
|
66
|
+
def apply(engine, ctx)
|
|
67
|
+
sources = other_creatures(engine, ctx)
|
|
68
|
+
powers = sources.to_h { [it, engine.power(engine.object(it))] } # determined before any is dealt
|
|
69
|
+
sources.each { |source| refs(engine, ctx).each { engine.deal_damage!(source, it, powers[source]) } }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_s = "#{other} each deal damage equal to their power → #{to}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../abilities/turn_rules"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Effects
|
|
8
|
+
# Flipping coins (CR 705). With `count: "wins"` (default) the flipper calls each coin, heads or tails,
|
|
9
|
+
# and wins the flips whose result matches the call (CR 705.2); with `count: "heads"` the effect only
|
|
10
|
+
# cares how many coins come up heads and nobody wins or loses. The number counted is the effect's
|
|
11
|
+
# amount for later effects ({"this_way": n}): "if you win the flip, ..." is an `if` comparing it.
|
|
12
|
+
class FlipCoins < ChoiceEffect
|
|
13
|
+
COUNTS = %w[wins heads].freeze
|
|
14
|
+
CALLS = [{ "type" => "call", "call" => "heads" }, { "type" => "call", "call" => "tails" }].freeze
|
|
15
|
+
|
|
16
|
+
primitive :effect, :flip_coins,
|
|
17
|
+
description: "You flip `amount` coins (default 1), each coming up heads or tails at random (the " \
|
|
18
|
+
"game's random number generator). `count`: \"wins\" (default; you call each flip and " \
|
|
19
|
+
"win it if the call matches) or \"heads\" (no one wins or loses). How many were " \
|
|
20
|
+
"counted is this effect's amount: {\"this_way\": n} in later effects and conditions.",
|
|
21
|
+
rules: %w[705.1 705.2 705.3 608.2c],
|
|
22
|
+
params: { "amount" => "amount (default 1)", "count" => COUNTS.join(" | ") },
|
|
23
|
+
example: { "flip_coins" => {} }
|
|
24
|
+
|
|
25
|
+
def self.keys = %w[amount count if]
|
|
26
|
+
def self.default_to = "you"
|
|
27
|
+
|
|
28
|
+
def initialize(to:, params:, errors:, path:, targets_count: 0, results_count: 0)
|
|
29
|
+
params = params.transform_keys(&:to_s)
|
|
30
|
+
params["amount"] = 1 unless params.key?("amount")
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def setup(params, errors, path)
|
|
35
|
+
@count = Mechanics.fetch_param(params, :count) || "wins"
|
|
36
|
+
errors << "#{path}.count: must be one of #{COUNTS.join(", ")}" unless COUNTS.include?(@count)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s = "flip #{amount == 1 ? "a coin" : "#{amount} coins"} (counting #{@count})"
|
|
40
|
+
|
|
41
|
+
# A call is asked before each flip (CR 705.2); the flips happen as they are called.
|
|
42
|
+
def run(engine, ctx, locals)
|
|
43
|
+
unless locals["left"]
|
|
44
|
+
return :done unless condition.nil? || condition.true?(engine, ctx)
|
|
45
|
+
|
|
46
|
+
locals["left"] = amount_for(engine, ctx)
|
|
47
|
+
locals["fixed"] = true if Abilities::FirstFlipsWin.applies?(engine, ctx.controller) # CR 705.3
|
|
48
|
+
end
|
|
49
|
+
calls = @count == "wins" && !locals["fixed"] # a flip with a stated result needs no call (CR 705.3)
|
|
50
|
+
while locals["left"].positive?
|
|
51
|
+
call = calls ? answers(locals).shift&.fetch("call") : nil
|
|
52
|
+
return ask(engine, locals, ctx.controller, "Call the coin flip", CALLS) if calls && call.nil?
|
|
53
|
+
|
|
54
|
+
flip!(engine, ctx, call, fixed: locals["fixed"])
|
|
55
|
+
locals["left"] -= 1
|
|
56
|
+
end
|
|
57
|
+
:done
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# CR 705.3: a flip with a stated result (heads, and won) ignores the actual result.
|
|
63
|
+
def flip!(engine, ctx, call, fixed: false)
|
|
64
|
+
heads = fixed || engine.state.rng.rand(2).zero?
|
|
65
|
+
won = fixed ? @count == "wins" || nil : call && (call == "heads") == heads
|
|
66
|
+
call = "heads" if fixed && @count == "wins"
|
|
67
|
+
engine.emit Events::CoinFlipped.new(player: engine.name(ctx.controller), heads: heads, won: won)
|
|
68
|
+
engine.record_amount!(1) if call ? won : heads
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
module Abilities
|
|
74
|
+
# "The first time you flip one or more coins each turn, those coins come up heads and you win those flips"
|
|
75
|
+
# (CR 705.3): the flips of the first flip_coins effect its controller performs each turn have that result
|
|
76
|
+
# (no call is asked). A flip is remembered in the turn's history (Engine::TurnHistory "coin_flips").
|
|
77
|
+
class FirstFlipsWin < TurnRule
|
|
78
|
+
primitive :ability, :first_flips_win,
|
|
79
|
+
description: "The first time each turn its controller flips one or more coins, those coins come up " \
|
|
80
|
+
"heads and they win those flips.",
|
|
81
|
+
rules: %w[705.3 604.1], example: "first_flips_win"
|
|
82
|
+
|
|
83
|
+
INSTANCE = new.freeze
|
|
84
|
+
|
|
85
|
+
def kind = :first_flips_win
|
|
86
|
+
def to_s = "The first time you flip one or more coins each turn, those coins come up heads and you win those flips"
|
|
87
|
+
|
|
88
|
+
def self.applies?(engine, index)
|
|
89
|
+
engine.turn_log(index).fetch("coin_flips", 0).zero? &&
|
|
90
|
+
engine.state.battlefield.any? do |id|
|
|
91
|
+
engine.object(id).controller == index && engine.chars(id).abilities.any? { it.ability.kind == :first_flips_win }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# "Gain control of target permanent" / "... until end of turn" / "Target opponent gains control of
|
|
7
|
+
# target permanent you control" (CR 613.1b, 108.4, 506.4, 302.6).
|
|
8
|
+
class GainControl < Effect
|
|
9
|
+
DURATIONS = %w[permanent end_of_turn while_source_on_battlefield while_source_tapped while_you_control_source].freeze
|
|
10
|
+
|
|
11
|
+
primitive :effect, :gain_control,
|
|
12
|
+
description: "The player referenced by `gains` (default you) gains control of each referenced " \
|
|
13
|
+
"permanent: indefinitely (`duration: \"permanent\"`, default), until end of turn, or for " \
|
|
14
|
+
"as long as this effect's source remains on the battlefield / tapped / under your control (it does " \
|
|
15
|
+
"nothing if that is already over). " \
|
|
16
|
+
"A permanent that changes control leaves combat and is newly under its controller's " \
|
|
17
|
+
"control (it can't attack or {T} without haste).",
|
|
18
|
+
rules: %w[613.1b 108.4 506.4 302.6 514.2 611.2b],
|
|
19
|
+
params: { "to" => "selector (permanents)", "gains" => "selector (a player; default \"you\")",
|
|
20
|
+
"duration" => DURATIONS.join(" | ") },
|
|
21
|
+
example: { "gain_control" => { "to" => { "target" => 0 }, "duration" => "end_of_turn" } }
|
|
22
|
+
|
|
23
|
+
attr_reader :gains, :duration
|
|
24
|
+
|
|
25
|
+
def self.keys = %w[to gains duration if]
|
|
26
|
+
|
|
27
|
+
def setup(params, errors, path)
|
|
28
|
+
@gains = Selector.build(Mechanics.fetch_param(params, :gains) || "you", errors, "#{path}.gains",
|
|
29
|
+
targets_count: @targets_count, results_count: @results_count)
|
|
30
|
+
@duration = Mechanics.fetch_param(params, :duration) || "permanent"
|
|
31
|
+
errors << "#{path}.duration: must be one of #{DURATIONS.join(", ")}" unless DURATIONS.include?(@duration)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def apply(engine, ctx)
|
|
35
|
+
player = gains.resolve(engine, ctx).filter_map { Filters.player_index(engine, it) }.first or return
|
|
36
|
+
return unless engine.source_duration_holds?(duration, ctx.source, ctx.controller)
|
|
37
|
+
|
|
38
|
+
objects(engine, ctx).each do |id|
|
|
39
|
+
obj = engine.object(id)
|
|
40
|
+
next engine.gain_control!(obj, player) if duration == "permanent"
|
|
41
|
+
|
|
42
|
+
engine.gain_control_for!(obj, player, ctx, duration: duration,
|
|
43
|
+
source: duration == "end_of_turn" ? nil : ctx.source)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s = "#{gains} gains control#{" until end of turn" if duration == "end_of_turn"} → #{to}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# "Exchange control of target artifact you control and target artifact an opponent controls" (CR 701.12a-b):
|
|
51
|
+
# if both permanents are still there and controlled by different players, each player gains control of
|
|
52
|
+
# the other one (indefinitely); otherwise nothing happens.
|
|
53
|
+
class ExchangeControl < Effect
|
|
54
|
+
primitive :effect, :exchange_control,
|
|
55
|
+
description: "The permanent `to` references and the one `with` references exchange controllers. " \
|
|
56
|
+
"If either is gone or both have the same controller, nothing happens.",
|
|
57
|
+
rules: %w[701.12a 701.12b 613.1b],
|
|
58
|
+
params: { "to" => "selector (a permanent)", "with" => "selector (a permanent)" },
|
|
59
|
+
example: { "exchange_control" => { "to" => { "target" => 0 }, "with" => { "target" => 1 } } }
|
|
60
|
+
|
|
61
|
+
attr_reader :with
|
|
62
|
+
|
|
63
|
+
def self.keys = %w[to with if]
|
|
64
|
+
|
|
65
|
+
def setup(params, errors, path)
|
|
66
|
+
@with = Exchange.with(params, errors, path, @targets_count, @results_count)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def apply(engine, ctx)
|
|
70
|
+
first, second = [to, with].map { |sel| sel.resolve(engine, ctx).find { engine.state.battlefield.include?(it) } }
|
|
71
|
+
return if first.nil? || second.nil? || first == second
|
|
72
|
+
|
|
73
|
+
a = engine.object(first)
|
|
74
|
+
b = engine.object(second)
|
|
75
|
+
return if a.controller == b.controller
|
|
76
|
+
|
|
77
|
+
timestamp = engine.next_timestamp!
|
|
78
|
+
[[a, b.controller], [b, a.controller]].each { |obj, controller| engine.gain_control!(obj, controller, timestamp) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_s = "exchange control of #{to} and #{with}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The `with` side of an exchange (CR 701.12a): a required selector.
|
|
85
|
+
module Exchange
|
|
86
|
+
def self.with(params, errors, path, targets_count, results_count)
|
|
87
|
+
raw = Mechanics.fetch_param(params, :with)
|
|
88
|
+
errors << "#{path}.with: required" if raw.nil?
|
|
89
|
+
Selector.build(raw || "you", errors, "#{path}.with", targets_count: targets_count, results_count: results_count)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Base for effects that may end the game: once it's over, nothing else in the list happens.
|
|
7
|
+
class GameEndEffect < Effect
|
|
8
|
+
def self.keys = %w[to if]
|
|
9
|
+
def self.default_to = "you"
|
|
10
|
+
|
|
11
|
+
def run(engine, ctx, locals)
|
|
12
|
+
super
|
|
13
|
+
engine.state.over? ? :wait : :done # the game ended: resolution stops here
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# "You win the game." (CR 104.2b)
|
|
18
|
+
class WinGame < GameEndEffect
|
|
19
|
+
primitive :effect, :win_game,
|
|
20
|
+
description: "Each referenced player (default you) wins the game: every opponent loses it.",
|
|
21
|
+
rules: %w[104.2a 104.2b 104.3f],
|
|
22
|
+
params: { "to" => "selector" },
|
|
23
|
+
example: "win_game"
|
|
24
|
+
|
|
25
|
+
def apply(engine, ctx) = players(engine, ctx).each { engine.win_game!(it) }
|
|
26
|
+
def to_s = "win the game → #{to}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# "You lose the game." (CR 104.3e)
|
|
30
|
+
class LoseGame < GameEndEffect
|
|
31
|
+
primitive :effect, :lose_game,
|
|
32
|
+
description: "Each referenced player (default you) loses the game.",
|
|
33
|
+
rules: %w[104.3e 104.4a],
|
|
34
|
+
params: { "to" => "selector" },
|
|
35
|
+
example: "lose_game"
|
|
36
|
+
|
|
37
|
+
def apply(engine, ctx) = players(engine, ctx).each { engine.lose_game!(it) }
|
|
38
|
+
def to_s = "lose the game → #{to}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|