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.
Files changed (269) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +201 -0
  5. data/SUPPORTED.md +387 -0
  6. data/docs/ARCHITECTURE.md +141 -0
  7. data/docs/DESIGN.md +139 -0
  8. data/docs/MECHANICS_COVERAGE.md +670 -0
  9. data/docs/guides/adding-a-mechanic.md +60 -0
  10. data/docs/guides/api.md +614 -0
  11. data/docs/guides/card-data-format.md +1472 -0
  12. data/docs/guides/deck-builder.md +157 -0
  13. data/docs/plans/standard.md +371 -0
  14. data/docs/reference/abilities.md +1680 -0
  15. data/docs/reference/costs.md +339 -0
  16. data/docs/reference/counters.md +121 -0
  17. data/docs/reference/decisions.md +102 -0
  18. data/docs/reference/effects.md +2340 -0
  19. data/docs/reference/filters.md +172 -0
  20. data/docs/reference/keywords.md +647 -0
  21. data/docs/reference/subtypes.md +169 -0
  22. data/docs/reference/supertypes.md +31 -0
  23. data/docs/reference/triggers.md +797 -0
  24. data/docs/reference/types.md +216 -0
  25. data/exe/enjine +7 -0
  26. data/lib/enjine/actions/activate_mana_ability.rb +24 -0
  27. data/lib/enjine/actions/cast.rb +57 -0
  28. data/lib/enjine/actions/pass.rb +18 -0
  29. data/lib/enjine/actions/play_land.rb +52 -0
  30. data/lib/enjine/actions/special_action.rb +37 -0
  31. data/lib/enjine/actions.rb +20 -0
  32. data/lib/enjine/agents/busy.rb +27 -0
  33. data/lib/enjine/agents/random.rb +28 -0
  34. data/lib/enjine/card_definition.rb +433 -0
  35. data/lib/enjine/card_faces.rb +116 -0
  36. data/lib/enjine/catalog.rb +56 -0
  37. data/lib/enjine/characteristics.rb +39 -0
  38. data/lib/enjine/cli/text.rb +295 -0
  39. data/lib/enjine/cli.rb +312 -0
  40. data/lib/enjine/decisions/bottom.rb +29 -0
  41. data/lib/enjine/decisions/casting.rb +188 -0
  42. data/lib/enjine/decisions/combat.rb +75 -0
  43. data/lib/enjine/decisions/discard.rb +29 -0
  44. data/lib/enjine/decisions/effect_choice.rb +21 -0
  45. data/lib/enjine/decisions/enter_choice.rb +26 -0
  46. data/lib/enjine/decisions/legend_rule.rb +21 -0
  47. data/lib/enjine/decisions/mulligan.rb +26 -0
  48. data/lib/enjine/decisions/order_triggers.rb +21 -0
  49. data/lib/enjine/decisions/play_or_draw.rb +19 -0
  50. data/lib/enjine/decisions/priority.rb +22 -0
  51. data/lib/enjine/decisions/untap_choice.rb +35 -0
  52. data/lib/enjine/decisions.rb +22 -0
  53. data/lib/enjine/deck.rb +151 -0
  54. data/lib/enjine/deck_builder.rb +454 -0
  55. data/lib/enjine/docs.rb +90 -0
  56. data/lib/enjine/engine/ability_layers.rb +118 -0
  57. data/lib/enjine/engine/attachments.rb +86 -0
  58. data/lib/enjine/engine/casting/alt_casting.rb +254 -0
  59. data/lib/enjine/engine/casting/cast_choices.rb +292 -0
  60. data/lib/enjine/engine/casting/casting.rb +326 -0
  61. data/lib/enjine/engine/casting/cost_options.rb +99 -0
  62. data/lib/enjine/engine/casting/costs.rb +117 -0
  63. data/lib/enjine/engine/casting/payment.rb +353 -0
  64. data/lib/enjine/engine/casting/permissions.rb +412 -0
  65. data/lib/enjine/engine/casting/target_counts.rb +113 -0
  66. data/lib/enjine/engine/casting/waterbend.rb +90 -0
  67. data/lib/enjine/engine/characteristics.rb +379 -0
  68. data/lib/enjine/engine/class_levels.rb +25 -0
  69. data/lib/enjine/engine/combat.rb +315 -0
  70. data/lib/enjine/engine/copies.rb +208 -0
  71. data/lib/enjine/engine/damage.rb +196 -0
  72. data/lib/enjine/engine/designations.rb +46 -0
  73. data/lib/enjine/engine/emblems.rb +37 -0
  74. data/lib/enjine/engine/entering.rb +156 -0
  75. data/lib/enjine/engine/face_down.rb +30 -0
  76. data/lib/enjine/engine/faces.rb +222 -0
  77. data/lib/enjine/engine/follow_ups.rb +27 -0
  78. data/lib/enjine/engine/mana_sources.rb +230 -0
  79. data/lib/enjine/engine/phasing.rb +74 -0
  80. data/lib/enjine/engine/pregame.rb +95 -0
  81. data/lib/enjine/engine/priority.rb +47 -0
  82. data/lib/enjine/engine/resolution.rb +168 -0
  83. data/lib/enjine/engine/rules.rb +287 -0
  84. data/lib/enjine/engine/sagas.rb +41 -0
  85. data/lib/enjine/engine/speed.rb +104 -0
  86. data/lib/enjine/engine/spell_grants.rb +46 -0
  87. data/lib/enjine/engine/state_based_actions.rb +157 -0
  88. data/lib/enjine/engine/triggers.rb +364 -0
  89. data/lib/enjine/engine/turn_history.rb +240 -0
  90. data/lib/enjine/engine/turn_structure.rb +224 -0
  91. data/lib/enjine/engine/zones.rb +278 -0
  92. data/lib/enjine/engine.rb +55 -0
  93. data/lib/enjine/errors.rb +62 -0
  94. data/lib/enjine/events.rb +161 -0
  95. data/lib/enjine/frames.rb +84 -0
  96. data/lib/enjine/fuzz.rb +112 -0
  97. data/lib/enjine/game.rb +204 -0
  98. data/lib/enjine/history.rb +26 -0
  99. data/lib/enjine/mana.rb +142 -0
  100. data/lib/enjine/mechanics/abilities/additional_triggers.rb +86 -0
  101. data/lib/enjine/mechanics/abilities/chosen_values.rb +213 -0
  102. data/lib/enjine/mechanics/abilities/cost_changes.rb +154 -0
  103. data/lib/enjine/mechanics/abilities/damage_replacements.rb +569 -0
  104. data/lib/enjine/mechanics/abilities/double_counters.rb +71 -0
  105. data/lib/enjine/mechanics/abilities/entering_choices.rb +91 -0
  106. data/lib/enjine/mechanics/abilities/entering_copy.rb +69 -0
  107. data/lib/enjine/mechanics/abilities/enters_tapped.rb +159 -0
  108. data/lib/enjine/mechanics/abilities/enters_with_counters.rb +116 -0
  109. data/lib/enjine/mechanics/abilities/mana_ability.rb +209 -0
  110. data/lib/enjine/mechanics/abilities/play_permissions.rb +527 -0
  111. data/lib/enjine/mechanics/abilities/player_rules.rb +169 -0
  112. data/lib/enjine/mechanics/abilities/replacement_effects.rb +368 -0
  113. data/lib/enjine/mechanics/abilities/restrictions.rb +144 -0
  114. data/lib/enjine/mechanics/abilities/spell_keywords.rb +55 -0
  115. data/lib/enjine/mechanics/abilities/stack_abilities.rb +542 -0
  116. data/lib/enjine/mechanics/abilities/static.rb +365 -0
  117. data/lib/enjine/mechanics/abilities/tapping_power.rb +71 -0
  118. data/lib/enjine/mechanics/abilities/turn_rules.rb +49 -0
  119. data/lib/enjine/mechanics/card_spec.rb +177 -0
  120. data/lib/enjine/mechanics/copy_except.rb +73 -0
  121. data/lib/enjine/mechanics/costs/base.rb +115 -0
  122. data/lib/enjine/mechanics/costs/behold.rb +76 -0
  123. data/lib/enjine/mechanics/costs/discard.rb +65 -0
  124. data/lib/enjine/mechanics/costs/exert.rb +29 -0
  125. data/lib/enjine/mechanics/costs/exile_cards.rb +39 -0
  126. data/lib/enjine/mechanics/costs/from_graveyard.rb +45 -0
  127. data/lib/enjine/mechanics/costs/get_counters.rb +39 -0
  128. data/lib/enjine/mechanics/costs/mana.rb +37 -0
  129. data/lib/enjine/mechanics/costs/one_of.rb +49 -0
  130. data/lib/enjine/mechanics/costs/pay_life.rb +50 -0
  131. data/lib/enjine/mechanics/costs/remove_counters.rb +51 -0
  132. data/lib/enjine/mechanics/costs/sacrifice.rb +59 -0
  133. data/lib/enjine/mechanics/costs/tap.rb +56 -0
  134. data/lib/enjine/mechanics/costs/waterbend.rb +29 -0
  135. data/lib/enjine/mechanics/counters/kinds.rb +190 -0
  136. data/lib/enjine/mechanics/effects/add_subtypes.rb +30 -0
  137. data/lib/enjine/mechanics/effects/airbend.rb +41 -0
  138. data/lib/enjine/mechanics/effects/amass.rb +124 -0
  139. data/lib/enjine/mechanics/effects/assimilate.rb +72 -0
  140. data/lib/enjine/mechanics/effects/base.rb +148 -0
  141. data/lib/enjine/mechanics/effects/become.rb +182 -0
  142. data/lib/enjine/mechanics/effects/behold.rb +48 -0
  143. data/lib/enjine/mechanics/effects/blight.rb +94 -0
  144. data/lib/enjine/mechanics/effects/choose_cards.rb +168 -0
  145. data/lib/enjine/mechanics/effects/copy.rb +370 -0
  146. data/lib/enjine/mechanics/effects/counter_spell.rb +21 -0
  147. data/lib/enjine/mechanics/effects/counters.rb +161 -0
  148. data/lib/enjine/mechanics/effects/damage.rb +72 -0
  149. data/lib/enjine/mechanics/effects/delayed_triggers.rb +112 -0
  150. data/lib/enjine/mechanics/effects/destroy.rb +22 -0
  151. data/lib/enjine/mechanics/effects/discard.rb +86 -0
  152. data/lib/enjine/mechanics/effects/draw.rb +16 -0
  153. data/lib/enjine/mechanics/effects/earthbend.rb +70 -0
  154. data/lib/enjine/mechanics/effects/emblem.rb +52 -0
  155. data/lib/enjine/mechanics/effects/endure.rb +63 -0
  156. data/lib/enjine/mechanics/effects/exile.rb +49 -0
  157. data/lib/enjine/mechanics/effects/extra_turn.rb +122 -0
  158. data/lib/enjine/mechanics/effects/faces.rb +115 -0
  159. data/lib/enjine/mechanics/effects/fight.rb +76 -0
  160. data/lib/enjine/mechanics/effects/flip_coins.rb +97 -0
  161. data/lib/enjine/mechanics/effects/gain_control.rb +94 -0
  162. data/lib/enjine/mechanics/effects/game_end.rb +42 -0
  163. data/lib/enjine/mechanics/effects/library.rb +327 -0
  164. data/lib/enjine/mechanics/effects/life.rb +83 -0
  165. data/lib/enjine/mechanics/effects/look_at_hand.rb +17 -0
  166. data/lib/enjine/mechanics/effects/loot.rb +118 -0
  167. data/lib/enjine/mechanics/effects/mana.rb +96 -0
  168. data/lib/enjine/mechanics/effects/may.rb +82 -0
  169. data/lib/enjine/mechanics/effects/meld.rb +77 -0
  170. data/lib/enjine/mechanics/effects/mill.rb +22 -0
  171. data/lib/enjine/mechanics/effects/pay.rb +231 -0
  172. data/lib/enjine/mechanics/effects/phasing.rb +51 -0
  173. data/lib/enjine/mechanics/effects/proliferate.rb +72 -0
  174. data/lib/enjine/mechanics/effects/pump.rb +60 -0
  175. data/lib/enjine/mechanics/effects/put_in_library.rb +98 -0
  176. data/lib/enjine/mechanics/effects/return_choice.rb +29 -0
  177. data/lib/enjine/mechanics/effects/return_to_battlefield.rb +80 -0
  178. data/lib/enjine/mechanics/effects/return_to_hand.rb +20 -0
  179. data/lib/enjine/mechanics/effects/roll_dice.rb +45 -0
  180. data/lib/enjine/mechanics/effects/sacrifice.rb +118 -0
  181. data/lib/enjine/mechanics/effects/scry_and_surveil.rb +74 -0
  182. data/lib/enjine/mechanics/effects/search.rb +156 -0
  183. data/lib/enjine/mechanics/effects/tap_untap.rb +23 -0
  184. data/lib/enjine/mechanics/effects/tokens.rb +76 -0
  185. data/lib/enjine/mechanics/filters/any_target.rb +15 -0
  186. data/lib/enjine/mechanics/filters/base.rb +386 -0
  187. data/lib/enjine/mechanics/filters/graveyard.rb +23 -0
  188. data/lib/enjine/mechanics/filters/one_of.rb +55 -0
  189. data/lib/enjine/mechanics/filters/permanents.rb +52 -0
  190. data/lib/enjine/mechanics/filters/players.rb +20 -0
  191. data/lib/enjine/mechanics/filters/stack.rb +97 -0
  192. data/lib/enjine/mechanics/follow_up.rb +46 -0
  193. data/lib/enjine/mechanics/keywords/alternative_costs.rb +361 -0
  194. data/lib/enjine/mechanics/keywords/beam_me_up.rb +78 -0
  195. data/lib/enjine/mechanics/keywords/changeling.rb +58 -0
  196. data/lib/enjine/mechanics/keywords/convoke.rb +77 -0
  197. data/lib/enjine/mechanics/keywords/crew.rb +155 -0
  198. data/lib/enjine/mechanics/keywords/cycling.rb +58 -0
  199. data/lib/enjine/mechanics/keywords/decayed.rb +25 -0
  200. data/lib/enjine/mechanics/keywords/devour.rb +61 -0
  201. data/lib/enjine/mechanics/keywords/evergreen.rb +54 -0
  202. data/lib/enjine/mechanics/keywords/extort.rb +26 -0
  203. data/lib/enjine/mechanics/keywords/flags.rb +46 -0
  204. data/lib/enjine/mechanics/keywords/foretell.rb +45 -0
  205. data/lib/enjine/mechanics/keywords/gift.rb +50 -0
  206. data/lib/enjine/mechanics/keywords/hideaway.rb +28 -0
  207. data/lib/enjine/mechanics/keywords/increment.rb +31 -0
  208. data/lib/enjine/mechanics/keywords/job_select.rb +29 -0
  209. data/lib/enjine/mechanics/keywords/kicker.rb +92 -0
  210. data/lib/enjine/mechanics/keywords/mobilize_and_firebending.rb +84 -0
  211. data/lib/enjine/mechanics/keywords/paradigm.rb +91 -0
  212. data/lib/enjine/mechanics/keywords/power_up.rb +48 -0
  213. data/lib/enjine/mechanics/keywords/protection.rb +111 -0
  214. data/lib/enjine/mechanics/keywords/prowess.rb +30 -0
  215. data/lib/enjine/mechanics/keywords/riot.rb +35 -0
  216. data/lib/enjine/mechanics/keywords/saddle.rb +115 -0
  217. data/lib/enjine/mechanics/keywords/speed.rb +89 -0
  218. data/lib/enjine/mechanics/keywords/station.rb +163 -0
  219. data/lib/enjine/mechanics/keywords/storied.rb +41 -0
  220. data/lib/enjine/mechanics/keywords/storm_and_cascade.rb +86 -0
  221. data/lib/enjine/mechanics/keywords/sunburst.rb +29 -0
  222. data/lib/enjine/mechanics/keywords/suspend.rb +191 -0
  223. data/lib/enjine/mechanics/keywords/teamwork.rb +45 -0
  224. data/lib/enjine/mechanics/keywords/ward.rb +67 -0
  225. data/lib/enjine/mechanics/linked.rb +67 -0
  226. data/lib/enjine/mechanics/mana_spent.rb +99 -0
  227. data/lib/enjine/mechanics/selector.rb +207 -0
  228. data/lib/enjine/mechanics/subtypes/auras_and_equipment.rb +176 -0
  229. data/lib/enjine/mechanics/subtypes/basic_land_types.rb +36 -0
  230. data/lib/enjine/mechanics/subtypes/class_levels.rb +102 -0
  231. data/lib/enjine/mechanics/subtypes/saga.rb +89 -0
  232. data/lib/enjine/mechanics/supertypes/basic.rb +17 -0
  233. data/lib/enjine/mechanics/supertypes/legendary.rb +18 -0
  234. data/lib/enjine/mechanics/triggers/ability_triggers.rb +51 -0
  235. data/lib/enjine/mechanics/triggers/any_of.rb +43 -0
  236. data/lib/enjine/mechanics/triggers/base.rb +228 -0
  237. data/lib/enjine/mechanics/triggers/casting.rb +219 -0
  238. data/lib/enjine/mechanics/triggers/combat.rb +123 -0
  239. data/lib/enjine/mechanics/triggers/damage.rb +130 -0
  240. data/lib/enjine/mechanics/triggers/dilemma.rb +37 -0
  241. data/lib/enjine/mechanics/triggers/draw_and_discard.rb +85 -0
  242. data/lib/enjine/mechanics/triggers/keyword_actions.rb +55 -0
  243. data/lib/enjine/mechanics/triggers/life.rb +35 -0
  244. data/lib/enjine/mechanics/triggers/steps.rb +39 -0
  245. data/lib/enjine/mechanics/triggers/tap_and_counters.rb +111 -0
  246. data/lib/enjine/mechanics/triggers/zone_changes.rb +249 -0
  247. data/lib/enjine/mechanics/types/kindred.rb +30 -0
  248. data/lib/enjine/mechanics/types/land.rb +24 -0
  249. data/lib/enjine/mechanics/types/permanent_types.rb +67 -0
  250. data/lib/enjine/mechanics/types/spell_types.rb +38 -0
  251. data/lib/enjine/mechanics/values.rb +675 -0
  252. data/lib/enjine/mechanics.rb +74 -0
  253. data/lib/enjine/player.rb +30 -0
  254. data/lib/enjine/primitive.rb +61 -0
  255. data/lib/enjine/replacements.rb +191 -0
  256. data/lib/enjine/rng.rb +57 -0
  257. data/lib/enjine/rules_monitor.rb +404 -0
  258. data/lib/enjine/setup.rb +45 -0
  259. data/lib/enjine/snapshot/player_checks.rb +86 -0
  260. data/lib/enjine/snapshot/turn_checks.rb +91 -0
  261. data/lib/enjine/snapshot.rb +307 -0
  262. data/lib/enjine/state.rb +173 -0
  263. data/lib/enjine/steps.rb +117 -0
  264. data/lib/enjine/tokens.rb +25 -0
  265. data/lib/enjine/verify.rb +233 -0
  266. data/lib/enjine/version.rb +7 -0
  267. data/lib/enjine/view.rb +162 -0
  268. data/lib/enjine.rb +46 -0
  269. 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,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+ require "enjine/cli"
6
+
7
+ exit Enjine::CLI.new(ARGV).run
@@ -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