ETHAN CROOKS

Programming Magic the Gathering - Part 4: Turns/Phases/Steps/Priority Shenanigans

2020-11-15

Now the game has the ability to present the player with choices, and there is a board state for it to manage, there now needs to be a loop of actions the that can be taken in order to run a game. So, how is a game of Magic: The Gathering structured?

Why, with an entire section of the massive rules pdf of course!

A screenshot of the table of contents for section 5 of the MTG comprehensive rules.

Oh and this whole section as well. As a bonus.

A screenshot of the MTG comp rules on timing and priority.

Aaaah.

How is a MTG turn played out?

During a game of Magic, players only need to have a vague, board knowledge of the turn structure. Upkeep, First Main, Combat, Second Main, End. However, when creating this implementation I need to make sure that each detail of Magic's complex turn structure is represented.

I've been OK at interpreting the raw MTG rules so far, but for this I was really struggling. I was finding it nigh-impossible to draw up a diagram so I could wrap my head around exactly how the turn loop worked. Then I started looking around to see if anyone else had attempted the same, and thankfully someone had!

Irina Smonova, a professional MTG judge, had written up a brilliant article explaining the intricacies of how priority, the stack and turn structure interact. For that article, she made the following flowchart:

A flowchart explaining the turn structure.

Glorious flowcharts. I can work with flowcharts.

Implementing the turn structure

First I made a class to represent the current phase, complete with a giant empty switch statement to fill later:

public class Phase
{
    // Quick note - these are technically called *steps*, but since there is
    // no real difference between a step and a phase anymore it doesn't matter.
    public enum PhaseType
    {
        Untap,
        Upkeep,
        Draw,
        Main1,
        CombatStart,
        CombatAttackers,
        CombatBlockers,
        CombatDamage,
        CombatEnd,
        Main2,
        End,
        Cleanup
    }

    // To be used to work out when to start a new turn
    public const PhaseType StartingPhase = PhaseType.Untap;
    public const PhaseType FinalPhase = PhaseType.Cleanup;

    public PhaseType type = StartingPhase;

    public void StartCurrentPhase()
    {
        var mtg = MTG.Instance;
        switch (type)
        {
            // "At the beginning of" effects

            case PhaseType.Untap:
                // Phasing happens

                // All permanents untap
                foreach (OID oid in mtg.battlefield)
                {
                    var mtgobj = mtg.objects[oid];
                    mtgobj.permanentStatus.tapped = false;
                }

                // No priority
                break;
            case PhaseType.Upkeep:
                // AP Priority
                break;
            case PhaseType.Draw:
                // The active player draws a card
                mtg.players[mtg.turn.playerTurnIndex].Draw();

                // AP Priority
                break;
            case PhaseType.Main1:
                // Increment and resolve sagas
                // AP Priority
                break;
            case PhaseType.CombatStart:
                // AP Priority
                break;
            case PhaseType.CombatAttackers:
                // Attackers are declared by AP
                // AP Priority
                break;
            case PhaseType.CombatBlockers:
                // Blockers are declared
                // Attacking player chooses damage assignment order of attackers
                // Defending player chooses damage assignment order of blockers
                // AP Priority
                break;
            case PhaseType.CombatDamage:
                // Attacking player assigns combat damage
                // Defending player assigns combat damage
                // Damage is dealt (simultaneously)
                // AP Priority
                break;
            case PhaseType.CombatEnd:
                // AP Priority
                break;
            case PhaseType.Main2:
                // AP Priority
                break;
            case PhaseType.End:
                // AP Priority
                break;
            case PhaseType.Cleanup:
                // Players discard to max hand size
                foreach (var player in mtg.players)
                {
                    player.Discard(player.hand.DiscardsNeeded);
                }

                // All marked damage is removed
                // No priority
                break;
            default:
                throw new NotImplementedException();
        }
    }

    public void EndCurrentPhase()
    {
        // Empty mana pools
        foreach (var player in MTG.Instance.players)
        {
            player.manaPool.Empty();
        }

        // "Until the end of" effects end
    }

    // Can the active player cast sorcery speed stuff during this phase?
    public bool SorceryPhase { get
        {
            switch (type)
            {
                case PhaseType.Main1:
                case PhaseType.Main2:
                    return true;
                default:
                    return false;
            }
        }
    }

    // Does each player get priority at the end of this phase?
    public bool GivesPriority { get
        {
            switch (type)
            {
                case PhaseType.Untap:
                case PhaseType.Cleanup:
                    return false;
                default:
                    return true;
            }
        }
    }
}

And with it a struct to represent what's going on with the current turn, with a few helper functions to increment priority/phase/turn:

public struct Turn
{
    public int turnCount;
    public int playerTurnIndex;
    public int playerPriorityIndex;
    public Phase phase;

    public void Init()
    {
        turnCount = 0;
        playerTurnIndex = 0;
        playerPriorityIndex = 0;
        phase = new Phase();
    }

    // Resets priority to AP
    public void ResetPriority()
    {
        playerPriorityIndex = playerTurnIndex;
    }

    public bool ActivePlayerPriority {
        get { return playerPriorityIndex == playerTurnIndex; }
    }

    // Returns true if it rolled over
    public bool IncTurn(int playerCount)
    {
        playerTurnIndex++;
        turnCount++;
        if (playerTurnIndex >= playerCount)
        {
            playerTurnIndex = 0;
            return true;
        }
        return false;
    }

    // Returns true if it rolled over
    public bool IncPhase()
    {
        if (phase.type == Phase.FinalPhase)
        {
            phase.type = Phase.StartingPhase;
            return true;
        } else
        {
            phase.type = phase.type.Next();
            return false;
        }
    }

    // Returns true if it rolled over
    public bool IncPriority(int playerCount)
    {
        playerPriorityIndex++;
        if (playerPriorityIndex >= playerCount)
        {
            playerPriorityIndex = 0;
            return true;
        }
        return false;
    }
}

I then implemented the turn structure that the flowchart described. With a whole bunch of while loops :)

public void GameLoop()
{
    // Main loop
    while (true)
    {
        // Beginning of step
        CalculateBoardState();
        // Stuff that triggers at start of step
        Console.WriteLine("Start " + turn.phase.type.GetString());
        Console.WriteLine("Active player - " + turn.playerTurnIndex);
        Console.WriteLine();
        turn.phase.StartCurrentPhase();
        CalculateBoardState();

        if (turn.phase.GivesPriority || theStack.Count > 0)
        {
            // Loop until priority has finished
            while (true)
            {
                // Active player gets priority
                turn.ResetPriority();
                int passCount = 0;
                // Loop until all players have passed in a row
                while (true)
                {
                    // Loop until current player has passed
                    while (true)
                    {
                        Console.WriteLine("Priority for player " + turn.playerPriorityIndex);
                        CalculateBoardState();
                        // State based actions go here
                        CalculateBoardState();
                        bool passed = ResolveCurrentPriority();
                        if (passed) break;
                        else
                        {
                            passCount = 0;
                        }
                    }
                    passCount++;

                    // Switch priority if all players have passed
                    if (passCount < players.Count)
                    {
                        turn.IncPriority(players.Count);
                    }
                    else break;
                }
                if (theStack.Count > 0)
                {
                    CalculateBoardState();
                    theStack.ResolveTop();
                }
                else break;
            }
        }
        // End of step
        Console.WriteLine();
        CalculateBoardState();
        turn.phase.EndCurrentPhase();
        bool endturn = turn.IncPhase();
        if (endturn)
        {
            turn.IncTurn(players.Count);
            // Stuff at end of turn
        }
    }
}

I think 4 nested while loops is a new personal record. This is the function that gets passed to the new thread, and should continuously run until the game is over, hence the unbroken while loop enclosing everything else. Until there is a way for a player to win the game, this will run until the program is closed.

Testing it out

Here's how it looks:

A screenshot of the game loop setup.

And here's how it runs when a spell is cast:

Screenshot of a spell being cast, part 1

Screenshot of a spell being cast, part 2

OK, this is where looking through the terminal window starts to get a little incomprehensible. As far as I can tell it seems to work OK?

  • The phase ends when all players pass in succession
  • When an item is on the stack, there is a round of priority and then it resolves
  • The untap and end steps have no priority (if nothing is on the stack)

I'll need to create a bunch of unit tests to make sure that everything works as it should, as it is pretty complex and it's crucial that I get it right. After looking at this it is probably also time I freshen up the command line interface so that I can develop future features without feeling like I'm solving a wordsearch.

Thanks for reading!

Stuff is starting to slot together! Next post I'll be taking about paying for costs to cast spells and activate abilities.

View my source code here.