Contents
- Wait, what’s Magic?
- Which language?
- Drafting the language
- Player and Global scope
- Gameplay scope
- Effects scope
- Designing the language
- Planning implemenation
- Actually hacking
Wait, what’s Magic?
As someone who’s been paid as a magician (I was once hired to be a comedy magician doing intentionally mediocre tricks for the grand opening ceremony of a kava bar and there are people who to this day still only know me as a magician) and gotten multiple people into the game of Magic the Gathering, the briefest way I can describe the difference between the two kinds of “magic” is that Magic the Gathering is the one where surprises aren’t so delightful.
Created in 1993, there are plenty of rabbit holes to go down if you’re interested in learning more about the strategies or lore behind the game. However, the focus for this project is specifically making a modern way to easily programmatically play Magic the Gathering with a similar developer experience to python-chess (so folks would be able to design and simulate games as well as have their bots play against others).
While there are some existing engines for the Magic the Gathering game (thanks to the Wizards of the Coast’s Fan Content Policy), the best existing example I found of what I’m looking for is the Init.txt example for mage where part of the code sample is copied below:
[init]
battlefield:Human:Forest:5
battlefield:Human:Plains:5
battlefield:Human:Mountain:5
battlefield:Human:Swamp:5
battlefield:Human:Island:5
hand:Human:Lightning Bolt:2
[quick game example]
graveyard:Human:Bloodghast:1
graveyard:Computer:Bloodghast:1
hand:Human:Bloodghast:1
hand:Computer:Bloodghast:1
[real game example]
graveyard:ReachyRichGuy:Bloodghast:1
graveyard:Computer 3:Bloodghast:1
[add tokens]
token:Human:TreasureToken:10
token:Human:ZombieArmyToken:3
[add planeswalker with loyalty]
battlefield:Human:Karn Liberated:1
loyalty:Human:Karn Liberated:20
[add emblem]
emblem:Human:ElspethSunsChampionEmblem:1
[add commander]
commander:Human:Silvercoat Lion:1Except with an API similar to Lichess, which means each action or update would need to be handleable one at a time after a game’s already been initialized. As it stands today, there isn’t an up to date package or web app that makes something like this quickly accessible:
const player1 = new MinMaxPlayer(/* ... */);
const player2 = new LLMPlayer(/* ... */);
const game = new Game(
[player1, player2],
/* some configuration/connection arguments... */
);
while (!game.done()) {
if (game.turn() === "1") {
const nextMove = player1.move(game); // e.g. tap two lands
game.play(move); // Any interactions/decisions are handled by the game object using players
} else {
const nextMove = player2.move(game); // e.g. activate an artifact ability
game.play(move); // Any interactions/decisions are handled by the game object using players
}
}
if (!game.draw()) {
console.log(`${game.winner()} won the game!`);
} else {
console.log("Game was a draw");
}Now that we have a broad idea of the goal, let’s dive into planning how to go about making this.
Which language?
In principle, you could make a game engine in any language; it just might not be the most performant or portable to different systems. In a perfect world, one implementation for a Magic the Gathering engine would be taking the latest official rules which look something like this when you’re reading through the individual rule items:
110. Permanents
110.1. A permanent is a card or token on the battlefield. A permanent remains on the battlefield indefinitely. A card or token becomes a permanent as it enters the battlefield and it stops being a permanent as it’s moved to another zone by an effect or rule.
110.2. A permanent’s owner is the same as the owner of the card that represents it (unless it’s a token; see rule 111.2). A permanent’s controller is, by default, the player under whose control it entered the battlefield. Every permanent has a controller.
110.2a If an effect instructs a player to put an object onto the battlefield, that object enters the battlefield under that player’s control unless the effect states otherwise.
110.2b If an effect causes a player to gain control of another player’s permanent spell, the first player controls the permanent that spell becomes, but the permanent’s controller by default is the player who put that spell onto the stack. (This distinction is relevant in multiplayer games; see rule 800.4c.)Then, looping over every entry, using a library or framework to translate the “rule speak” into that codified form. Like if you were given a description of a 3D scene, you’d be able to ‘translate’ details into Unity code (the code below is taken from this forum post).
GameObject obj = new GameObject("NewGameObjectsName!");
obj.AddComponent<Light>();
obj.transform.position = new Vector3(10,0,0);Rather than a toolkit for objects and methods relating to 3D scenes, there’s got to be something for games, right?
While each of the pseudocode examples above show functions being called, this isn’t a job for any particular paradigm for programming so long as the resulting “rule to code” output is a single statement that ensures the written rule gets enforced (otherwise imagine playing chess except any piece can teleport to anywhere on the board). And, as with any technical question, the answer is it depends.
A general purpose programming language with a reliable web framework would do the trick for the API part but there isn’t a game engine or library available for the purpose of a card game rather than physics oriented. Meaning doing this predominantly in Python or TypeScript would mean basically recreating forge in that language instead of Java with all the mechanics/rules handling as well.
So, how about a language specific for games? Turns out there is one called GDL except it’s really a variant of Datalog which isn’t too different from Prolog. While Prolog did the job for the initial version of Erlang, an important consideration is to be made around checking legality of states and moves. I’m less interested in asking the question “what’s every possible card that could be played via sequence of moves from this point?” and more interested in asking either “is this particular action doable?” or “is this creature dead?”.
While optimizations can be done (cc people claiming X language/framework is inefficient but Y large company shows engineering improvements can be done) to a Prolog project, I figure it’d be better to spend time during the hackathon tweaking the edge cases of the game rather than the edge cases of logical derivation (ie being able to dynamically overwrite rules due to card effects in restrictive ways).
Other options such as formal specification languages (ie Alloy, TLA+) do a better job at capturing the state that is the game at a given point in time however they’re not accessibly runnable nor is that their intended purpose. If the goal here was to answer the question “are the rules fully evaluative and not in need of corrections because of a cheat code?” then they would make more sense to use.
Which then brings us to the hackathon in question where the prompt is to both:
- Create a language for a game
- Make a game using said language
Fantastic, so if we made a language for representing game mechanics that are specific to card games then that’d make the idea of “one code statement per rule” more accomplishable. And, should the language have notions around “voluntary player actions” versus “defining involuntary steps in the game” then it should be doable to also have an exported API or DSL for programmatically playing the game as well (this consequentially also means a UI is doable atop this ‘protocol’).
Drafting the language
Fortunately, this doesn’t need to be a full language with dependent types or formal verification; meaning it could look more syntactically like a simple logic oriented language with some declarative patterns. But it may help to think about other card games in order to determine the necessary abstractions to make accessible from the language. For example, here’s a way to abstract the game Uno.
Or the game Go Fish.
While there are only 52 cards in a deck of playing cards (54 if you include jokers) compared to the numerous printed Magic cards that are out there, the “zones” that cards can fall into is similar albeit on a simpler scale. Both of the games above show a single deck being shared by both players however there are games where that’s not the case such as Speed.
Pile] SPACER[ ]:::invisible R[Right Play
Pile] RP2[Replacement pile] end subgraph p1 [Player 1] P1Deck[Deck] ~~~ P1Hand[Hand] end classDef invisible fill:none,stroke:none
Now, let’s take a look a diagram for Yugioh (here is the source for below with colors) and see what patterns we notice before looking at the diagram for Magic.
As you can see there may or may not be “universal” piles of cards (ie the deck in Go Fish) and every player contains the same places for cards to go (while different strategies may leverage certain ones or not at all). Which then means we have two core concepts to scaffold before getting into card relations or mechanics.
Where a pile of cards may be face up or face down (and maybe restrictively so rather than a placeholder for any state) and a slot can be restricted to a single card (ie two slots for only being allowed two cards in Texas Hold ‘Em or a single slot for the command zone in the Commander variant of Magic). While the above shows a game with there being five properties of a Player and two properties of the Global game, the premise here is that there can be an arbitrary number of them depending on the game being defined (here with a SQL-esque pseudocode):
ADD PILE TO PLAYER deck FACE DOWN;
ADD PILE TO PLAYER hand FACE HIDDEN;
ADD SLOT TO PLAYER active_pokemon FACE UP;
ADD PILE TO GAME arena FACE UP;So then, let’s take a look at an abstraction for Magic to see if there are other details or conditions we should be aware of. Note that instead of “Deck”, the term in Magic is “Library”.
As a caveat, the above doesn’t include phasing or a distinction between where lands go and where the other spells go as you would find on a playmat. Nevertheless, for these cases or the matter of “temporary” zones (ie when cards phase in/out or when a player is choosing one of the top N cards that were on their library), these can be handled by simply defining new piles or slots with the necessary conditions set.
On top of that, this doesn’t begin to convey the rules or relations between cards that are standalone (ie creatures) versus ones that must be attached (ie enchantments or sets in Go Fish). Regarding card effects and interactions, that’s something that ought to be handled differently since those can be affected by conditions or voluntary decisions made by players in constrast to the unchangeable scaffolds. Although, in terms of programming, this resembles somthing like defining base classes or models prior to then specifying application logic.
Therefore it may make sense to treat the different “scopes” of game definition as being separate similar to how a single Svelte file contains the JavaScript portion, the UI/components portion, and the styling portion. Here, in the case of card games, that could be described with the following:
- Player and global scope
- Gameplay scope
- Effect scope
Player and global scope contains the scaffolds being described above. The Gameplay scope contains the core mechanics for playing the game (ie drawing a card at the start of a turn, having certain phases throughout a turn, having checks or effects at certain steps) as though you were to define playing the game with effectless cards that don’t serve a purpose besides attacking. Since it’s the most granular, I’ll be following the Magic approach for a turn being comprised of phases followed by steps in each of them.
Lastly, the Effect scope contains the abilities or modification to gameplay scope that can come as a result from cards (ie letting players draw more than once at the start of a turn, requiring additional tax for casting spells, unlimited hand sizes, gaining a counter every turn). In the case of games involving a deck of playing cards, there usually aren’t rules that belong in the Effect scope with the exception of games involving a “Trump” that’s dynamically introduced at the start of the game.
In terms of how these scopes relate to each other:
- Player and Global scope -> Does not change
- Gameplay scope -> Defined based on Player and Global scope, may change due to effects
- Effect scope -> Defined based on properties defined in higher scopes, may overwrite rules/conditions and may not be permanent
Player and Global scope
The player and global scope would include properties such as whose turn it is, or player life. Continuing the SQL pseudocode above that could look something like:
ADD PROPERTY TO PLAYER life NUMBER; -- A player's life total
ADD PROPERTY TO GAME storm_count NUMBER; -- A number used to count the number of spells cast thus far this turnSince this is the scope for immutable constructs, this also includes the topics of card structure and variants (both for cards and games). In a deck of playing cards, there are arguably three variants of cards: number cards, face cards, jokers. A way that could look like with the SQL-esque definition would be:
CARD VARIANT NUMERICAL (
num number
suit string
)
CARD VARIANT FACE (
letter string
suit string
)
CARD VARIANT JOKER (
black_and_white bool
)For the sake of succinctness in the eventual code in this language, the variants of cards make more sense as a union type rather than an enum field that’s used inside a generic object. Although, for actual implementation, it could make sense to take the card variant labels “NUMERICAL”, “FACE”, and “JOKER” then treat them in an enum sort of way in the resulting consumer API.
For game variants, since they are usually extending the base rules rather than defining entirely new schematics (like in the case of card variants), they can instead be treated like how CSS rules are able to be overwritten. The game variant labels would also be a means of checking correctness when a game match is instantiated for an invalid variant.
ADD PROPERTY TO PLAYER life NUMBER DEFAULT 20;
ADD PILE TO PLAYER library FACE DOWN;
GAME VARIANT COMMANDER (
ADD PROPERTY TO PLAYER life NUMBER DEFAULT 40;
ADD SLOT TO PLAYER command_zone FACE UP;
);Gameplay scope
With the Player and Global scope sketched up, let’s now consider the Gameplay scope. Here is where would define rules for the game like the phases and steps in a turn as well as new primitives such as drawing from a particular pile (whether at random, from the top, or by choice).
The features this portion requires are:
- Being able to reference nested attributes of player/global properties (ie player’s life total, appending new game loss condition of having no life)
- Being able to manipulate/filter collections of values (ie clearing mana pool at end of phase)
- Being able to handle conditions (ie maximum hand size at end of turn)
- Voluntary actions to be made by a player (ie optionally move cards from one zone into another if they meet a condition)
The intent with the above features being that this Card Game Design Language (CGDL for short, might come up with a better name later) works for both defining Magic as well as other card games (ie Uno, Egyptian War). So if we’re to imagine a LISP-y approach for this:
(defclass gameplay ()
((conditions
:lose '((lambda (player)
(<= player.life 0)) ;; A player loses if they have less than or equal to zero for the life property using dot-accessor
(lambda (player)
(>= (player.poison-counters) 20)))) ;; A player loses if they have 20 or more poison counters
(turn
:phases '(("Beginning phase" . ("Step 1" . (lambda ())) ;; Do nothing, purely mechanical step
("Step 2" . (lambda ()
(draw (current-player) ("top" . (library current-player))))) ;; Current player draws from the top of their library
("Step 3" . (lambda ()
(priority (current-player) '(("Tap card" . (lambda (card)
(unless (not card.tapped)
(violation "Cannot tap an already tapped card")) ;; Condition not met
(set (card.tapped) #'t))))
("Activate ability" . (lambda (card) ...))))) ;; Current player may optionally do an action
("Cleanup step" . (lambda ()
(set (mana-pool (current-player)) 0)))) ;; At the end of this phase, empty the player's mana pool
("Attack phase" . ("Declare Attackers Step" . (lambda ()
(priority (current-player) '(("Choose attackers" . (lambda (cards)
(unless (not (any (card.summoning-sickness) cards))
(violation "Cannot attack with a creature that entered this turn")))))))))))))As shown above, dotted pairs can provide a way for components of the game to be labeled with readable names. Additionally, we introduce an idea of a violation that’s alike errors or exceptions but not exactly because an opinionated approach may go like:
- Errors are when something actually goes wrong
- Exceptions are when a user’s sequence of actions results in an expectable exception to what they’re supposed to do
- Violations are when a user (or in this case player) attempts to perform an action that is actually not legal
Also visible here is that any manipulation done to the rules or actions (ie from a card effect/ability) can be captured in a single atomic or functional statement that’d make it removable from the game after the turn’s ended or the card providing the effect is removed from the battlefield; leveraging a perk of “code as data”.
Lastly, inspired by the interactive method from emacs lisp, there’s also a priority method introduced here which grants the current player priority and of course would need to be spelled out explicitly for each of the other players as well.
Effects scope
Rather than follow the Svelte pattern referenced earlier by having three different languages for the three sections (JavaScript/TypeScript, JSX, then CSS), it makes sense to have the rules and definitions in the Effects scope to actually match the language used in the Gameplay scope. A keyword ability like Deathtouch (which has it so any creature that combats with a creature possessing Deathtouch dies at the end of combat) references card properties (whether it’s alive or dead as well as the zone it’s in whether that’s the battlefield or graveyard).
While many different programmaning analogies could be made for the structure of the scopes and each of their intentions, to reference a basic image of how the code for a configurable text editor such as emacs or vim is like:
First, there’s the code for the editor or the part that actually works with files. Next, there’s the part that’s introduced by an .emacsrc or .vimrc file which includes the real “meat” of making an editor really feel like ones own. Finally, there’s what the user does with the editor such as opening a particular file or reading a certain directory.
Translating that to the scopes here, what that then looks like is:
Ergo preserving the s-expression approach where “code as data” means the rules and game mechanics are mutable which is the preferable scenario here so we don’t re-implement core gameplay mechanics in different parts of the codebase.
Designing the language
Now, it’s time to outline what I plan on working on for the hackathon. Given the task of “making a language”, we technically need to make at least one language. The gameplan here is to develop two languages to encapsulate a given card game:
- A SQL-esque language for defining the immutable properties of the game itself
- A LISP-y configuration language for defining the Gameplay as well as Effects scopes
The SQL-esque language provides what I think is a slightly better developer experience for establishing immutable constructs than just more stacked s-expressions. The LISP-y language would provide flexible and atomic ways of representing both game mechanics as well as changes to those mechanics.
Having made lexers and parsers from scratch multiple times, I figure this would be a good opportunity to try out a library to handle this part so I can focus more on the grammar than clever optimizations around error handling. I came across ohm.js from a comment on lobste.rs and, when comparing to ts-parsec, it seems to be more succinct at defining the grammar with the tradeof of being less precise at parsing under the hood.
As for why not instead making custom DSLs in a language “better suited” for language development like Ocaml? There’d eventually be a decent amount of reinventing the wheel due to a less popular package ecosystem.
As for why not instead using a language like Ruby which already has good examples for metaprogramming such as Sonic Pi? Or even doing similar metaprogramming with a framework such as Phoenix / Elixir where a DSL inside the web server is used to define the logic of the game? Admittedly, this idea was very enticing. In the end, putting aside all matters of popularity of language, the way metaprogramming would be done in either of those languages (despite having nice web frameworks), would end up with a similar problem to using Prolog where some amount of work would be done specifically wrangling with the language behavior itself. Ruby/Elixir metaprogramming feel like the usual developer experience for “exposing the AST” in some languages where it’s just dumped in front of you like you’re supposed to deal with it instead of getting to cleanly write macros.
Knowing this’d be a TypeScript project, it then makes sense to imagine an initialization API to look something like the following:
const schema = `ADD PILE TO PLAYER deck FACE DOWN;
ADD PILE TO PLAYER hand FACE HIDDEN;
ADD SLOT TO PLAYER active_pokemon FACE UP;
ADD PILE TO GAME arena FACE UP`;
const gameplay = `(defclass gameplay ()
((conditions
:lose '((lambda (player)
(<= player.life 0)) ;; A player loses if they have less than or equal to zero for the life property using dot-accessor
(lambda (player)
(>= (player.poison-counters) 20)))) ;; A player loses if they have 20 or more poison counters
(turn
:phases '(("Beginning phase" . ("Step 1" . (lambda ())) ;; Do nothing, purely mechanical step
("Step 2" . (lambda ()
(draw (current-player) ("top" . (library current-player))))) ;; Current player draws from the top of their library
("Cleanup step" . (lambda ()
(set (mana-pool (current-player)) 0)))))))) ;; At the end of this phase, empty the player's mana pool`
// Creating a game that can be played
const game = new Game(schema, gameplay);
// Establishing players that match a certain spec
const player1 = new MinMaxPlayer(/* ... */);
const player2 = new LLMPlayer(/* ... */);
// Starting a match with the given players to play the specified game
const match = game.play(player1, player2);
const result = match.go();
if (result.draw()) {
console.log("Was a draw");
} else {
console.log(`${result.winner()} won!`);
}The “spec” being referenced for players would be a certain collection of async methods (so that API requests and/or UI interactions can be the source of a decision). Additionally, in order for programmable players to be possible, rather than invent some obfuscated FEN notation for playing card games (which would look different for each game), I think the following protocol makes sense for the logic surrounding players’ decisions:
1. Initialize the game state and stats including dealing cards
2. Give pre-game priority for dealt cards (ie mulligan, vote to re-deal)
3. Take random player to go first
4. Proceed with phases and steps for current player's turn
5. Grant priority to players to elect to act when specified
6. Continue with phases and steps until end of turn and pass on to next player then return to Step 4Number 2 above implies that a new keyword would be introduced in the Gameplay scope for defining a pre-match play hook (like providing option to mulligan). And where giving priority to players to act then looks like a method with the following type signature:
interface Action {
name: string;
callback: Function;
}
interface GrantedPriority {
possibleActions: Array<Action>;
}
interface PlayerDecision {
chosenActionIndex: number; // -1 for no action
}
class ImplementsTheSpec {
constructor() {}
async makeMove(grantedPriority: GrantedPriority): Promise<PlayerDecision> {
return {
chosenActionIndex: -1, // Do nothing
}
}
async registerMove(otherPlayerIndex: number, otherPlayerDecision: PlayerDecision) {
// We're not capturing opponent moves, just do nothing and wait for them to draw out
}
}In conjunction with being given opportunities to receive priority, players are given the decisions made by other players allowing for them to capture a picture of what the board state looks like to them.
Somewhat referenced in the TypeScript snippet above, it’s possibly better to simulate Magic games than chess because you eventually draw out so it forces enough logic to be implemented to intentionally play the game (unlike playing random moves in chess which has a possibility of going on forever).
Planning implementation
With a mostly decent idea of what I’ll be building during this hackathon, now’s a good time to consider two important elements that’d contribute to the iterability of the overall project:
- A REPL environment in which new statements can be iteratively processed
- A test suite that covers different games in different scenarios
Since the grammar would be describable in a handful of lines of PEG for both the SQL as well as LISP, the bulk of work being done around the language would be making sure the rules are interpreted correctly as well as exported properly.
Card Game Configuration Language
ADD PROPERTY TO GAME starting_player string;
ADD PROPERTY TO PLAYER life number DEFAULT 20;
ADD PILE TO PLAYER library FACE DOWN;
ADD PILE TO PLAYER graveyar FACE UP;
ADD PILE TO PLAYER exile FACE UP;
GAME VARIANT commander (
ADD PROPERTY TO PLAYER life number DEFAULT 40;
ADD SLOT TO PLAYER command_zone FACE UP;
)
CARD VARIANT foo (
column_name type
)
CARD VARIANT bar (
column_name_2 type
)Card Game Design Language
(game
(conditions
:lose '())
(phase "Beginning Phase"
(step "Step 1" (lambda ())) ;; Do nothing
(step "Step 2" (lambda ()
(draw current-player library top))) ;; "library" {pile} can be checked as can "top" {"random", "chosen"}
(step "Step 3" (lambda ()
(priority current-player (option "Label" (lambda ()))
(option "Label" (lambda ()))))))
(phase "Attack Phase"
(step "Declare attackers step" (lambda ()
(priority current-player ...)))))With a few unit tests with parsing and defining a game as well as game mechanics, there’d be enough for having both example code as well as some assurance that the grammar’s working as intentional. The real bulk of tests would be in specifying the simpler games (ie Go Fish) but with a full game being simulated. Then the bulk of work can be focused for expanding the featureset to meet Magic’s requirements.
Actually hacking
For the first stretch, I focused on getting the CGCL and CGDL working since they were going to be the backbone for further development. Along the way, I realized I needed to add a third DSL (which I kept SQL-esque due to immutability) for representing the cards that are in different piles (ie what are the cards in a deck of playing cards when playing War, or the cards in players’ decks in Pokemon).
After a simple crude version and a bit of iterating, I got a basic version of Go Fish working with the following code for configuring the game.
ADD PILE TO GLOBAL deck FACE DOWN;
ADD PILE TO PLAYER hand HIDDEN;
ADD PILE TO PLAYER revealed_matches FACE UP;
CARD VARIANT NUMERICAL (
num number
suit string
)
CARD VARIANT FACE (
letter string
suit string
)
CARD VARIANT JOKER (
black_and_white bool
)And the following for designing the game.
(game
(conditions
:win (lambda (player) (= 0 (count (hand player)))) ; A player wins when they have no cards in their hand
:lose (lambda (player other-player)
(and
(= (count deck) 0)
(< (count (revealed_matches player)) (count (revealed_matches other-player)))))) ; A player loses if the deck is empty and the other player has more revealed cards than them
(setup
(set deck cards) ;; Set the deck pile to be the cards (symbol like player)
(let (num-cards-in-hand 7)
(map players (lambda (p) (repeat num-cards-in-hand (lambda () (draw p deck))))))) ; Each player starts with 7 cards in their hand
(phase "Player's move"
(step "Ask opponent for card" (lambda ()
(let (card-in-question (priority current-player (option num)
(option letter))) ; Current player begins their turn by asking the other player for a card
(if (contains (hand opposing-player) card-in-question)
(draw current-player (hand opposing-player) card-in-question) ; If the other player's hand contains the requested card then draw it to current player's hand
(draw current-player deck))))) ; Otherwise current player draws from the deck
(step "Reveal sets" (lambda ()
(let* ((legal-options (filter (hand current-player) (lambda (card) (>= (count (hand current-player) card) 3))))
(set-to-reveal (priority current-player
(map legal-options (lambda (opt) ;; If legal options is empty then priority will autofill `pass`
(if (num opt) ;; If is a numerical card then use the number as option
(option (num opt))
(option (letter opt))))))))
(if set-to-reveal ;; If player chose a set (not pass)
(if (>= (count (hand current-player) set-to-reveal) 3)
(draw (hand current-player) (revealed_matches current-player) set-to-reveal) ;; Move all matching cards to revealed_matches
(illegal "You do not have enough " set-to-reveal " to reveal a set")))))))) ;; When draw is given piles as first args, then cards equal to third arg are 'drawn' from first to secondIdeally, when you read the sections of the code above, it makes sense if you’ve ever played Go Fish before. I’m not working on something to enter the Linux Foundation so improvable areas are bound to arise. With the random players, it often ended with either the deck running out and a player having more “revealed cards” or a player ending up empty handed due to the other player requesting the last card out of their hand.
Nevertheless, it’s time to get working on Magic! At this point, I know I won’t be able to build a full implemenation of all the evaluative rules but I can still assemble something that shows some adequate gameplay (so more than just casting and attacking with creatures) plus add a web UI on top of it.
Where I got started was by assembling two simple decks: one mono-white and one mono-blue. Both predominately consist of vanilla creatures without effects but a few spells are included to make up for that. From there, it was a matter of assembling the initial CGDL for outlining the game.
With a couple hours to go, I have a partly working Magic game engine and a somewhat helpful UI displaying the game as it takes place. Crunching to get this to the finish line…
At last! I got a game to play till completion between random players (although lands and mana did behave funnily). Less than two hours to go.

After some more fixes and pieces, I present to you: two random bots playing Magic the Gathering:
You can view the source code on GitHub.