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,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
class Engine
|
|
5
|
+
# Characteristics after continuous effects (CR 613).
|
|
6
|
+
#
|
|
7
|
+
# The layer pass runs over the whole battlefield in three stages, each finished for every
|
|
8
|
+
# object before the next starts, so an effect in a later layer sees the earlier layers' result:
|
|
9
|
+
# layers 1-4 the object's own changes (station symbols, assimilate) and type-changing effects
|
|
10
|
+
# layers 5-6 keywords and granted abilities
|
|
11
|
+
# layer 7 7a characteristic-defining abilities, 7b setting effects, 7c modifications (effects,
|
|
12
|
+
# counters, static pumps), 7d switching (none yet)
|
|
13
|
+
# While the pass runs, `chars` answers from the stages done so far, so a static ability counting
|
|
14
|
+
# "artifact creatures you control" sees types after layer 4 without recursing (CR 613.8 guards keep
|
|
15
|
+
# statics from reading power, toughness or combat status).
|
|
16
|
+
module Characteristics
|
|
17
|
+
# The object's current characteristics. Off the battlefield: as printed (with a characteristic-
|
|
18
|
+
# defining power/toughness, CR 604.3). On the battlefield: the result of a whole-board layer pass,
|
|
19
|
+
# cached until state changes.
|
|
20
|
+
def chars(object)
|
|
21
|
+
object = self.object(object) if object.is_a?(String)
|
|
22
|
+
return emblem_characteristics(object) if object.kind == "emblem" # CR 114.3 (Engine::Emblems)
|
|
23
|
+
return Enjine::Characteristics::FACE_DOWN if face_down?(object) && @face_up != object.id # CR 406.3a
|
|
24
|
+
|
|
25
|
+
# layer 1 (CR 613.2a-c): its copiable values
|
|
26
|
+
return board_characteristics.fetch(object.id) { copy_base(object) } if object.zone == :battlefield
|
|
27
|
+
return @options_memo[object.id] ||= off_board_chars(object) if @options_memo && object.zone == :hand
|
|
28
|
+
|
|
29
|
+
off_board_chars(object)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Off the battlefield. The options of one decision ask about each card in hand often (Engine#options).
|
|
33
|
+
def off_board_chars(object)
|
|
34
|
+
defn = definition(object)
|
|
35
|
+
base = copy_base(object)
|
|
36
|
+
base = defined_power_toughness(object, base, defn) if defined_pt_copied?(object)
|
|
37
|
+
base = with_spell_grants(object, base) if object.zone == :stack # Engine::SpellGrants
|
|
38
|
+
with_max_speed(object, base) # CR 702.178b: e.g. an ability that works from the graveyard (Engine::Speed)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def power(object) = chars(object).power.to_i
|
|
42
|
+
def toughness(object) = chars(object).toughness.to_i
|
|
43
|
+
def power_toughness(object) = chars(object).then { [it.power.to_i, it.toughness.to_i] }
|
|
44
|
+
def loyalty(object) = object.counters.fetch("loyalty", 0)
|
|
45
|
+
|
|
46
|
+
# Events after which characteristics may differ; any other event (taps, mana, priority)
|
|
47
|
+
# keeps the cached layer pass. Replacing the whole state (import, fork, rollback) is
|
|
48
|
+
# detected separately by identity. While a static ability has a condition or a computed bonus, or a
|
|
49
|
+
# permanent has a characteristic-defining power/toughness, any event may change the result, so every
|
|
50
|
+
# event drops the cache (`@volatile`, see Engine#emit).
|
|
51
|
+
LAYER_EVENTS = [Events::ZoneChanged, Events::CountersChanged, Events::EffectStarted,
|
|
52
|
+
Events::EffectsEnded, Events::Attached, Events::Unattached, Events::ControlChanged,
|
|
53
|
+
Events::PhasedOut, Events::PhasedIn, Events::SpeedChanged,
|
|
54
|
+
Events::Transformed, Events::ClassLevelChanged, Events::EmblemCreated].freeze
|
|
55
|
+
|
|
56
|
+
def touch! = @epoch = @epoch.to_i + 1
|
|
57
|
+
def volatile_layers? = @volatile
|
|
58
|
+
|
|
59
|
+
def board_characteristics
|
|
60
|
+
return @layering if @layering # mid-pass: the layers applied so far
|
|
61
|
+
return @board if @board && @board_epoch == @epoch && @board_state.equal?(state)
|
|
62
|
+
|
|
63
|
+
@board_epoch = @epoch
|
|
64
|
+
@board_state = state
|
|
65
|
+
@board = apply_layers
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# CR 613.1, 613.4. Only modified objects get an entry; the rest use their printed characteristics.
|
|
69
|
+
def apply_layers
|
|
70
|
+
statics = static_sources
|
|
71
|
+
effects = state.effects
|
|
72
|
+
@volatile = statics.any? { |_, ability| ability.condition || ability.computed? } || defined_on_battlefield?
|
|
73
|
+
return {} if statics.empty? && effects.empty? && !@volatile && printed_board?
|
|
74
|
+
|
|
75
|
+
@layering = {}
|
|
76
|
+
ids = state.battlefield
|
|
77
|
+
active = statics.select { |source, ability| ability.active?(self, source) }
|
|
78
|
+
ids.each { @layering[it] = type_layers(object(it), active, effects) }
|
|
79
|
+
later_layers(ids, statics, ids.select { losing_abilities?(object(it), active, effects) }, effects)
|
|
80
|
+
@layering.reject { |id, c| c.equal?(definition(object(id)).printed_characteristics) }
|
|
81
|
+
ensure
|
|
82
|
+
@layering = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Layers 5-7. CR 613.8a: a static ability of a permanent that loses all abilities in layer 6 no longer
|
|
86
|
+
# exists there, so its layer 5-7 effects depend on the removal and don't apply (its layer 1-4 effects
|
|
87
|
+
# already have). `stripped`: the ids of those permanents.
|
|
88
|
+
def later_layers(ids, statics, stripped, effects)
|
|
89
|
+
live = proc { |source, ability| !stripped.include?(source.id) && ability.active?(self, source) }
|
|
90
|
+
active = statics.select(&live)
|
|
91
|
+
ids.each { @layering[it] = ability_layers(object(it), @layering[it], active, effects) }
|
|
92
|
+
active = statics.select(&live)
|
|
93
|
+
bonuses = active.to_h { |source, ability| [[source.id, ability], ability.bonus(self, source)] }
|
|
94
|
+
ids.each do |id|
|
|
95
|
+
@layering[id] = power_toughness_layers(object(id), @layering[id], active, bonuses, effects, stripped: stripped.include?(id))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Without effects, every permanent is as printed: none has counters or is a noncreature with printed
|
|
100
|
+
# power and toughness (an uncrewed Vehicle has none, CR 208.3).
|
|
101
|
+
# A max speed ability applying changes it too (Engine::Speed).
|
|
102
|
+
def printed_board?
|
|
103
|
+
speedy = state.players.each_index.any? { max_speed?(it) }
|
|
104
|
+
state.battlefield.none? do |id|
|
|
105
|
+
obj = object(id)
|
|
106
|
+
!obj.counters.empty? || printed_noncreature_pt?(obj) || (speedy && !active_max_speed(obj, obj.controller).empty?) ||
|
|
107
|
+
obj.memory.key?("class_level") || obj.memory.key?("haste_while_controlled_by") # CR 717.2a, 702.62a
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def defined_on_battlefield? = state.battlefield.any? { definition(object(it)).defined_pt? }
|
|
112
|
+
|
|
113
|
+
def facts(obj, current) = Mechanics::Filters::Facts.new(chars: current, counters: obj.counters, token: token?(obj))
|
|
114
|
+
|
|
115
|
+
# ---- layer 1 (CR 613.2a-c, 707.2) ---------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
# The object's copiable values (CR 707.2): the printed characteristics of its card, which is the
|
|
118
|
+
# copied card while a copy effect applies (see Engine::Copies), with the exceptions of the copy effect
|
|
119
|
+
# (CR 707.9a-b: "except it isn't legendary", "except it has menace"). Then the characteristic-defining
|
|
120
|
+
# changeling (CR 702.73a, all zones) and abilities a card outside the battlefield gained (CR 113.6b).
|
|
121
|
+
def copy_base(obj)
|
|
122
|
+
defn = definition(obj)
|
|
123
|
+
except = copy_exceptions(obj)
|
|
124
|
+
base = if except && !except.empty?
|
|
125
|
+
Mechanics::Keywords::Changeling.apply(Copies.apply_exceptions(defn.printed_characteristics, except))
|
|
126
|
+
elsif defn.keywords.include?(:changeling)
|
|
127
|
+
(@changeling_printed ||= {})[defn.id] ||= Mechanics::Keywords::Changeling.apply(defn.printed_characteristics)
|
|
128
|
+
else
|
|
129
|
+
defn.printed_characteristics
|
|
130
|
+
end
|
|
131
|
+
return base if state.effects.none? { it["kind"] == "card_ability" }
|
|
132
|
+
|
|
133
|
+
granted = card_granted_abilities(obj)
|
|
134
|
+
granted.empty? ? base : base.with(abilities: (base.abilities + granted).freeze)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# CR 604.3, 707.9d: a characteristic-defining power/toughness, unless a copy exception sets them.
|
|
138
|
+
def defined_pt_copied?(obj)
|
|
139
|
+
except = copy_exceptions(obj)
|
|
140
|
+
definition(obj).defined_pt? && !(except&.key?("power") || except&.key?("toughness"))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Layers 1-4: the object's own changes and resolved effects that add types (#own_layers),
|
|
144
|
+
# then static abilities that add card types ("this Vehicle is an artifact creature", CR 205.1b), then effects
|
|
145
|
+
# that set its creature types (CR 205.1a, 613.1d), statics before resolved effects, each in timestamp order.
|
|
146
|
+
# A noncreature permanent has no power or toughness, even a Vehicle with them printed (CR 208.3, 301.7a).
|
|
147
|
+
def type_layers(obj, statics, effects)
|
|
148
|
+
base = own_layers(obj, copy_base(obj), effects)
|
|
149
|
+
types = nil
|
|
150
|
+
subtypes = []
|
|
151
|
+
matching = statics.select { |source, ability| ability.type_changes? && ability.affects?(obj, source, facts(obj, base)) }
|
|
152
|
+
# Layers 3-4 in timestamp order (CR 613.7a): a name or card types set later win (CR 612.8, 205.1a).
|
|
153
|
+
matching = matching.each_with_index.sort_by { |(source, _), i| [timestamp_of(source), i] }.map(&:first) if matching.size > 1
|
|
154
|
+
matching.each do |source, ability|
|
|
155
|
+
base = base.with(name: ability.name) if ability.name
|
|
156
|
+
base = with_set_types(base, ability.set_types) if ability.set_types
|
|
157
|
+
types = ability.creature_types if ability.creature_types
|
|
158
|
+
base = with_added_types(base, ability.card_types) unless ability.card_types.empty?
|
|
159
|
+
subtypes |= ability.added_subtypes(source)
|
|
160
|
+
end
|
|
161
|
+
effects.each { types = it["types"].map(&:to_sym) if it["kind"] == "creature_types" && it["affected"].include?(obj.id) }
|
|
162
|
+
base = base.with(subtypes: ((base.subtypes & Characteristics::NON_CREATURE_SUBTYPES) | types).freeze) if types
|
|
163
|
+
base = with_gained_subtypes(base, subtypes - base.subtypes) unless (subtypes - base.subtypes).empty?
|
|
164
|
+
base.creature? || base.power.nil? ? base : base.with(power: nil, toughness: nil)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Subtypes it has in addition to its others; a land that gains a basic land type has that type's mana
|
|
168
|
+
# ability (CR 305.6).
|
|
169
|
+
def with_gained_subtypes(base, gained)
|
|
170
|
+
intrinsic = base.land? ? gained.flat_map { CardDefinition.intrinsic_instances(it) } : []
|
|
171
|
+
base.with(subtypes: (base.subtypes | gained).freeze,
|
|
172
|
+
abilities: intrinsic.empty? ? base.abilities : (base.abilities + intrinsic).freeze)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Card types added "in addition to its other types" (CR 205.1b). A permanent that becomes a creature has
|
|
176
|
+
# its printed power and toughness (CR 301.7b), or 0/0 without any, until a layer 7 effect changes them.
|
|
177
|
+
def with_added_types(base, added)
|
|
178
|
+
return base if (added - base.types).empty?
|
|
179
|
+
|
|
180
|
+
base = base.with(types: (base.types | added).freeze)
|
|
181
|
+
base.creature? && base.power.nil? ? base.with(power: 0, toughness: 0) : base
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# "Is a black Skeleton creature" (CR 205.1a): the card types are exactly these, and subtypes correlated with
|
|
185
|
+
# a card type it no longer has are lost (creature types without creature, an Equipment without artifact).
|
|
186
|
+
def with_set_types(base, set)
|
|
187
|
+
kept = base.subtypes.select do |sub|
|
|
188
|
+
correlated = CORRELATED_SUBTYPES.find { |_, list| list.include?(sub) }&.first || :creature
|
|
189
|
+
set.include?(correlated)
|
|
190
|
+
end
|
|
191
|
+
base = base.with(types: set, subtypes: kept.freeze)
|
|
192
|
+
base.creature? && base.power.nil? ? base.with(power: 0, toughness: 0) : base
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def printed_noncreature_pt?(obj) = definition(obj).then { !it.creature? && !it.power.nil? }
|
|
196
|
+
|
|
197
|
+
# Layer 7 (CR 613.4): 7a defined power/toughness (not once the ability defining them is lost in layer 6,
|
|
198
|
+
# CR 613.1f), 7b setting effects (statics by their source's timestamp and resolved effects, the latest
|
|
199
|
+
# last), 7c counters, static bonuses and pumps (CR 613.4c). Objects without power and toughness are
|
|
200
|
+
# unchanged.
|
|
201
|
+
def power_toughness_layers(obj, base, statics, bonuses, effects, stripped: false)
|
|
202
|
+
defn = definition(obj)
|
|
203
|
+
if base.creature? && defined_pt_copied?(obj)
|
|
204
|
+
# without the ability defining it, an undefined power or toughness is 0 (CR 107.2)
|
|
205
|
+
base = stripped ? base.with(power: base.power || 0, toughness: base.toughness || 0) : defined_power_toughness(obj, base, defn)
|
|
206
|
+
end
|
|
207
|
+
return base if base.power.nil?
|
|
208
|
+
|
|
209
|
+
power, toughness = base_setting(obj, base, statics, effects) || [base.power, base.toughness]
|
|
210
|
+
delta = Mechanics::Counters.power_toughness(obj.counters)
|
|
211
|
+
statics.each do |source, ability|
|
|
212
|
+
next unless ability.pump? && ability.affects?(obj, source, facts(obj, base))
|
|
213
|
+
|
|
214
|
+
bonus = bonuses.fetch([source.id, ability])
|
|
215
|
+
delta = [delta[0] + bonus[0], delta[1] + bonus[1]]
|
|
216
|
+
end
|
|
217
|
+
effects.each do
|
|
218
|
+
delta = [delta[0] + it["power"], delta[1] + it["toughness"]] if it["kind"] == "pump" && it["affected"].include?(obj.id)
|
|
219
|
+
end
|
|
220
|
+
power += delta[0]
|
|
221
|
+
toughness += delta[1]
|
|
222
|
+
return base if power == base.power && toughness == base.toughness
|
|
223
|
+
|
|
224
|
+
base.with(power: power, toughness: toughness)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Layer 7b (CR 613.4b): the latest base power/toughness setting, or nil.
|
|
228
|
+
def base_setting(obj, base, statics, effects)
|
|
229
|
+
sets = statics.filter_map do |source, ability|
|
|
230
|
+
next unless ability.base_power_toughness && ability.affects?(obj, source, facts(obj, base))
|
|
231
|
+
|
|
232
|
+
[timestamp_of(source), ability.base_power_toughness.values_at("power", "toughness")]
|
|
233
|
+
end
|
|
234
|
+
effects.each do |effect|
|
|
235
|
+
next unless effect["kind"] == "base_power_toughness" && effect["affected"].include?(obj.id)
|
|
236
|
+
|
|
237
|
+
sets << [effect["timestamp"], effect.values_at("power", "toughness")]
|
|
238
|
+
end
|
|
239
|
+
sets.max_by(&:first)&.last
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# CR 604.3, 613.4a: power/toughness from a characteristic-defining ability, in every zone.
|
|
243
|
+
def defined_power_toughness(obj, base, defn)
|
|
244
|
+
ctx = Mechanics::EffectContext.new(obj.controller, obj.id, obj.id, [], [])
|
|
245
|
+
base.with(power: defn.power_amount ? defn.power_amount.value(self, ctx) : base.power,
|
|
246
|
+
toughness: defn.toughness_amount ? defn.toughness_amount.value(self, ctx) : base.toughness)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# [[source object, StaticAbility], ...] for permanents on the battlefield (printed abilities:
|
|
250
|
+
# an ability granting static abilities would need layer dependencies, CR 613.8).
|
|
251
|
+
def static_sources
|
|
252
|
+
state.battlefield.flat_map do |id|
|
|
253
|
+
obj = object(id)
|
|
254
|
+
own_ability_instances(obj).filter_map { [obj, it.ability] if it.ability.kind == :static }
|
|
255
|
+
end + emblems.flat_map { |obj| chars(obj).abilities.filter_map { [obj, it.ability] if it.ability.kind == :static } }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Starts a continuous effect from a resolving spell or ability (CR 611.2).
|
|
259
|
+
# The affected set is locked in now (CR 611.2c). An indefinite effect ("permanent") on objects that
|
|
260
|
+
# have all left the battlefield no longer does anything and is dropped. `details` may set "duration".
|
|
261
|
+
def add_continuous_effect!(kind, ctx, affected, details)
|
|
262
|
+
state.effects.reject! do |effect| # a phased-out permanent is still there (CR 702.26d, 702.26f)
|
|
263
|
+
effect["duration"] == "permanent" && effect["affected"].none? { state.battlefield.include?(it) || state.phased_out.include?(it) }
|
|
264
|
+
end
|
|
265
|
+
details = details.dup
|
|
266
|
+
duration = details.delete("duration") || "end_of_turn"
|
|
267
|
+
effect = { "kind" => kind, "timestamp" => next_timestamp!, "controller" => ctx.controller,
|
|
268
|
+
"affected" => affected, "duration" => duration }.merge(details)
|
|
269
|
+
state.effects << effect
|
|
270
|
+
emit Events::EffectStarted.new(kind: kind, affected: affected, details: details)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def next_timestamp!
|
|
274
|
+
state.next_id.tap { state.next_id += 1 }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# CR 514.2: damage wears off and "until end of turn" effects end.
|
|
278
|
+
def cleanup_turn!
|
|
279
|
+
expire_permissions! # Engine::Permissions
|
|
280
|
+
end_copy_effects! # Engine::Copies
|
|
281
|
+
state.battlefield.each { remove_damage!(object(it)) unless chars(it).keyword?(:keeps_damage) }
|
|
282
|
+
ending = state.effects.count { it["duration"] == "end_of_turn" }
|
|
283
|
+
return if ending.zero?
|
|
284
|
+
|
|
285
|
+
state.effects.reject! { it["duration"] == "end_of_turn" }
|
|
286
|
+
emit Events::EffectsEnded.new(count: ending)
|
|
287
|
+
end
|
|
288
|
+
# Subtypes that aren't creature types (CR 205.3g-i). "Becomes a Borg artifact creature"
|
|
289
|
+
# replaces creature types only (CR 205.1b).
|
|
290
|
+
# The card type each of them is correlated with (CR 205.1a).
|
|
291
|
+
CORRELATED_SUBTYPES = {
|
|
292
|
+
artifact: %i[attraction blood bobblehead book clue communicator contraption equipment food fortification gold
|
|
293
|
+
heartwood incubator infinity junk lander map mutagen powerstone spacecraft stone treasure vehicle
|
|
294
|
+
vibranium],
|
|
295
|
+
enchantment: %i[aura background cartouche case class curse plan role room rune saga shard shrine],
|
|
296
|
+
land: %i[cave desert forest gate island lair locus mine mountain plains planet power_plant sphere swamp tower town
|
|
297
|
+
urzas]
|
|
298
|
+
}.freeze
|
|
299
|
+
NON_CREATURE_SUBTYPES = CORRELATED_SUBTYPES.values.flatten.freeze
|
|
300
|
+
|
|
301
|
+
# ---- layers 4 and 7b from the object's own state ------------------------------------
|
|
302
|
+
|
|
303
|
+
# Station symbols that apply: "as long as this permanent has N or more charge counters" (CR 722.2a).
|
|
304
|
+
def active_station_symbols(obj)
|
|
305
|
+
charge = obj.counters.fetch("charge", 0)
|
|
306
|
+
defn = catalog.fetch(obj.card)
|
|
307
|
+
defn.ability_instances.select { it.ability.kind == :station_symbol && charge >= it.ability.threshold }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# The abilities the object has from its own card: printed ones plus those of applying station symbols
|
|
311
|
+
# and max speed abilities (Engine::Speed).
|
|
312
|
+
def own_ability_instances(obj)
|
|
313
|
+
printed = catalog.fetch(obj.card).ability_instances
|
|
314
|
+
symbols = active_max_speed(obj, obj.controller) + active_class_levels(obj) # CR 717.2a (Engine::ClassLevels)
|
|
315
|
+
symbols += active_station_symbols(obj) if obj.counters.fetch("charge", 0).positive?
|
|
316
|
+
return printed if symbols.empty?
|
|
317
|
+
|
|
318
|
+
printed + symbols.flat_map { catalog.fetch(obj.card).nested_instances(it.ref) }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# The printed characteristics changed by the object's own station symbols (CR 722.2a-b: abilities,
|
|
322
|
+
# layer 4 "is a creature in addition to its other types", layer 7b base power/toughness) and by
|
|
323
|
+
# assimilation (CR 701.72, 205.1b: an artifact creature whose creature types are replaced by Borg).
|
|
324
|
+
def own_layers(obj, printed, effects)
|
|
325
|
+
symbols = obj.counters.fetch("charge", 0).positive? ? active_station_symbols(obj) : []
|
|
326
|
+
# same shape: keywords and nested abilities (CR 702.178a, 717.2a)
|
|
327
|
+
symbols += active_max_speed(obj, obj.controller) + active_class_levels(obj)
|
|
328
|
+
assimilated = !effects.empty? && effects.any? { it["kind"] == "assimilate" && it["affected"].include?(obj.id) }
|
|
329
|
+
added = effects.empty? ? [] : added_subtypes(obj, effects)
|
|
330
|
+
becomes = effects.empty? ? [] : effects.select { it["kind"] == "add_types" && it["affected"].include?(obj.id) }
|
|
331
|
+
return printed if symbols.empty? && !assimilated && added.empty? && becomes.empty?
|
|
332
|
+
|
|
333
|
+
attrs = printed.to_h
|
|
334
|
+
symbols.each { station_symbol_layers(attrs, it, catalog.fetch(obj.card)) }
|
|
335
|
+
assimilated_layers(attrs) if assimilated
|
|
336
|
+
attrs[:subtypes] = attrs[:subtypes] | added
|
|
337
|
+
becomes.each { added_type_layers(attrs, it) }
|
|
338
|
+
Enjine::Characteristics.new(**attrs.transform_values { it.is_a?(Array) ? it.freeze : it })
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# "Becomes an artifact creature", "becomes a legendary Spider Hero in addition to its other types" (CR 205.1b):
|
|
342
|
+
# card types, supertypes and subtypes are added, none lost. Power/toughness: see Characteristics#with_added_types.
|
|
343
|
+
def added_type_layers(attrs, effect)
|
|
344
|
+
attrs[:types] = attrs[:types] | effect["types"].map(&:to_sym)
|
|
345
|
+
attrs[:supertypes] = attrs[:supertypes] | effect["supertypes"].map(&:to_sym)
|
|
346
|
+
attrs[:subtypes] = attrs[:subtypes] | effect["subtypes"].map(&:to_sym)
|
|
347
|
+
return unless attrs[:types].include?(:creature) && attrs[:power].nil?
|
|
348
|
+
|
|
349
|
+
attrs[:power] = 0
|
|
350
|
+
attrs[:toughness] = 0
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def station_symbol_layers(attrs, inst, defn)
|
|
354
|
+
symbol = inst.ability
|
|
355
|
+
attrs[:abilities] = attrs[:abilities] + defn.nested_instances(inst.ref)
|
|
356
|
+
attrs[:keywords] = attrs[:keywords] | symbol.keywords
|
|
357
|
+
return unless symbol.power
|
|
358
|
+
|
|
359
|
+
attrs[:types] = attrs[:types] | [:creature] # CR 722.2b
|
|
360
|
+
attrs[:power] = symbol.power
|
|
361
|
+
attrs[:toughness] = symbol.toughness
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def assimilated_layers(attrs)
|
|
365
|
+
attrs[:types] = attrs[:types] | %i[artifact creature]
|
|
366
|
+
attrs[:subtypes] = (attrs[:subtypes] & NON_CREATURE_SUBTYPES) | [:borg]
|
|
367
|
+
attrs[:power] ||= 0 # a card without printed P/T that becomes a creature is 0/0 before counters
|
|
368
|
+
attrs[:toughness] ||= 0
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Layer 4 (CR 613.1d, 205.1b): "becomes a [subtype] in addition to its other types" until end of turn.
|
|
372
|
+
def added_subtypes(obj, effects)
|
|
373
|
+
effects.flat_map { it["kind"] == "add_subtypes" && it["affected"].include?(obj.id) ? it["subtypes"].map(&:to_sym) : [] }
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
include Characteristics
|
|
378
|
+
end
|
|
379
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
class Engine
|
|
5
|
+
# Class levels (CR 717.2b): a designation of a permanent, kept in its memory ("class_level"), not copiable,
|
|
6
|
+
# and lost when it leaves the battlefield (CR 400.7). Without one a permanent is level 1 (CR 717.2d).
|
|
7
|
+
module ClassLevels
|
|
8
|
+
NO_LEVELS = [].freeze
|
|
9
|
+
|
|
10
|
+
def class_level(obj) = obj.memory.fetch("class_level", 1)
|
|
11
|
+
|
|
12
|
+
def set_class_level!(obj, level)
|
|
13
|
+
obj.memory["class_level"] = level
|
|
14
|
+
emit Events::ClassLevelChanged.new(object: obj.id, level: level)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The class level bars whose abilities apply: level N or greater (CR 717.2a).
|
|
18
|
+
def active_class_levels(obj)
|
|
19
|
+
level = obj.memory["class_level"] or return NO_LEVELS
|
|
20
|
+
catalog.fetch(obj.card).ability_instances.select { it.ability.is_a?(Mechanics::Abilities::ClassLevel) && level >= it.ability.level }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
include ClassLevels
|
|
24
|
+
end
|
|
25
|
+
end
|