CGS Version 3.0
27.01.2010 • 22:51 • permalink • Comments (0)
I've been meaning to continue my compiler series for a few weeks, but performance issues and general doubt about how best to structure the code has resulted in some delays. I suppose I'm also guilty of jumping forward to the fun bits and as a result I've finished a version of the Card Game Simulator using the compiler. As it stands, it's version 3.0 but I'll incrementally add more features and card games as I move forward (Currently, I'm adding the functionality required to support the game Spider).
Apart from having it's guts totally replaced, the application has also recieved a graphical overhaul. It now features more and better animations and a new card design. The cards look a bit similar at the moment but I'll be making them a more distinguishable in the future. I'm still trying to figure out the best way to represent the face cards without ruining the style. As you can see I'm going for a worn, kind of old school look.
You can try out the new version here. If you feel like it, you can also compare and contrast with the old version 2. As I mentioned, there are some performance issues with the new version and it actually loads games much slower than the old one. It appears that the strategy I'm using for the compiler is not very efficient and it already begins to show when compiling games of a few hundred lines of code, possibly due the massive amounts of recursion or the heavy use of reflection. When time permits, I'll scour the web for a good profiler and see if it seems fixable.
Writing Clean Code
09.01.2010 • 18:19 • permalink • Comments (0)
Every once in a while, you come across a book that really resonates with you. For me, this recently happened with the book Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin and Co. The book is all about recognizing, writing and modifying readable and maintainable code, from an agile perspective with a particular focus on tests and test-driven development. The message it brings is quite simple: It is not enough to create working code in order to succeed in the long run, you need to build high quality software or be crushed under the weight of code that is hard if not impossible to modify and maintain. The proposed solution is a series of craftsmanship principles and best practices, backed by plenty of examples.
Clean Code is one of the best programming books I've read in a long while. It is based on many of the same guiding principles I have for software development, such as the central role of readable code and the need to continually improve yourself as a craftsman, beyond the standard requirements of your job.
Some of the practices presented in the book weren't new to me and, indeed, some of them were a bit too idealistic, but apart from that it was truly an insightful read.
One of the more eye opening concepts was a way to write code so that it can be read much like you would read a newspaper. The first step in this process is to separate your code into lots of small functions. Essentially there should be no functions larger than about 4 lines and they should do one thing and one thing only. This thing should be captured accurately and unambiguously in the function name. A function should read like a sentence - to do x (the function name), you must do a, b and c (lines of code). Note that it is important that the contents of the function should be at the same level of abstraction and that this level of abstraction should be one level lower than that of the function name. The point to all of this is to make it trivially easy to see what a function does. Consider the following code I recently wrote for resizing images:
- public enum ResizeStrategy
- {
- Stretch,
- BestFit,
- }
- public static class ImageFormatter
- {
- public static Image ScaleImage(Image originalBitmap, int maxDimension)
- {
- float aspectX = maxDimension / (float)originalBitmap.Width;
- float aspectY = maxDimension / (float)originalBitmap.Height;
- float actualAspect = Math.Min(aspectX, aspectY);
- int sourceWidth = (int)(originalBitmap.Width * actualAspect);
- int sourceHeight = (int)(originalBitmap.Height * actualAspect);
- var resized = new Bitmap(sourceWidth, sourceHeight);
- Graphics g = Graphics.FromImage(resized);
- g.DrawImage(originalBitmap, new Rectangle(0, 0, resized.Width, resized.Height));
- return resized;
- }
- public static Image ScaleImage(Image originalImage, float scale)
- {
- int width = (int)(originalImage.Width * scale);
- int height = (int)(originalImage.Height * scale);
- var resized = new Bitmap(width, height);
- Graphics g = Graphics.FromImage(resized);
- g.DrawImage(originalImage, new Rectangle(0, 0, resized.Width, resized.Height));
- return resized;
- }
- public static Image ScaleImage(Image originalImage, float scaleWidth, float scaleHeight)
- {
- int width = (int)(originalImage.Width * scaleWidth);
- int height = (int)(originalImage.Height * scaleHeight);
- var resized = new Bitmap(width, height);
- Graphics g = Graphics.FromImage(resized);
- g.DrawImage(originalImage, new Rectangle(0, 0, resized.Width, resized.Height));
- return resized;
- }
- public static Image ResizeImage(Image originalImage, int width, int height, ResizeStrategy thumbnailFormat)
- {
- Image thumbnail = new Bitmap(width, height);
- switch (thumbnailFormat)
- {
- case ResizeStrategy.Stretch:
- RenderStretchedThumbnail(originalImage, thumbnail);
- break;
- case ResizeStrategy.BestFit:
- RenderBestFitThumbnail(originalImage, thumbnail);
- break;
- default:
- throw new ArgumentException("Thumbnail format not valid");
- }
- return thumbnail;
- }
- private static void RenderBestFitThumbnail(Image originalImage, Image thumbnail)
- {
- float aspectX = thumbnail.Width / (float)originalImage.Width;
- float aspectY = thumbnail.Height / (float)originalImage.Height;
- float actualAspect = Math.Max(aspectX, aspectY);
- int sourceWidth = (int)(thumbnail.Width / actualAspect);
- int sourceHeight = (int)(thumbnail.Height / actualAspect);
- Graphics g = Graphics.FromImage(thumbnail);
- g.DrawImage(originalImage, new Rectangle(0, 0, thumbnail.Width, thumbnail.Height), new Rectangle(0, 0, sourceWidth, sourceHeight), GraphicsUnit.Pixel);
- }
- private static void RenderStretchedThumbnail(Image originalImage, Image thumbnail)
- {
- Graphics g = Graphics.FromImage(thumbnail);
- g.DrawImage(originalImage, 0, 0, thumbnail.Width, thumbnail.Height);
- }
- }
I don't consider this code bad in any particular way, but lets see what happens if we rewrite it, using the above guidelines:
- public enum ResizeStrategy
- {
- Stretch,
- BestFit
- }
- public class ImageFormatter
- {
- private Image _workingImage;
- private Image _originalImage;
- public Image ResizeImage(Image originalImage, int width, int height, ResizeStrategy resizeStrategy)
- {
- _originalImage = originalImage;
- PrepareImage(width, height);
- switch (resizeStrategy)
- {
- case ResizeStrategy.Stretch:
- RenderStretchedImage();
- break;
- case ResizeStrategy.BestFit:
- RenderBestFitThumbnail();
- break;
- default:
- throw new ArgumentException("Thumbnail format not valid");
- }
- return _workingImage;
- }
- private void PrepareImage(int width, int height)
- {
- _workingImage = new Bitmap(width, height);
- }
- private void RenderStretchedImage()
- {
- Graphics g = Graphics.FromImage(_workingImage);
- g.DrawImage(_originalImage, GetTargetDrawingRectangle());
- }
- private Rectangle GetTargetDrawingRectangle()
- {
- return new Rectangle(0, 0, _workingImage.Width, _workingImage.Height);
- }
- private void RenderBestFitThumbnail()
- {
- float scale = CalculateMaximumScaleNotCroppingBothDimensions();
- Rectangle source = CalculateRectangleFromScaledWorkingImage(scale);
- RenderPartialImage(source);
- }
- private float CalculateMaximumScaleNotCroppingBothDimensions()
- {
- float xScale = _workingImage.Width / (float)_originalImage.Width;
- float yScale = _workingImage.Height / (float)_originalImage.Height;
- return Math.Max(xScale, yScale);
- }
- private Rectangle CalculateRectangleFromScaledWorkingImage(float scale)
- {
- int width = (int)(_workingImage.Width / scale);
- int height = (int)(_workingImage.Height / scale);
- return new Rectangle(0, 0, width, height);
- }
- private void RenderPartialImage(Rectangle source)
- {
- Graphics g = Graphics.FromImage(_workingImage);
- g.DrawImage(_originalImage, GetTargetDrawingRectangle(), source, GraphicsUnit.Pixel);
- }
- public Image ResizeImage(Image originalImage, int maxDimension)
- {
- _originalImage = originalImage;
- float scale = CalculateScaleForFixedMaxDimension(maxDimension);
- return ScaleImage(originalImage, scale);
- }
- private float CalculateScaleForFixedMaxDimension(int lengthOfMaxDimension)
- {
- float xScale = lengthOfMaxDimension / (float)_originalImage.Width;
- float yScale = lengthOfMaxDimension / (float)_originalImage.Height;
- return Math.Min(xScale, yScale);
- }
- public Image ScaleImage(Image originalImage, float scale)
- {
- _originalImage = originalImage;
- PrepareScaledImage(scale);
- RenderStretchedImage();
- return _workingImage;
- }
- private void PrepareScaledImage(float scale)
- {
- int width = (int)(_originalImage.Width * scale);
- int height = (int)(_originalImage.Height * scale);
- _workingImage = new Bitmap(width, height);
- }
- public Image ScaleImage(Image originalImage, float widthScale, float heightScale)
- {
- _originalImage = originalImage;
- PrepareScaledImage(widthScale, heightScale);
- RenderStretchedImage();
- return _workingImage;
- }
- private void PrepareScaledImage(float widthScale, float heightScale)
- {
- int width = (int)(_originalImage.Width * widthScale);
- int height = (int)(_originalImage.Height * heightScale);
- _workingImage = new Bitmap(width, height);
- }
- }
One of the first thing you might notice is that the code has grown from 75 lines to 117. This is just a price you have to pay, but I believe that readability should almost always outweigh brevity.
In the first method, I violate the principle of function size, but that is, unfortunately, necessary when using switch statements. You'll notice that the actions performed in the function is relegated to other functions, which is the main way this technique requires code to be structured.
The functions are topologically sorted, such that a given function is directly followed by the functions used in its body, which again are followed by their own dependencies, etc. Think of it as a stack that, as you read through the file, pushes the most relevant functions to the top as needed. Thus, the first ResizeImage function is followed by the function PrepareImage, which is its first dependency. If this function had had any dependencies, they would have followed immediately after before continuing with the dependencies of ResizeImage.
Apart from creating a very literate piece of code, the transformation also revealed a few cases of repeated code which was turned into reusable functions. Where before I might not have bother with looking for such similarities in the code, it becomes more obvious when splitting everything into its essential parts.
When I read about this approach, it struct me as a very clever way to structure your code, but there are a few caveats that I'm not too sure about, yet. I find that the resulting code can be a bit overwhelming to look at as its structure has become more homogeneous and, as mentioned above, there are just more lines of code to read. It might help, though, if the IDE would highlight the public functions, so you could quickly navigate to the part of the code you need. Regardless of these issues, I find the approach very interesting and I'm looking forward to seeing how well it works for me.
If you found the technique interesting, I'm sure you'll love the book, as it's filled with little nuggets like this.
Resizer 1.2 - More Goodness
27.12.2009 • 11:59 • permalink • Comments (0)
Once again, I've felt the need to upgrade my image resizing application, and I present to you version 1.2! The app seems to follow a natural evolution as I come up with more and more requirements for it, this time because I decided to put more images into my blog posts. These images must be no wider than 450 pixels so I thought it would be useful to have a feature specifying which dimension to resize after, instead of it being the biggest dimension.
Version 1.2 changelog:
- Added options for setting length of the minimum, vertical and horizontal dimensions, instead of just the maximum dimension.
- The resizing work is now done in a background thread making the UI responsive while working.
- Added support of cancelling work.
- Removed a bug preventing the progress bar from being updated properly.
Recently, I've been very interested in architectural patterns for improving modularity, testability and other such desirable properties. The Model View Presenter pattern (The Supervising Controller variant) is one of these, and after reading an introduction to it by Phil Haack, I've been playing around with it for the GUI of my card game compiler. It provides a great way to keep the UI classes very thin and only deal with UI business, allowing us to maintain the single responsibility principle.
While it was hardly necessary, I implemented the MVP pattern for the single view in the Resizer app but I'm beginning to get the feeling that it will grow a lot in the future.
For those who are interested in architecting better apps, the Composite Application Guidance for WPF (Code named Prism) is also a good read. Actually, it's more than just guidance, it's also a library that can help you get up to speed on these practices in no time and it includes very helpful code samples for all the important points. The project comes out of the Microsoft Patterns & Practices department which provides guidance for all sorts of .NET development.
As usual, you can get the source code here and the binary here.
Compiler Series Part II: Compiler Basics
25.12.2009 • 23:08 • permalink • Comments (0)
This is the second 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.
In this article I'll introduce the concepts used as building blocks for the compiler and how they can be composed to match complex textual patterns. At the end of this article we'll be able to parse ordered bits of text and turn them into managed objects.
There are a number of ways to approach creating a compiler, but the one we'll be exploring here is a simple recursive decent parser where the grammar for the language is encoded in C# code rather than being described using formats such as EBNF. This is not really a theoretical series so I won't be going into detail on the type of compiler or discuss topics such as formal semantics, just the practical bits.
The main concept I'm building my compiler around is a delegate for parsing source code which returns a parse result of an arbitrary C# type. A number of these can then be composed together to match complex patterns and create a whole object graph. This concept is not something I've invented myself, it's based on a blog post by Louis DeJardin, the guy behind the Spark View Engine for ASP.NET MVC. This post is very similar to Louis', which can be found here, but in the next article and forward we use it for our own specialized grammar.
To get started we'll define the interfaces of the basic concepts we're going to need:
- public delegate ParseResult<TValue> ParseAction<TValue>(Position position);
- public class ParseResult<TValue>
- {
- public Position Rest { get; }
- public TValue Value { get; }
- public ParseError Error { get; }
- }
- public class Position
- {
- public int Line { get; }
- public int Column { get; }
- public string Context { get; }
- public char Peek();
- public string Peek(int length);
- public Position Advance(int offset);
- }
- public class ParseError
- {
- public int Line { get; }
- public int Column { get; }
- public string Message { get; }
- }
The ParseAction delegate is a function which, given a position in the source code, yields either a result of the generic type or a parse error if the pattern did not match. The result of the parser is stored in a ParseResult object which hold either of the possible outcomes. It also contains the position in the source code where a subsequent parser would start off from. The Position object is an immutable description of a point in the source code and it can potentially wrap a source that is more complex than a string if needed.
Using these classes we can begin to create a Grammar class where we define the most basic patterns we want to match.
- public class Grammar
- {
- public static ParseAction<char> Ch(char match)
- {
- return delegate(Position input)
- {
- if (input.Peek() == match)
- return new ParseResult<char>(input.Advance(1), match);
- return new ParseResult<char>("Encountered unexpected char", input.Line, input.Column);
- };
- }
- public static ParseAction<IList<TValue>> Rep<TValue>(ParseAction<TValue> parser)
- {
- return delegate(Position input)
- {
- Position rest = input;
- List<TValue> list = new List<TValue>();
- var results = new List<ParseResult<TValue>>();
- results.Add(parser(input));
- while (!results.Last().HasError)
- {
- list.Add(results.Last().Value);
- rest = results.Last().Rest;
- results.Add(parser(rest));
- }
- return new ParseResult<IList<TValue>>(rest, list);
- };
- }
- public static ParseAction<TValue> Or<TValue>(ParseAction<TValue> parser1, ParseAction<TValue> parser2)
- {
- return delegate(Position input)
- {
- var result1 = parser1(input);
- if (result1.HasError) return parser2(input);
- return result1;
- };
- }
- public static ParseAction<Chain<TValue1, TValue2>> And<TValue1, TValue2>(ParseAction<TValue1> parser1, ParseAction<TValue2> parser2)
- {
- return delegate(Position input)
- {
- var result1 = parser1(input);
- if (result1.HasError)
- return new ParseResult<Chain<TValue1, TValue2>>(result1.Error);
- var result2 = parser2(result1.Rest);
- if (result2.HasError)
- return new ParseResult<Chain<TValue1, TValue2>>(result2.Error);
- var value = new Chain<TValue1, TValue2>(result1.Value, result2.Value);
- return new ParseResult<Chain<TValue1, TValue2>>(result2.Rest, value);
- };
- }
- }
Each of the above methods generate a ParseAction for a specific, useful purpose. The first matches a specific char while Rep matches a repeated sequence of a pattern. Using these, we can write the following code (I've removed the references to the Grammar class from the method calls to simplify the example):
- var parseChar = Ch('c');
- var parseChars = Rep(parseChar);
- var result1 = parseChar(new Position("c"));
- var result2 = parseChars(new Position("cccc"));
As you can see, we used one parser to create another. The Rep function keeps using the Ch('c') parser to match characters, which it stuffs into a List<char>. The methods Or and And does the same for matching one pattern or another, or one pattern followed by another, respectively. As a side note, you can see that I have supplied none of the type arguments for the generic function. This is because of the powerful type inference capability of the C# compiler.
- var parseAnotherChar = Ch('k');
- var parseEitherChar = Rep(Or(parseChar, parseAnotherChar));
- var parseBothChars = Rep(And(parseChar, parseAnotherChar));
- var result1 = parseEitherChar(new Position("kkkckckc"));
- var result2 = parseBothChars(new Position("ckckckckck"));
Here, we define two parsers that matches series of characters. The one using Or matches a sequence where each char can either be c or k where the one using And matches only sequences of cs followed by a k. To end up with more fluent code, we can define a few useful extension methods:
- public static class GrammarExtensions
- {
- public static ParseAction<TValue> Or<TValue>(this ParseAction<TValue> parser1, ParseAction<TValue> parser2)
- {
- return Grammar.Or(parser1, parser2);
- }
- public static ParseAction<Chain<TValue1, TValue2>> And<TValue1, TValue2>(this ParseAction<TValue1> parser1, ParseAction<TValue2> parser2)
- {
- return Grammar.And(parser1, parser2);
- }
- }
- // Allows for the following
- var parseEitherChar = Rep(parseChar.Or(parseAnotherChar));
- var parseBothChars = Rep(parseChar.And(parseAnotherChar));
As you might have noticed, the And parser returns an object of the type Chain. This is simply a container for the results of the two parse actions. However, it introduces the problem of creating increasingly complex result types as your parsers become more complex. To reduce this problem we introduce the Build function which translates a complex result into a simpler one. It basically just takes the result of a parser and applies a function to it.
- public static ParseAction<TValue2> Build<TValue1, TValue2>(ParseAction<TValue1> parser, Func<TValue1, TValue2> builder)
- {
- return delegate(Position input)
- {
- var result = parser(input);
- if (result.HasError) return new ParseResult<TValue2>(result.Error);
- try
- {
- return new ParseResult<TValue2>(result.Rest, builder(result.Value));
- }
- catch (ParseException e)
- {
- return new ParseResult<TValue2>(e.Message, input.Line, input.Column);
- }
- };
- }
- var parseBothChars = And(parseChar, parseAnotherChar).Build(hit => hit.Left + hit.Down);
In this case, we use Build to change the result type from Chain<char,char> to string. Once again, the type inferral of the C# compiler comes to our rescue, providing us with proper intellisense on the properties on the Chain object. This will save your sanity when building from more complex chains. Also notice that we catch ParseException errors, a custom exception type which we can use to signal that some parse rule was broken when trying to build the object.
These, and more, are common building blocks for any grammar and, therefore, our primary job is now to create a Grammar class which holds all the basic parsers. To create a more specialized grammar you simply inherit from Grammar and add new parsers.
We're getting to the end of this article but hopefully you got the idea of how this all works. If not, I urge you to go read up on Louis' blog on the same topic. I've also uploaded some of the code from my compiler so you can see some of the other parsers to put in the Grammar class. The code won't compile as it relies on some other code of mine, but it should be possible to understand it regardless.
In the next article we'll begin to create a more specialized grammar for the CGL language using the building blocks presented here.
Compiler Series Part I: An Overview of CGL
07.12.2009 • 15:23 • permalink • Comments (0)
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:
- game:Solitaire
- 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:
- game:Solitaire
- info "Classic Solitaire game, with one card turned from the pile at a time."
- init sourcepile:mainDeck = GetShuffledDeck();
- 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:
- area:deckArea
- Position = {10, 10};
- <- mainDeck;
- pile:deck
- CardOffset = {1, 10};
- pile
- clicked
- // snip
- clicked
- 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:
- clicked
- if deck.Size != 0
- {
- ReturnFromHand();
- theTurnedCards <- deck : 1;
- theTurnedCards.TopCard.Turn();
- }
- else
- {
- deck <- 1 <- theTurnedCards;
- deck.Turn();
- }
- 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:
- game:Solitaire
- info "Classic Solitaire game, with one card turned from the pile at a time."
- init sourcepile:mainDeck = GetShuffledDeck();
- area:area1
- // snip
- area
- area:area2
- // snip
- area
- end
- // snip
- end
- 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).
- end
- if hearts.Size + spades.Size + diamonds.Size + clubs.Size == 52
- win();
- 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:
- if 10 < deck.Size < 20
- // Equivalent C# version
- 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:
- The source code for a simple solitaire game I've invented, called Gypsy Eye
- Solitaire terminology: http://en.wikipedia.org/wiki/Solitaire_terminology
- List of solitaire games: http://en.wikipedia.org/wiki/List_of_solitaire_card_games
In the next article we'll take a look at how to put together a grammar and parse a piece of source code.