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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 265a3b07d3414d02b42954fde369269737f0dfa6b24ab7f80e3edfc59a6bf918
4
+ data.tar.gz: '0846a35e1e3830a04a375a8b21ebca22a86f039dcf54dcff3f657db584fa9fc9'
5
+ SHA512:
6
+ metadata.gz: 939b4f7e9dedfb39d6a2ac88278a7d431b54c1934a799812a9d82fa27b05adfa8f9995cb6758658389c81bd4fee06d2d1a4f11983e4eaf4cbcbba71da4df9078
7
+ data.tar.gz: 327d05af97cc9614d1c7348270e8227f7afe4f832fdfcb6b3638df6747e6ac7e3743657568926ba4d2ea4d10c39827f2c75f6de16403d7c6a3899854db729fc4
data/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/). Before 1.0, minor versions may
6
+ change the public API; the stable surface is described in
7
+ [docs/guides/api.md](docs/guides/api.md).
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [0.1.0] - 2026-11-11
12
+
13
+ ### Added
14
+ - Two-player game engine pinned to the 2026-11-06 Comprehensive Rules: turn
15
+ structure, priority, the stack, casting with manual mana payment, combat,
16
+ state-based actions, layers, replacement and prevention effects, copies,
17
+ London mulligan.
18
+ - Atomic decision API (`Game#pending_decision`, `submit`, `apply`), per-player
19
+ views and histories, export/import of any moment, `fork`, deterministic replay.
20
+ - Card DSL: cards as JSON data, validated with path-addressed errors; the
21
+ mechanics of the Standard-legal sets (Foundations to Star Trek).
22
+ - `Enjine::DeckBuilder` (random decks from a card pool), `Enjine::RulesMonitor`,
23
+ `Enjine::Fuzz` and `Enjine::Verify`.
24
+ - `enjine` CLI: hot-seat and random play, `--validate`, `--verify`,
25
+ `--random-deck`, save and load.
26
+
27
+ [Unreleased]: https://github.com/thisiscetin/enjine/compare/v0.1.0...HEAD
28
+ [0.1.0]: https://github.com/thisiscetin/enjine/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Mehmet Cetin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # Enjine
2
+
3
+ **A deterministic, rules-accurate Magic: The Gathering engine for Ruby, built for the Standard format.**
4
+
5
+ Enjine runs two-player Magic: The Gathering games by the
6
+ [Comprehensive Rules](https://magic.wizards.com/en/rules) (pinned to the version
7
+ effective **2026-11-06**). It turns every moment of the game into an explicit,
8
+ atomic decision with a list of legal options. That makes it a solid base for
9
+ simulations, bots and reinforcement learning, or for a game client.
10
+
11
+ **Scope: Standard.** Enjine implements the core rules and the mechanics of the
12
+ current Standard-legal sets. Mechanics found only outside Standard (morph,
13
+ madness, infect, ...) are out of scope until a Standard card needs them. The
14
+ [mechanics coverage](docs/MECHANICS_COVERAGE.md) page has the details and the
15
+ numbers.
16
+
17
+ - **Strict rules.** Turn structure, priority, the stack, layers, replacement
18
+ effects, combat, state-based actions, London mulligan. Code and tests cite rule
19
+ numbers.
20
+ - **Cards are data.** You write each card as a JSON block in the
21
+ [card DSL](docs/guides/card-data-format.md); Enjine validates it and never
22
+ reads card text. Unknown mechanics are rejected with path-addressed errors.
23
+ - **Atomic decisions.** One small choice at a time (cast → target → tap → pay),
24
+ each one cancellable until the costs are paid.
25
+ - **Deterministic.** Seeded PCG32 RNG: the same seed and decisions give the same
26
+ game, bit for bit. Export or import any moment, even mid-cast.
27
+ - **Hidden information handled properly.** Per-player views and histories, plus an
28
+ omniscient observer view.
29
+ - **No runtime dependencies.** Plain Ruby 4.0.
30
+
31
+ > **Status: 0.1.** Start with the [API guide](docs/guides/api.md) and the
32
+ > [card DSL guide](docs/guides/card-data-format.md). Progress and what's pending
33
+ > are in [docs/plans/standard.md](docs/plans/standard.md).
34
+
35
+ ## Set support
36
+
37
+ Enjine ships no card data. These are the Standard sets and the card types for
38
+ which real cards are written in the DSL and tested
39
+ (`spec/fixtures/cards/real/`, each card with a scenario spec and rules-monitored
40
+ play):
41
+
42
+ | Set | Creature | Instant | Sorcery | Artifact | Enchantment | Land | Planeswalker | Battle | Kindred |
43
+ |---|---|---|---|---|---|---|---|---|---|
44
+ | Star Trek (`trk`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a | n/a |
45
+ | Reality Fracture (`fra`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a |
46
+ | The Hobbit (`hob`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a | n/a |
47
+ | Marvel Super Heroes (`msh`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a | n/a |
48
+ | Secrets of Strixhaven (`sos`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a |
49
+ | Teenage Mutant Ninja Turtles (`tmt`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a | n/a |
50
+ | Lorwyn Eclipsed (`ecl`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | ✓ |
51
+ | Avatar: The Last Airbender (`tla`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a | n/a |
52
+ | Spider-Man (`spm`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a | n/a |
53
+ | Edge of Eternities (`eoe`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a |
54
+ | Final Fantasy (`fin`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a | n/a |
55
+ | Tarkir: Dragonstorm (`tdm`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | n/a | n/a |
56
+ | Aetherdrift (`dft`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a |
57
+ | Foundations (`fdn`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | n/a |
58
+
59
+ ✓: at least one real card of that type from that set is written and tested.
60
+ —: none yet. n/a: the set has no card of that type. Known gaps are listed per
61
+ set in [docs/plans/standard.md](docs/plans/standard.md#mechanics-gaps).
62
+
63
+ ## Try it: play both sides in your terminal
64
+
65
+ ```sh
66
+ git clone https://github.com/thisiscetin/enjine && cd enjine
67
+ bundle install
68
+ # hot-seat: you play both players, with two of the example decks
69
+ bundle exec exe/enjine --deck1 examples/decks/gruul_beats.json --deck2 examples/decks/azorius_walkers.json
70
+ # play against a random opponent, with a random red-green deck from a card pool
71
+ bundle exec exe/enjine --pool examples/pool.json --deck1 random:colors=RG --deck2 examples/decks/izzet_spells.json --p2 random
72
+ bundle exec exe/enjine --pool examples/pool.json --random-deck colors=WU seed=7 # print a random deck
73
+ ```
74
+
75
+ [`examples/`](examples) holds ready-to-play deck documents (each carries the cards it
76
+ uses) and `pool.json`, a JSON array of cards to build random decks from. They're not
77
+ part of the gem.
78
+
79
+ ```
80
+ ━━ Turn 3 · alice's turn · Main 1 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81
+ Beginning › ▶MAIN 1 › Combat › Main 2 › End
82
+ Priority: alice
83
+ BOB — life 18 · library 50 · hand 6 · graveyard 0
84
+ Lands: Plains [plains#88], Island [island#97]
85
+ Permanents:
86
+ Glory Seeker · 2/2 [glory_seeker#101]
87
+ ALICE — life 20 · library 51 · hand 5 · graveyard 1
88
+ Lands: Mountain [mountain#99], Forest [forest#104], Forest [forest#110]
89
+ Permanents:
90
+ Grizzly Bears · 2/2 [grizzly_bears#106]
91
+ Hand: Lightning Bolt {R} [lightning_bolt#93], Hill Giant {3}{R} [hill_giant#108], …
92
+ Stack (top first): empty
93
+
94
+ ▶ ALICE must act: priority (Turn 3 · alice's turn · Main 1)
95
+ 1) Pass priority
96
+ 2) Cast Lightning Bolt {R}
97
+ 3) Tap Mountain for mana ({T}: Add {R}.)
98
+ …
99
+ alice> 2
100
+ ```
101
+
102
+ When passing priority is your only option, the CLI passes for you. The board
103
+ header shows whose turn it is and the current phase and step. Commands: a number
104
+ picks an option, `o` toggles the observer view (both hands and library order), `h` shows the
105
+ history, `u` undoes, `s FILE` saves, `--load FILE` resumes, and `?` shows help.
106
+
107
+ ## Use it as a library
108
+
109
+ ```sh
110
+ gem install enjine # or in a Gemfile: gem "enjine"
111
+ ```
112
+
113
+ **[→ API guide for callers](docs/guides/api.md)**: games, the decision loop, views, events, save/replay, bots and RL, the deck builder, errors.
114
+
115
+ ```ruby
116
+ require "enjine"
117
+ require "enjine/deck_builder" # opt-in
118
+
119
+ # Enjine knows no cards: a deck document carries its own (see examples/decks).
120
+ alice = Enjine::Player.new(name: "alice", deck: JSON.parse(File.read("examples/decks/gruul_beats.json")))
121
+ # One long-lived builder over any card pool; build as many decks as you like.
122
+ builder = Enjine::DeckBuilder.new(JSON.parse(File.read("examples/pool.json")))
123
+ bob = Enjine::Player.new(name: "bob", deck: builder.build(colors: %w[W U], seed: 7))
124
+ setup = Enjine::Setup.new(alice, bob) # compile once ...
125
+ game = setup.new_game(seed: 42) # ... spawn many games cheaply
126
+
127
+ until game.over?
128
+ decision = game.pending_decision # player, kind, options, details
129
+ game.submit(decision.options.sample)
130
+ end
131
+
132
+ game.result # => {"winner" => "bob", "reason" => "life"}
133
+ game.view_for("alice")
134
+ game.history_for("alice")
135
+ game.export # JSON snapshot; Enjine::Game.import(json, catalog: setup.catalog)
136
+ ```
137
+
138
+ The full API for callers (decisions, views, events, saving and replaying, bots and RL, errors) is in [docs/guides/api.md](docs/guides/api.md).
139
+
140
+ ## Cards are data
141
+
142
+ ```json
143
+ { "name": "Lightning Bolt", "mana_cost": "{R}", "types": ["instant"],
144
+ "abilities": [{ "spell": {
145
+ "targets": ["any_target"],
146
+ "effects": [{ "deal_damage": { "amount": 3, "to": { "target": 0 } } }] } }],
147
+ "rarity": "common", "printing": { "set": "fdn", "number": "1" } }
148
+ ```
149
+
150
+ `rarity` and `printing` are optional and ignored by the rules. Check your cards:
151
+
152
+ ```sh
153
+ bundle exec exe/enjine --validate cards.json # JSON report; exit 1 if any card is invalid
154
+ bundle exec exe/enjine --verify my_cards/ # validate, then play-test them in rules-monitored games
155
+ ```
156
+
157
+ ## Documentation
158
+
159
+ | Document | What it covers |
160
+ | --- | --- |
161
+ | [SUPPORTED.md](SUPPORTED.md) | every supported type, keyword, ability, cost, effect and filter (generated) |
162
+ | [docs/reference/](docs/reference) | parameters and examples for every primitive and decision (generated) |
163
+ | [docs/guides/card-data-format.md](docs/guides/card-data-format.md) | the card DSL: fields, abilities, effects, validation, DB schema |
164
+ | [docs/guides/api.md](docs/guides/api.md) | the library API for callers: games, decisions, views, events, save/replay, bots and RL, deck builder, errors |
165
+ | [docs/guides/deck-builder.md](docs/guides/deck-builder.md) | random decks from any card pool (a set, your collection), in chosen or random colors (`--pool`, `--random-deck`) |
166
+ | [docs/guides/adding-a-mechanic.md](docs/guides/adding-a-mechanic.md) | how to extend the engine |
167
+ | [docs/MECHANICS_COVERAGE.md](docs/MECHANICS_COVERAGE.md) | scope and coverage against the Comprehensive Rules, and the full CR checklist |
168
+ | [docs/plans/standard.md](docs/plans/standard.md) | progress: what's done, what's in progress, what's pending, and the per-set gap census |
169
+ | [docs/DESIGN.md](docs/DESIGN.md), [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | design decisions, code layout and where things are |
170
+
171
+ ## Development
172
+
173
+ ```sh
174
+ bundle exec rake # specs (with coverage gate + docs drift check) and RuboCop
175
+ bundle exec rake docs # regenerate SUPPORTED.md and docs/reference after changing a primitive
176
+ bundle exec rake fuzz[50,1] # 50 random games, invariants checked after every decision
177
+ bundle exec rake bench # games per second (benchmark-ips)
178
+ bundle exec rake profile # stackprof CPU profile
179
+ ```
180
+
181
+ Every mechanic is tested at three levels:
182
+ - **Unit:** the primitive and its validation.
183
+ - **Integration:** scenario boards built with a small DSL, then legal options, then a decision, then the resulting state and events.
184
+ - **End-to-end:** full 60-card games, scripted and fuzzed, with export/import round-trips.
185
+
186
+ ## Contributing
187
+
188
+ Contributions are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) and the
189
+ [code of conduct](CODE_OF_CONDUCT.md). Report rules bugs in
190
+ [GitHub issues](https://github.com/thisiscetin/enjine/issues) with the seed and
191
+ the decision log (or a saved snapshot). Security issues: [SECURITY.md](SECURITY.md).
192
+
193
+ ## License
194
+
195
+ MIT, see [LICENSE.txt](LICENSE.txt). Changes are in [CHANGELOG.md](CHANGELOG.md).
196
+
197
+ Enjine is unofficial Fan Content, not approved or endorsed by Wizards of the
198
+ Coast. Magic: The Gathering, card names and the Comprehensive Rules are property
199
+ of Wizards of the Coast LLC. The repository contains no card text, art or rules
200
+ text: test cards are structured data written by hand, and only rule numbers are
201
+ cited.