Programming Magic The Gathering - Part 3: Players can do stuff
2020-11-13
OK, so I've done a bunch of stuff before the last time I updated this blog. I'll be making multiple posts to talk about different things. Here I'll talk about choices!
Choices between things
In a game of Magic the Gathering, a player can make decisions. The decisions that they make can impact the game. From the most abstract perspective possible, that is all there is - a player gets presented with a list of choices - then they pick one or more options from that list. That choice does something to the game state. Repeat until one player wins.
This means I can make a generic type to represent any sort of choice, from a choice between cards to a choice between permanents, even the choice between the options a player has when they have priority.
// Base abstract class
public abstract class Choice
{
public bool Resolved { get; protected set; } = false;
public abstract void ConsoleResolve();
// Helper function cause I miss python's input() :(
protected static string Prompt(string prompt)
{
Console.Write(prompt);
return Console.ReadLine();
}
}
// Generic classes are neat
public class Choice<T> : Choice
{
public List<T> Options;
public int Min = 1;
public int Max = 1;
public string Title = "Make a choice";
public List<T> Choices { get; protected set; }
public T FirstChoice { get
{
if (Choices.Count > 0)
return Choices[0];
throw new IndexOutOfRangeException();
}
}
// Can be overriden to allow for better string representations
protected virtual string OptionString(T option)
{
return option.ToString();
}
// Is the choices the user made valid?
protected virtual bool Verify(List<T> choices)
{
if (choices.Count < Min)
return false;
if (choices.Count > Max)
return false;
return true;
}
// A function that throws up a simple console interface to pick from the list
public override void ConsoleResolve() {
// ...
}
public bool Resolve(List<T> choices)
{
if (Verify(choices))
{
Resolved = true;
Choices = choices;
} else
{
Resolved = false;
}
return Resolved;
}
}
// Make a choice between objects (OID stands for Object ID)
// Objects can be cards, permanents, anything in the game. So this is probably the
// most used choice class.
public class OIDChoice : Choice<OID>
{
protected override string OptionString(OID option)
{
MTGObject obj = MTG.Instance.objects[option];
string str = obj.attr.name + " -";
foreach (var cardType in obj.attr.cardTypes)
{
str += " " + cardType.GetString();
}
return str;
}
}
After writing this I realised that OIDChoice
is basically all the choices a player would need to make. Pretty much every choice is a choice between objects, other than a few I can think of:
- Choice between counters (Heartless Act)
- Choice between colours (Black Lotus)
- Choice between players (Ancestral Recall)
- Choice between all possible card names/types (Runed Halo)
- Choice between permanent or player (Lightning Bolt)
- Choice between modal options (Crux of Fate)
- Choice between sides of a modal DFC (Spikefield Hazard // Spikefield Cave)
- Choice between the sides of the many flavours of split cards (Expansion // Explosion)
Ok that's quite a few actually. I'll get to them when I get to them 😅
However, there is a complex choice that I haven't mentioned - the choice of what to do when you have priority.
Priority Options
Here's the PriorityChoice
class which automatically fills it's option list with what the player could be currently doing.
// A struct to define the different things a player could do with priority
public struct PriorityOption
{
public enum OptionType
{
CastSpell,
ActivateAbility,
PassPriority,
ManaAbility,
// Special Actions
PlayLand,
TurnFaceUp, // man I hate morph
ExileSuspendCard,
RetrieveCompanion
// Special actions defined in rules 116.2c-d are very rare.
// I won't be implementing them until needed.
}
public OptionType type;
public OID source;
public override string ToString()
{
// OptionType.GetString is an extension method defined elsewhere
// Take a wild guess at what it does.
string s = type.GetString();
if (source != null)
{
s += " -> " + MTG.Instance.objects[source].attr.name;
}
return s;
}
}
public class PriorityChoice : Choice<PriorityOption>
{
// Read the board state and populate the option list with
// what the current player can do.
public PriorityChoice ()
{
Min = 1; Max = 1;
Title = "Choose what to do with your priority!";
var mtg = MTG.Instance;
var player = mtg.players[mtg.turn.playerPriorityIndex];
// What can a player do when they have priority?
Options = new List<PriorityOption>();
// They can pass, of course
Options.Add(new PriorityOption
{
type = PriorityOption.OptionType.PassPriority
});
// 117.1a A player may cast an instant spell any time they have priority. A player may cast a noninstant spell during their main phase any time they have priority and the stack is empty.
// TODO - Stuff outside the hand can also be cast in certain situations
foreach (var oid in player.hand)
{
var cardtypes = mtg.objects[oid].attr.cardTypes;
// TODO - This is an oversimplification
if (cardtypes.Contains(MTGObject.CardType.Land))
continue;
// TODO - Also an oversimplification :) (for the card type check at least)
if (!mtg.CanCastSorceries && !cardtypes.Contains(MTGObject.CardType.Instant))
continue;
Options.Add(new PriorityOption
{
type = PriorityOption.OptionType.CastSpell,
source = oid,
});
}
// 117.1b A player may activate an activated ability any time they have priority.
// ...
// 117.1c A player may take some special actions any time they have priority.A player may take other special actions during their main phase any time they have priority and the stack is empty.See rule 116, “Special Actions.”
foreach(var oid in player.hand)
{
var cardtypes = mtg.objects[oid].attr.cardTypes;
if (mtg.CanCastSorceries && cardtypes.Contains(MTGObject.CardType.Land))
{
// TODO - One land per turn. Need the events log and a land drop total system
Options.Add(new PriorityOption
{
type = PriorityOption.OptionType.PlayLand,
source = oid
});
}
}
// 117.1d A player may activate a mana ability whenever they have priority, whenever they are casting a spell or activating an ability that requires a mana payment, or whenever a rule or effect asks for a mana payment(even in the middle of casting or resolving a spell or activating or resolving an ability).
// ...
}
}
Here's how it looks in action (with the turn system that I will go into next post)
You can play some lands, cast your cards or pass without doing anything. Perfect!
Now on to how the program actually makes and resolves these choices.
Game loop
The end goal for the program is a library that I can plug into anything, allowing an external program like Unity to read the board state and display it. However, an external program will not have time to wait for MTGLib to finish doing whatever it's doing - the main game loop needs to run in a separate thread. But then how do you communicate with that thread?
Have a photo of my vague notes from when I was trying to work out how all this is going to work.
Once the game loop thread starts, the main thread waits for a signal that there is a new choice to be resolved. Once there is, it wakes up and resolves that choice, however it wants. It doesn't matter if it's Unity generating some user interface, a bot is automatically deciding what to do, or if I'm just running unit tests - all MTGLib cares about is that the choice gets resolved.
Once the choice is resolved, the main thread pushes the resolved choice to MTGLib and sends another signal to wake it up. Now MTGLib can continue until there is a new choice to be made!
Here is the code for the main thread:
// Create the MTG object, with two new libraries of cards.
var mtg = new MTG(lib1, lib2);
mtg.Start();
// Start the game loop
Thread gameLoopThread = new Thread(mtg.GameLoop);
gameLoopThread.Start();
while (true)
{
// Wait until there is a new choice
mtg.ChoiceNewEvent.WaitOne();
// We need to resolve this choice, for this example just
// call the ConsoleResolve helper function to provide a
// command line input.
Choice choice = mtg.CurrentUnresolvedChoice;
choice.ConsoleResolve();
// Send the resolved choice to the game loop
mtg.ResolveChoice(choice);
}
And here is the code in the MTG object, in the game loop thread:
// Signal when there is a new choice
public EventWaitHandle ChoiceNewEvent = new EventWaitHandle(false, EventResetMode.AutoReset);
// Signal when the current choice has been resolved
public EventWaitHandle ChoiceResolvedEvent = new EventWaitHandle(false, EventResetMode.AutoReset);
// The current unresolved choice, with a thread-safe lock
private Choice _unresolvedChoice;
private object _unresolvedChoiceLock = new object();
public Choice CurrentUnresolvedChoice {
get
{
lock (_unresolvedChoiceLock)
{
return _unresolvedChoice;
}
}
private set
{
lock (_unresolvedChoiceLock)
{
_unresolvedChoice = value;
}
}
}
// Add a new unresolved choice - to be called from inside the game loop
public void PushChoice(Choice choice)
{
if (choice.Resolved)
throw new ArgumentException("This choice is already resolved.");
if (!Headless)
choice.ConsoleResolve();
else
{
CurrentUnresolvedChoice = choice;
ChoiceNewEvent.Set();
}
// Loop until event is received and choice is resolved.
while (true)
{
if (CurrentUnresolvedChoice.Resolved)
break;
ChoiceResolvedEvent.WaitOne();
}
}
// Take in the current resolved choice - to be called from the main thread
public void ResolveChoice(Choice choice)
{
if (!choice.Resolved)
throw new ArgumentException("This choice is not resolved.");
CurrentUnresolvedChoice = choice;
ChoiceResolvedEvent.Set();
}
So during the game loop, the program just needs to call PushChoice
whenever it wants to prompt the user for a choice, and execution blocks until that choice is made. For example, here is the Player.Discard
function doing exactly that:
public void Discard(int count = 1)
{
if (count < 1) return;
if (count < hand.Count)
{
// Construct the choice of what cards to discard
OIDChoice choice = new OIDChoice
{
Options = new List<OID>(),
Min = count,
Max = count,
Title = $"Discard {count} card(s)."
};
choice.Options.AddRange(hand);
// Present the choice
MTG.Instance.PushChoice(choice);
// Discard each card chosen
foreach (var card in choice.Choices)
{
MTG.Instance.MoveZone(card, hand, graveyard);
}
} else
{
// If we are discarding more cards than we have, don't bother with the choice
// Discard each card in hand
foreach (var card in hand)
{
MTG.Instance.MoveZone(card, hand, graveyard);
}
}
}
Thanks for reading!
This thing is beginning to look like a game of sorts. Next post I'll talk about implementing steps and phases and Magic's weirdly awkward system of priority.