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
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.
|