I see a lot of requests on how to create simple games (such as a Tic Tac Toe game), and more than often the examples given consist of a big portion of messy code. This mess is usually hard to understand, even harder to debug and downright impossible to maintain, should you wish to make any changes.
Of course, for a simple game that isn't really that important, but the idea of creating a simple game is usually to learn something. Teaching people to write structured code that is simple to understand (and thus: simple to maintain and debug) is much better than slapping together some code that does what this person wants without actually knowing how it works.
So, I decided to build a quick and simple Tic Tac Toe game using Object Oriented Programming concepts (using multiple classes instead of slapping all code in the Form_Load event). I hope this will teach people the basics of OOP with a simple to understand game.
[1] About the game
The Tic Tac Toe game should be familiar to everyone. There is a grid of (usually) 3x3 tiles (although a different number of tiles is possible too) and there are two players. One player plays with crosses (X) and the other with circles (O). Players place their symbol (X or O) on a free tile and try to fill a full row, column or diagonal with their own symbols.
I have emphasized some keywords. You will see that these keywords will become classes in our game.
In my example, you can choose the name of player 1, the name of player 2, and the number of rows and columns (> 1). To the right of the playing grid, a
GroupBox with some labels indicates (with a blue background color) whose turn it is. The GroupBoxes also show the name and score of each player.
The current player can set his symbol by clicking a tile in the grid. Tiles will highlight when the mouse hovers over them.
When a player has won (or when the game has drawn), the score is updated and a new game starts. Alternatively, the Restart button can be used to restart the game. Both scores will be reset to 0 when the Restart button is pressed.
[B][2] The classes
I'll start with a very brief summation of the classes and what they are used for.
The Player class is the easiest: it is used to store the name, score and playing symbol of each player.
The Tile class represents a single tile on the grid. It has some properties that determine its position (such as the X, Y coordinates, and the
Location property), and it can contain a symbol (X, O or empty). There are also some properties that deal with its appearance, such as the BackColor and HotTrackColor properties; these are not very important for the example, but they are used in the Draw method.
Finally, the Grid class represents the playing grid. Intuitively it should contain some kind of collection of Tile objects, and indeed it has a 'Tiles' property that holds all tiles. It also has some properties that determine the number, and separation of the tiles.
Important to note here is that Grid inherits the Panel control. This makes it a Panel, so we can drag it on our form later.
The Grid class has some interesting methods as well. The GetRowValue, GetColumnValue and GetDiagonalValue_.._.. functions will be discussed below; they are important for checking for a winner.
The XyToIndex function converts a coordinate (x, y) into an integer index corresponding to the index at which tile (x, y) can be found in the Tiles list. In essence, it provides to translation between the one-dimensional List(Of Tile) and the two-dimensional grid.
The GetTileFromPoint function returns the Tile that is found under some point. This is used when the mouse is clicked on the grid. Since the Tiles are not actually objects on the grid (they only exist 'in memory', not really like a Button on a Form), they cannot be clicked directly, so we need to find the clicked tile by trying all of them (or at least until we have found the correct one).
The OnMouseClick method is automatically called when the mouse is clicked on the grid. The clicked tile is determined using the GetTileFromPoint method, and a custom TileClicked event is raised, which also passes the clicked Tile in its EventArgs.
The OnMouseMove method is used to re-draw the highlighted tile.
The OnPaint method is automatically called when the grid needs re-painting, and it simply paints each tile.
Furthermore, the actual game implementation (such as creating players, keeping their score and checking for a winner) is done in the frmPlay form. This is all straightforward except for the win-check, so finally:
[3] How the win-check works
The most important, and probably most complicated in this example, bit for a Tic Tac Toe game is probably the win-check. How can a game determine if there are only crosses on a row or column?
The win-check may seem complicated at first, but the concept is very easy.
We check for a winner by checking the rows first, then the columns and finally the two diagonals (the order does not matter of course, but that is the order I have chosen).
When checking a row, column or diagonal, I simply iterate through all tiles in that row, column or diagonal (usually 3, may be different, so let's call the number of rows/colmns n). For each tile that contains a Circle (O), I increment a counter by 1. For each tile that contains a Cross (X), I decrement the same counter by 1. The final counter value (after each tile in the row/column/diagonal) could be anything between -n and +n.
If it is +n, that means all tiles incremented the counter, which in turn means that all tiles contained a Circle (O), so the player using circles has won.
If it is -n, that means all tiles decremented the counter, which in turn means that all tiles contained a Cross (X), so the player using crosses has won.
Any other value means that either there are different kinds of symbols, or there are still empty tiles, so nobody has won.
In the Grid class, there is a function GetRowValue that returns the value of this counter for the specified row index. There is also a function GetColumnValue that returns the counter value for a specific column, and there are two functions GetDiagonalValue_.._.. that return the value for the two diagonals.
All of these functions use the same concept. They loop through the Tiles list in a 'clever' way. For example, the GetRowValue (which is the easiest) starts at the index corresponding to coordinate (X=0, Y=row) (= left-side of the row), and loops, in steps of 1, to the index corresponding to coordinate (X=n-1, row) (= right-side of the row). It checks the Symbol property of each of the tiles visited in the loop.
The GetColumnValue function starts looping at index (X=col, Y=0) (= top of the column) and ends at (X=col, Y=n-1) (= bottom of column). This loop however cannot use a stepsize of 1 because then we would visit way too many loops. We need to skip exactly n tiles before we end up in the same column, but one row down, so we need to use a step size of n.
The following image should help to visualize this. There are 3 steps needed to get to the next row in the same column (since n = 3):
I hope you can see now why the GetDiagonalValue_.._.. functions use step sizes of n + 1 (for \ direction) and n - 1 (for / direction).
Notice especially that these functions do NOT handle any 'winning' whatsoever. They are just here so someone using this class can determine whether someone has won.
The actual check is done in the play form (frmPlay), in the TileClicked event. For each loop, row and diagonal, the Get...Value function is called. The value it returns is checked against the row/column count (TileCount). If the absolute value of this value (-x becomes x) is equal to the row/column count, then someone has won. But how can we determine who has won? That's easy: the player that placed this last symbol. That player is simply the current player (in the code indicated by Turn).
-----------------
Well, that's it (at least for now). I have attached the example project. Since I only have Visual Studio 2008, I could not create a project for earlier versions. Anyone with an earlier version will have to add the classes and forms manually. I cannot guarantee that it will work then, I might have used some 2008-only (or .NET Framework 3.5-only) code. If it doesn't work post a reply and I'll see how we can fix it.
Since I had to remove the bin/obj folders, you may need to Build the solution (or just run it) before opening the form designers.