Monday, October 25, 2010

Forge's Architecture or "How to program your own Magic engine" - Part 2

In Forge each player's life and zones (hand, library, battlefield, exile, stack) are all global variables. Much of the user interface and card code needs to access this information, so instead of passing the data everywhere, I just made it global.

GameAction is a loose collection of methods:

drawCard(String player),
moveToGraveyard(Card c)
discard(Card c)
discardRandom(String player)
sacrifice(Card c)
destroy(Card c)
destroyNoRegeneration(Card c)
shuffle(String player)
getOpponent(String p) : String
isCardInPlay(Card c) : boolean

Most of the code in GameAction could be done by the individual card code but putting all of the code in one place promotes code reuse and DRY (don't repeat yourself). GameAction at first only had newGame() and drawCard(String player). The other methods were added as needed.

GameAction.checkStateEffects() checks for state effects like "should a creature go to the graveyard" and the "legendary rule". At first I had no idea how to implement ongoing state effects like Glorious Anthem but then I hacked this method, voila! At first I thought I was bending the rules but this is completely correct after all.

(Forge has GameActionUtil which helps GameAction and is made up of static methods. GameActionUtil continues Forge's tradition of huge classes is 1/2 MB of code. And no that isn't Forge's largest class, CardFactory is a cool 1 MB all by itself. 1 MB of code is absolutely huge and is as long as a novel.)

Playing a spell or ability seems easy but there are many checks that you need to do. When choosing targets for a spell or ability, first you have to check if the chosen card has protection from that spell. Then each time a targeted spell/ability resolves, it has to check "is this card still on the battlefield" by GameAction.isCardInPlay(Card c). (I wrote this back when the battlefield was called "in play".)


The Input class implements the State Pattern and handles all of the mouse clicks. Input is used to implement phases and choosing targets. Initially the Input class can be confusing but if you look at the code, you should be able to understand my design.

Input is just an interface (abstract class). The getMessage() method would return something like "Main1" or "Choose target creature to receive 2 damage".
interface Input
  String  getMessage();
  void    clickCard(Card c, Zone z);
The class InputControl is attached to the user interface and is just a "wrapper" for Input.
class InputControl
  private Input i;

  void setInput(Input input)
      i = input;
      String s = i.getMessage();
      //display s on the user interface (gui)

  void clickCard(Card c, Zone z)
      i.clickCard(c, z);
Hopefully you can see how InputControl changes. InputMain.getMessage() would return "Main Phase" and InputMain.clickCard() would allow the player to play a card in his hand or an activated ability on a creature on the battlefield. Input is also used to pay mana costs, to mulligan at the beginning of the game and to declare attackers and blockers.

Since InputControl.setInput() always calls Input.getMessage() first, you can put hacky stuff in getMessage(). Planeswalkers were introduced to Magic after I had much of Forge written and I was wondering if I could add them somehow. I put a bunch of hacky code in Input.getMessage() to restrict the player to only using one ability per turn. Also at that time Forge didn't even have the concept of loyalty counters, so I just used an int. Finally I just created another Combat object to simulate planeswalker combat. (For a long time if the AI had two planeswalkers, you could only attack the first one.)

A more complete Input class would also have clickPlayer(String player) and clickManaPool(). Technically the Input class does not process all of the mouse clicks since dialog boxes are used to get additional information from the player. The Input class could be modified to include dialog boxes which would be more "unifying" versus allowing the card code create dialog boxes at will.

The Input class may not seem very important but it was one of the first, big problems that I faced. I have no idea how Shandalar or other Magic programs process the mouse. Without stumbling onto the state pattern, I have no idea how badly I would have coded an inferior Input class.

Well that covers the main classes in Forge: Card, SpellAbility, Input, and GameAction. Forge has numerous other classes like CardList but these are the most important ones. If you understand these 5 classes, you can write your own Magic rules engine.


p.s. Feel free to post any further technical questions about Forge's architecture.


Kite_ said...

I've been thinking about making a program to draft with 8 ppl online(for freeeee).
But I have no idea how to do the online part. How to enable 8 ppl to connect to each other ect.
So I'll probably start with making the rules engine. Which'll be needed anyways.
Great articles!

Forge said...

The networking stuff can be very complicated. Find a computer language that you understand and then read everything that you can find about how it implements networking.

I think the cool part of making an online drafting application is to output the draft into a Magic Online format, so you guys could play against each other. Or you could use the free program Incantus to play against each other.

nantuko84 said...

the networking stuff is very important and not so easy. and for sure creating online version can make any program more popular. but I would like also to write that implementing rules enforcement application is not so easy as you may expect at the beginning. you need to implement network, ai, to know comprehensive rules very very good, think over your architecture at the very beginning not to rewrite almost everything later, and don't forget that you will need to adv your project somehow, add documentation, add news cards every 3 months. I spent hundreds hours for my project and I know what all this mean.