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,216 @@
|
|
|
1
|
+
# Card types
|
|
2
|
+
|
|
3
|
+
<!-- Generated by `rake docs` from the primitive registry. Do not edit. -->
|
|
4
|
+
|
|
5
|
+
## artifact
|
|
6
|
+
|
|
7
|
+
A permanent cast at sorcery speed.
|
|
8
|
+
|
|
9
|
+
Rules: CR 301.1
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"name": "Example Relic",
|
|
14
|
+
"mana_cost": "{2}",
|
|
15
|
+
"types": [
|
|
16
|
+
"artifact"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## creature
|
|
22
|
+
|
|
23
|
+
A permanent with power and toughness that can attack and block. Cast at sorcery speed. It can't attack or use {T} abilities unless controlled continuously since its controller's most recent turn began (summoning sickness).
|
|
24
|
+
|
|
25
|
+
Rules: CR 302.1, CR 302.4, CR 302.6, CR 307.1
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"name": "Grizzly Bears",
|
|
30
|
+
"mana_cost": "{1}{G}",
|
|
31
|
+
"types": [
|
|
32
|
+
"creature"
|
|
33
|
+
],
|
|
34
|
+
"subtypes": [
|
|
35
|
+
"bear"
|
|
36
|
+
],
|
|
37
|
+
"power": 2,
|
|
38
|
+
"toughness": 2
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## enchantment
|
|
43
|
+
|
|
44
|
+
A permanent cast at sorcery speed.
|
|
45
|
+
|
|
46
|
+
Rules: CR 303.1
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"name": "Example Aura",
|
|
51
|
+
"mana_cost": "{1}{W}",
|
|
52
|
+
"types": [
|
|
53
|
+
"enchantment"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## instant
|
|
59
|
+
|
|
60
|
+
A spell cast any time its controller has priority. Its spell ability resolves, then it goes to the graveyard.
|
|
61
|
+
|
|
62
|
+
Rules: CR 304.1, CR 304.2, CR 608.2n
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"name": "Lightning Bolt",
|
|
67
|
+
"mana_cost": "{R}",
|
|
68
|
+
"types": [
|
|
69
|
+
"instant"
|
|
70
|
+
],
|
|
71
|
+
"abilities": [
|
|
72
|
+
{
|
|
73
|
+
"spell": {
|
|
74
|
+
"targets": [
|
|
75
|
+
"any_target"
|
|
76
|
+
],
|
|
77
|
+
"effects": [
|
|
78
|
+
{
|
|
79
|
+
"deal_damage": {
|
|
80
|
+
"amount": 3,
|
|
81
|
+
"to": {
|
|
82
|
+
"target": 0
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## kindred
|
|
94
|
+
|
|
95
|
+
A card type that goes with another one ("Kindred Sorcery — Goblin"): the card is cast and resolves as its other type, and its subtypes are creature types.
|
|
96
|
+
|
|
97
|
+
Rules: CR 308.1, CR 308.2, CR 205.3m
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"name": "Example Goblin Gambit",
|
|
102
|
+
"mana_cost": "{R}",
|
|
103
|
+
"types": [
|
|
104
|
+
"kindred",
|
|
105
|
+
"sorcery"
|
|
106
|
+
],
|
|
107
|
+
"subtypes": [
|
|
108
|
+
"goblin"
|
|
109
|
+
],
|
|
110
|
+
"abilities": [
|
|
111
|
+
{
|
|
112
|
+
"spell": {
|
|
113
|
+
"targets": [
|
|
114
|
+
"any_target"
|
|
115
|
+
],
|
|
116
|
+
"effects": [
|
|
117
|
+
{
|
|
118
|
+
"deal_damage": {
|
|
119
|
+
"amount": 1,
|
|
120
|
+
"to": {
|
|
121
|
+
"target": 0
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## land
|
|
133
|
+
|
|
134
|
+
A permanent played with the special action "play a land" (one per turn, main phase, empty stack). It has no mana cost.
|
|
135
|
+
|
|
136
|
+
Rules: CR 305.1, CR 305.2, CR 305.3, CR 202.1a
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"name": "Forest",
|
|
141
|
+
"types": [
|
|
142
|
+
"land"
|
|
143
|
+
],
|
|
144
|
+
"supertypes": [
|
|
145
|
+
"basic"
|
|
146
|
+
],
|
|
147
|
+
"subtypes": [
|
|
148
|
+
"forest"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## planeswalker
|
|
154
|
+
|
|
155
|
+
A permanent that enters with loyalty counters equal to its printed loyalty. Its loyalty abilities can be activated at sorcery speed, one per turn. Damage dealt to it removes loyalty counters; at 0 loyalty it is put into its owner's graveyard.
|
|
156
|
+
|
|
157
|
+
Rules: CR 306.5b, CR 306.8, CR 120.3c, CR 606.3, CR 704.5i
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"name": "Vessa, Rally Captain",
|
|
162
|
+
"mana_cost": "{2}{W}{W}",
|
|
163
|
+
"types": [
|
|
164
|
+
"planeswalker"
|
|
165
|
+
],
|
|
166
|
+
"loyalty": 3,
|
|
167
|
+
"abilities": [
|
|
168
|
+
{
|
|
169
|
+
"loyalty": {
|
|
170
|
+
"cost": 1,
|
|
171
|
+
"effects": [
|
|
172
|
+
{
|
|
173
|
+
"pump": {
|
|
174
|
+
"power": 1,
|
|
175
|
+
"toughness": 1,
|
|
176
|
+
"to": {
|
|
177
|
+
"each": "creature",
|
|
178
|
+
"controller": "you"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## sorcery
|
|
190
|
+
|
|
191
|
+
A spell cast only in its controller's main phase while the stack is empty.
|
|
192
|
+
|
|
193
|
+
Rules: CR 307.1, CR 307.2, CR 608.2n
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"name": "Divination",
|
|
198
|
+
"mana_cost": "{2}{U}",
|
|
199
|
+
"types": [
|
|
200
|
+
"sorcery"
|
|
201
|
+
],
|
|
202
|
+
"abilities": [
|
|
203
|
+
{
|
|
204
|
+
"spell": {
|
|
205
|
+
"effects": [
|
|
206
|
+
{
|
|
207
|
+
"draw": {
|
|
208
|
+
"amount": 2
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
```
|
data/exe/enjine
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Actions
|
|
5
|
+
# CR 605.3a: a player may activate a mana ability whenever they have priority.
|
|
6
|
+
# It resolves immediately (CR 605.3b). Mana is always tapped manually, one source at a time.
|
|
7
|
+
class ActivateManaAbility
|
|
8
|
+
def self.doc
|
|
9
|
+
'`{type: "activate_mana", object: id, ability: ref}` — activate a mana ability; it resolves immediately ' \
|
|
10
|
+
'(CR 605.3). `mana: "{G}{G}"` names the mana when it is chosen as the ability is activated.'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.options(engine, index)
|
|
14
|
+
engine.mana_activation_options(engine.all_mana_sources(index))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.perform(engine, index, option)
|
|
18
|
+
engine.activate_mana_ability!(index, option[:object], option[:ability], option[:mana])
|
|
19
|
+
engine.action_taken!(index)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
register("activate_mana", ActivateManaAbility)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Actions
|
|
5
|
+
# CR 601: cast a spell from hand. Instants and flash cards at any priority; others at sorcery speed.
|
|
6
|
+
# A card in another zone can be cast when a rule or effect allows it (CR 601.3): `via` names how.
|
|
7
|
+
class Cast
|
|
8
|
+
def self.doc
|
|
9
|
+
'`{type: "cast", object: id}` — start casting a card from your hand (CR 601.2). ' \
|
|
10
|
+
'`{type: "cast", object: id, via: method}` casts a card from another zone (graveyard, exile, the top ' \
|
|
11
|
+
"of your library) by the permission or alternative cost `via` names, e.g. beam me up (CR 601.3, " \
|
|
12
|
+
"702.196). A card outside the game is named `\"outside:<card id>\"` (CR 400.11b). " \
|
|
13
|
+
'`{type: "cast", object: id, face: 1}` casts another face of a card in your hand: a modal double-faced ' \
|
|
14
|
+
"card's back face, an Adventure or an Omen (CR 712.8b, 716.3, 721.3). Offered only when timing " \
|
|
15
|
+
"allows, targets exist and enough mana is available."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.options(engine, index)
|
|
19
|
+
engine.player(index).hand.flat_map do |id|
|
|
20
|
+
(engine.castable?(index, id) ? [{ type: "cast", object: id }] : []) + engine.face_cast_options(index, id)
|
|
21
|
+
end + engine.permission_cast_options(index)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.perform(engine, index, option) = engine.begin_cast!(index, option[:object], via: option[:via], face: option[:face])
|
|
25
|
+
end
|
|
26
|
+
register("cast", Cast)
|
|
27
|
+
|
|
28
|
+
# CR 602 / 606: activate a non-mana activated or loyalty ability of a permanent you control,
|
|
29
|
+
# or an ability that functions from your hand, such as cycling (CR 113.6m, 702.29a).
|
|
30
|
+
class Activate
|
|
31
|
+
def self.doc
|
|
32
|
+
'`{type: "activate", object: id, ability: ref}` — start activating an ability (CR 602.2), ' \
|
|
33
|
+
"including planeswalker loyalty abilities (CR 606) and abilities of cards in your hand (cycling, CR 702.29)."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.options(engine, index)
|
|
37
|
+
out = []
|
|
38
|
+
engine.state.battlefield.each do |id|
|
|
39
|
+
add_options(engine, index, id, out) if engine.object(id).controller == index
|
|
40
|
+
end
|
|
41
|
+
engine.player(index).hand.each { add_options(engine, index, it, out) } # CR 602.2: the owner activates
|
|
42
|
+
engine.player(index).graveyard.each { add_options(engine, index, it, out) } # CR 113.6m: e.g. "exile this card from your graveyard"
|
|
43
|
+
engine.state.exile.each { add_options(engine, index, it, out) if engine.object(it).owner == index } # CR 113.6b
|
|
44
|
+
out
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.add_options(engine, index, id, out)
|
|
48
|
+
engine.chars(id).activated_abilities.each do |inst|
|
|
49
|
+
out << { type: "activate", object: id, ability: inst.ref } if engine.activatable?(index, id, inst.ref)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.perform(engine, index, option) = engine.begin_activation!(index, option[:object], option[:ability])
|
|
54
|
+
end
|
|
55
|
+
register("activate", Activate)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Actions
|
|
5
|
+
# CR 117.3d: pass priority.
|
|
6
|
+
class Pass
|
|
7
|
+
def self.doc
|
|
8
|
+
'`{type: "pass"}` — pass priority. When all players pass in succession with an empty stack, the step ends (CR 117.4).'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
OPTION = { type: "pass" }.freeze
|
|
12
|
+
|
|
13
|
+
def self.options(_engine, _index) = [OPTION]
|
|
14
|
+
def self.perform(engine, index, _option) = engine.pass_priority!(index)
|
|
15
|
+
end
|
|
16
|
+
register("pass", Pass)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Actions
|
|
5
|
+
# CR 305.1-305.2, 116.2a: playing a land is a special action — own main phase,
|
|
6
|
+
# empty stack, while holding priority, one land per turn plus any additional land plays
|
|
7
|
+
# (Engine#land_plays_allowed).
|
|
8
|
+
# A land outside the hand (the top of the library, exile) can be played when an effect allows it;
|
|
9
|
+
# `via` names the permission (CR 305.1: "from the zone it was in"); a land outside the game is named by
|
|
10
|
+
# its outside key (Engine::Permissions#outside_key) and brought in as it's played (CR 400.11b).
|
|
11
|
+
class PlayLand
|
|
12
|
+
def self.doc
|
|
13
|
+
'`{type: "play_land", object: id}` — special action: your main phase, empty stack, one land per turn ' \
|
|
14
|
+
"plus additional land plays (CR 305.1, 305.2). With `via`: a land from another zone that an effect lets you play. " \
|
|
15
|
+
"With `face: 1`: a modal double-faced card played with its land back face up (CR 712.9)."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.options(engine, index)
|
|
19
|
+
return [] unless engine.sorcery_timing?(index) && engine.land_play_available?(index)
|
|
20
|
+
|
|
21
|
+
engine.player(index).hand.flat_map do |id|
|
|
22
|
+
(engine.chars(id).land? ? [{ type: "play_land", object: id }] : []) + engine.face_land_options(id)
|
|
23
|
+
end + engine.permission_land_options(index)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.perform(engine, index, option)
|
|
27
|
+
engine.use_play_method!(engine.play_method(index, option[:object], option[:via])) if option[:via]
|
|
28
|
+
engine.player(index).lands_played += 1
|
|
29
|
+
face = option[:face] && { "face" => option[:face] } # CR 712.9
|
|
30
|
+
if !face && engine.entering_choice(engine.candidate_object(index, option[:object]))
|
|
31
|
+
engine.state.frames << { "type" => "play_land", "object" => option[:object], "player" => index }
|
|
32
|
+
return engine.ask!(nil)
|
|
33
|
+
end
|
|
34
|
+
engine.put_played_land!(index, option[:object], enters: face)
|
|
35
|
+
engine.action_taken!(index) # CR 117.3c: the player receives priority again
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The play_land frame: ask the "as this enters" choice, then put the land onto the battlefield.
|
|
39
|
+
def self.step(engine, frame)
|
|
40
|
+
land = engine.candidate_object(frame["player"], frame["object"])
|
|
41
|
+
return engine.ask_enter_choice!(frame["object"], frame["player"]) if engine.entering_choice_pending?(land, frame["enter"])
|
|
42
|
+
|
|
43
|
+
enters = engine.enters_from_answer(land, frame)
|
|
44
|
+
engine.pay_entering_costs!(frame["player"], frame)
|
|
45
|
+
engine.put_played_land!(frame["player"], frame["object"], enters: enters)
|
|
46
|
+
engine.action_taken!(frame["player"])
|
|
47
|
+
:done
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
register("play_land", PlayLand)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Actions
|
|
5
|
+
# CR 116.2f, 116.2h: special actions that pay a cost and exile a card from your hand (foretell, suspend).
|
|
6
|
+
# The card's ability names the cost, when the action is allowed and what it does; the cost is paid in a
|
|
7
|
+
# "pay" frame (Engine::Payment#begin_payment!), which can be cancelled, and the player gets priority
|
|
8
|
+
# again afterwards (CR 116.3).
|
|
9
|
+
class SpecialAction
|
|
10
|
+
def self.doc
|
|
11
|
+
'`{type: "special_action", object: id, ability: ref}` — a special action of a card in your hand: foretell ' \
|
|
12
|
+
"(during your turn, pay {2} and exile it face down, CR 116.2h) or suspend (when you could begin to cast it, " \
|
|
13
|
+
"pay its suspend cost and exile it with time counters, CR 116.2f)."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.options(engine, index)
|
|
17
|
+
engine.player(index).hand.flat_map do |id|
|
|
18
|
+
obj = engine.object(id)
|
|
19
|
+
engine.chars(obj).abilities.filter_map do |inst|
|
|
20
|
+
ability = inst.ability
|
|
21
|
+
next unless ability.respond_to?(:special!) && ability.special_allowed?(engine, obj, index)
|
|
22
|
+
next unless engine.affordable?(index, ability.special_cost, nil)
|
|
23
|
+
|
|
24
|
+
{ type: "special_action", object: id, ability: inst.ref }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.perform(engine, index, option)
|
|
30
|
+
ability = engine.chars(option[:object]).ability(option[:ability])
|
|
31
|
+
engine.begin_payment!(index, ability.special_cost, option[:object],
|
|
32
|
+
special: { "object" => option[:object], "ability" => option[:ability] })
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
register("special_action", SpecialAction)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
# Actions a player can take while holding priority. Each action class has:
|
|
5
|
+
# .options(engine, player_index) -> Array of option Hashes
|
|
6
|
+
# .perform(engine, player_index, option)
|
|
7
|
+
# Adding a new kind of action (cast a spell, activate an ability, ...) means adding
|
|
8
|
+
# one class and registering it here.
|
|
9
|
+
module Actions
|
|
10
|
+
PRIORITY = [] # rubocop:disable Style/MutableConstant -- extension registry
|
|
11
|
+
BY_TYPE = {} # rubocop:disable Style/MutableConstant -- extension registry
|
|
12
|
+
|
|
13
|
+
def self.register(type, klass)
|
|
14
|
+
PRIORITY << klass
|
|
15
|
+
BY_TYPE[type] = klass
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.fetch(type) = BY_TYPE.fetch(type)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Agents
|
|
5
|
+
# A random agent that would rather do something than pass: with priority it plays a land when it can,
|
|
6
|
+
# and otherwise casts a spell or activates an ability `eagerness` percent of the time. Every other
|
|
7
|
+
# decision (targets, payments, attacks, blocks) is uniform, as for Agents::Random. It still passes now
|
|
8
|
+
# and then, so a free repeatable ability can't hold a game forever. Card checks use it so cards get used.
|
|
9
|
+
class Busy < Random
|
|
10
|
+
ACTIONS = %w[play_land cast activate special_action].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(seed: 0, cancel_rate: 0.02, eagerness: 85)
|
|
13
|
+
super(seed: seed, cancel_rate: cancel_rate)
|
|
14
|
+
@eagerness = eagerness
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def choose(decision)
|
|
18
|
+
actions = decision.options.select { ACTIONS.include?(it[:type]) }
|
|
19
|
+
return super if actions.empty? || @rng.rand(100) >= @eagerness
|
|
20
|
+
|
|
21
|
+
lands = actions.select { it[:type] == "play_land" }
|
|
22
|
+
pick = lands.empty? ? actions : lands
|
|
23
|
+
pick[@rng.rand(pick.size)]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Agents
|
|
5
|
+
# Picks uniformly among the legal options. No strategy: a fuzzing driver and CLI seat.
|
|
6
|
+
#
|
|
7
|
+
# `cancel_rate` is the chance of choosing "cancel" when other options exist. With a
|
|
8
|
+
# uniform pick, a 4-mana spell (about 8 payment decisions, each offering cancel)
|
|
9
|
+
# would almost never finish casting, so expensive cards and planeswalkers would
|
|
10
|
+
# go untested. The default keeps cancellation exercised but rare.
|
|
11
|
+
class Random
|
|
12
|
+
CANCEL = { type: "cancel" }.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(seed: 0, cancel_rate: 0.02)
|
|
15
|
+
@rng = Rng.from_seed(seed)
|
|
16
|
+
@cancel_threshold = (cancel_rate * 1_000_000).round
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def choose(decision)
|
|
20
|
+
options = decision.options
|
|
21
|
+
others = options.include?(CANCEL) && options.size > 1 ? options - [CANCEL] : options
|
|
22
|
+
return CANCEL if others.size < options.size && @rng.rand(1_000_000) < @cancel_threshold
|
|
23
|
+
|
|
24
|
+
others[@rng.rand(others.size)]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|