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!
Oh and this whole section as well. As a bonus.
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:
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:
And here's how it runs when a spell is cast:
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.