Without Precedence

Compiler Series Part I: An Overview of CGL

by Morten Christiansen on 07-12-2009 at 15:23 | comments [0] | posted in CGS, Programming Languages

This is the first part in a series of articles on implementing a custom .NET compiler for a card game language called CGL. The project page for CGL, and a list of all the articles in the series can be found here.

For you to get a sense of the project I'm undertaking, i'll start out by stepping through the language. At this point in time the language hasn't been fully implemented so there's bound to be a number of changes to it before the end of this project, but I'll make sure to keep this article up to date as a reference document for anyone wanting to learn the language.

The approach taken in CGL is to describe the structure of a card game, as one would see it if observing the game being played with real cards, with snippets of behavior logic imbedded in different areas. In this sense it actually resembles the structure of an XML document, as the game is built up of a tree structure of areas and piles with a number of attributes.

The first bit of code you will need for any game is a game declaration inside which all the content will go:

  1. game:Solitaire
  2.  
  3. game

All the structural declarations follow this syntax, where the name becomes a variable reference you can use later (this becomes relevant with areas and piles).

The first thing found inside the game declaration must always be the info and the init declarations:

  1. game:Solitaire
  2.    info "Classic Solitaire game, with one card turned from the pile at a time."
  3.  
  4.    init sourcepile:mainDeck = GetShuffledDeck();
  5. game

The info declaration specifies a descriptive string which can be presented to the player of the game and has no impact on the game itself. The init declaration is used to declare source piles to be used during the initialization phase of the game. They simply represent the cards available for the setup of the game, and would typically consist of a single shuffled deck. This is the only point at which cards can be created from nothing, as an important principle of the language is to follow the same restraints as a normal game where cards can't just disappear or appear out of nowhere. This should also serve to make the code easier to follow, and not result in very confused players. The init declaration is actually a block which means that it would normally also end with init, but some blocks types doesn't need the closing tag if there is only a single statement in the block.

Moving on, we come to our first part of the actual playing setup. To define common logic for a set of related piles, you group them into an area. Areas doesn't have to match any physical or logical layout but they make it easier to define related types of piles. So lets take a look at the code for declaring an area:

  1. area:deckArea
  2.    Position = {10, 10};
  3.    <- mainDeck;
  4.  
  5.    pile:deck
  6.       CardOffset = {1, 10};
  7.    pile
  8.  
  9.    clicked
  10.       // snip
  11.    clicked
  12. area

This code captures the three types of declarations you can make inside an area: initializers, pile declarations and event handlers. Initializers take the form of property assignments, functions and card movements that apply to all the piles in the area. Property assignments take the form of integer pairs in brackets as they represent positional logic such as pile offsets (between each pile). The arrow <- is an operator for moving cards from one pile to another. It generally has the form of target <- source but when used in initializers the target is implicitly resolved. When using the move operator as an area initializer, the cards are distributed across all its piles. Other ways to use the move operator are introduced later in the article.

After any initializers, an area must always have at least one pile declaration, though it needs not have any body as is the case above. Piles can have most of the same types of initializers as areas, though if a property has been specified in both a pile and its parent area, the pile property either takes precedenc or becomes relative to the property on the area. If more than a single initializer is used in the pile declaration, a closing pile tag is required.

Finally, an area can implement one of two event handlers, clicked and doubleclicked. They are executed each time the user interacts with pile elements of the area and provides access to the three variables Hand, ClickedCard and ClickedPile. The hand is a virtual pile type that also acts much as a card and is used as a means to select one or more cards for performing some action. The other two variables are just what they seem. A clicked event handler could look like the following:

  1. clicked
  2.    if deck.Size != 0
  3.    {
  4.       ReturnFromHand();
  5.       theTurnedCards <- deck : 1;
  6.       theTurnedCards.TopCard.Turn();
  7.    }
  8.    else
  9.    {
  10.       deck <- 1 <- theTurnedCards;
  11.       deck.Turn();
  12.    }
  13. clicked

This code checks whether there are any cards left in the clicked pile (In this case deck and ClickedPile will always refer to the same instance as there is only one pile in the area). If there are, a single card is moved from the top of the pile to the pile theTurnedCards. The additional parameter to the move operator : 1 specifies the number of cards to move. The default is just to move all the cards. ReturnFromHand() takes any cards put into the hand and removes them (Note that they are not actually moved, just no longer registered as being stored in the hand). If the pile is empty, all the cards are returned to the deck pile and turned backside up. Instead of just moving the pile ordinarily, the optional extra pile operator specifies how many cards to move at a time. In this case we only move a single card at a time, effectively reversing the order of the pile.

If we take a look at the game declaration again we get the full picture of how a game can look:

  1. game:Solitaire
  2.    info "Classic Solitaire game, with one card turned from the pile at a time."
  3.  
  4.    init sourcepile:mainDeck = GetShuffledDeck();
  5.  
  6.    area:area1
  7.       // snip
  8.    area
  9.  
  10.    area:area2
  11.       // snip
  12.    area
  13.  
  14.    end
  15.       // snip
  16.    end
  17. game

As you can see, the game consists of some initializations, a number of areas and something called the end block. This block contains code that is executed much like the event handlers inside areas. The difference is that it is executed each time another event has finished to determine if the end conditions for the game has been met. There is nothing special about the code that is performed here except that you cannot move cards (This would effectively mean that cards would move themselves, which users would not expect). You just need to call one of the functions Win() or Lose() to end the game, as shown below (You might not need a condition for losing, but you should always have a winning condition).

  1. end
  2.    if hearts.Size + spades.Size + diamonds.Size + clubs.Size == 52
  3.       win();
  4. end

The philosophy behind the executing code in event handlers and the end block is to keep it as simple and readable as possible and stay close to the concepts of real card games. I want the developer to be thinking in terms of how he would play the game and it should be easy to identify what parts of the game rules a specific line of code represents. Because of these requirements you will find that common operations and control structures are not included in the language such as local variables and looping control flow. Hopefully, all the places you would need these types of constructs, I have managed to create functions and operators that can replace them. The truth is, only through creating many, many card games would I know if this is the case, and I may be forced to introduce them again (the examples both existed in the previous version of CGL, but I found no need for them to stay).

An interesting feature of the language that I haven't found anywhere else (hopefully not for a good reason) is the ability to write the following and having it evaluate as you would logically think:

  1. if 10 < deck.Size < 20
  2.  
  3. // Equivalent C# version
  4. if (10 < deck.Size && deck.Size < 20)

I can't count the number of times that I've cursed other languages for not working this way, so I'm happy to introduce the feature into CGL. It's not that I've actually found a need for it in the language, I just couldn't let the opportunity pass by :) Of course, changing the behavior of known constructs and introducing new operators such as the move operator all makes it harder for people to get into the language, but I hope they prove to be worthwhile. After all, you only have to learn them once but can benefit from them countless times, thereafter.

This should cover the structure of a game and the only thing missing, except perhaps for some semantic details here and there, is a reference documentation for all the properties and functions supported. These are still a bit into the air right now, so I won't add them here but when they're done I'll put them up in their own document and link to them.

Resources:

In the next article we'll take a look at how to put together a grammar and parse a piece of source code.

Comments