Building a Game Engine with Cocoa, Part 2
Pages: 1, 2
Generating Moves
With a spiffier user interface in place, we'll now turn our attention to generating moves for our game. What appears to be the official Lines of Action page lists a few basic rules:
- Black moves first.
- Each turn, the player moves one of his pieces, in a straight line, exactly as many squares as there are pieces of either color anywhere along the line of movement. (These are the "lines of action").
- You may jump over your own pieces.
- You may not jump over your opponent's pieces, but you can capture them by landing on them.
We'll implement a move generator for these rules, but wait until next time when we develop a routine to check for "gameover" to handle the corner cases that may arise.
To get started with move generation, we'll implement a routine that computes the total number of pieces in each "line of action," given a spot on the board. For any given board position, we'll inspect the board from the starting spot in the following manner: up/down, left/right, up/right, then down/left, and finally, up/left, then down/right. Remember, all that matters is the total number of pieces in each of those four lines, so we'll only be returning a total of four values.
To make our lives a little easier, we'll introduce a helper struct to encapsulate these four values, which we can pass off to another routine that will use this information to help generate the actual moves. Here are the necessary additions so far:
typedef struct {
int up_down;
int left_right;
int up_right_down_left;
int up_left_down_right;
} LineCounts;
- (LineCounts)lineCountsForSpot:(NSPoint)spot {
//up down
int up_down = 0;
int y;
for (y=0; y < DIMENSION; y++)
if (board[(int)spot.x][y] != EMPTY)
up_down++;
//left right
int left_right = 0;
int x;
for (x=0; x < DIMENSION; x++)
if (board[x][(int)spot.y] != EMPTY)
left_right++;
//up right, then down left
int up_right_down_left = 0;
x=spot.x; y=spot.y;
while (x < DIMENSION && y < DIMENSION) {
if (board[x][y] != EMPTY)
up_right_down_left++;
x++; y++;
}
x=spot.x; y=spot.y;
while (x >= 0 && y >= 0) {
if (board[x][y] != EMPTY)
up_right_down_left++;
x--; y--;
}
//remove duplicate count
if (board[(int)spot.x][(int)spot.y] != EMPTY)
up_right_down_left--;
//up left, then down right
int up_left_down_right = 0;
x=spot.x; y=spot.y;
while (x >= 0 && y < DIMENSION) {
if (board[x][y] != EMPTY)
up_left_down_right++;
x--; y++;
}
x=spot.x; y=spot.y;
while (x < DIMENSION && y >= 0) {
if (board[x][y] != EMPTY)
up_left_down_right++;
x++; y--;
}
//remove duplicate count
if (board[(int)spot.x][(int)spot.y] != EMPTY)
up_left_down_right--;
LineCounts lc = {
.up_down = up_down,
.left_right = left_right,
.up_left_down_right = up_left_down_right,
.up_right_down_left = up_right_down_left
};
return lc;
}
Using the information provided by the LineCounts struct and the board, we can easily start at a given spot and search in each of the eight possible directions to determine if any moves are available. For each direction, we only need to ensure a few conditions:
- We don't go off of the edge of the board
- The opponent doesn't stand between a piece and its destination
- The piece doesn't land on one of its own pieces when moving
Each of these conditions can be wrapped up as a piece of a large conditional logic statement; in our case, we even use some of the recently discussed macros to make the code a bit tidier and readable. The only remotely challenging of these three conditions is detecting when opponents stand between a piece and its destination, so it's nice to wrap this one up in its own routine. A snippet of one possible approach, opponentInLineBetweenCoord:andCoord:, is shown below and illustrates vertical detection of an opponent. All seven of the other directions work exactly the same way.
if (c1.x == c2.x) {
if ((int)(c2.y-c1.y) > 0) { //up
int dy;
for (dy=c1.y; dy < c2.y; dy++)
if (OPPONENT_AT(c1.x,dy))
return YES;
}
else {
int dy;
for (dy=c1.y; dy > c2.y; dy--)
if (OPPONENT_AT(c1.x,dy))
return YES;
}
}
One other detail associated with move generation involves the desire to pass back the NSArray from the validMovesFromSpot: function. Since NSArrays only house objects descended from NSObject, we won't be able to directly add our NSPoint structs to it. Apple, however, realized the convenience of passing around some of the most common structs and provides another class, NSValue, which we can use to wrap NSPoints whenever we'd like to add them to an array. This is precisely what we do inside of the POINT_OBJECT macro, and it works like a charm.
Without further ado, meet validMovesFromSpot:. Like other functions we've encountered so far, the same basic logic is in place for checking all eight directions, so a portion of the code is omitted inline below. The sample project, of course, contains all of the code for the entire project.
-(NSArray*)validMovesFromSpot:(NSPoint)spot {
//make sure spot is occupied or else there's no move
if (board[(int)spot.x][(int)spot.y] == EMPTY)
return [NSArray array];
LineCounts lc;
lc = [self lineCountsForSpot:spot];
NSMutableArray *moves = [NSMutableArray arrayWithCapacity:8];
//up
if (
spot.y + lc.up_down < DIMENSION &&
PLAYER_MOVING_NOT_AT(spot.x,spot.y+lc.up_down) &&
OPPONENT_NOT_BLOCKING(spot,spot.x,spot.y+lc.up_down)
)
[moves addObject:POINT_OBJECT(spot.x,spot.y+lc.up_down)];
//down
if (
spot.y - lc.up_down >= 0 &&
PLAYER_MOVING_NOT_AT(spot.x, spot.y-lc.up_down) &&
OPPONENT_NOT_BLOCKING(spot,spot.x, spot.y-lc.up_down)
)
[moves addObject:POINT_OBJECT(spot.x, spot.y-lc.up_down)];
//snip...
//left,right,up right, down left, up left, down right
//snip...
return (NSArray*)moves;
Recalling that validMovesFromSpot was called all the way back in mouseDown:, we've now completed the circuit and grown the project to the point that two users could sit down at the keyboard and take turns moving until one of them identifies a gameover condition. Next time, we'll implement gameover checking and a few final details before delving head first into a game tree search where we will develop an AI opponent using some custom heuristics.
To prepare you for next time, the sample project already includes a hook in AppController's called validMovesFromSpot:withBoard:, which we'll be able to use for querying the game board about particular moves that are available from our game tree search. The implementation details are fairly straightforward and totally reuse existing code by temporarily swapping out the GameBoard's instance variables board and playerMoving whenever it is invoked. Since all move generation logic depends on these two values, they're all we need to modify in order to produce a list of moves.
If you prefer to work on your own copy instead of using the sample project provided, be sure to remember to set the outlet for AppController's gameBoard, as shown in Figure 3, when you're trying to reference the gameBoard from within AppController.

Figure 3. Don't forget to set gameBoard's outlet in AppController
And while we're at it, there's also a method in GameBoard called board that simply returns a lightweight wrapper around the actual 2D integer array used to keep track of pieces. An illustration of this method is also in AppController's awakeFromNib method. In the next episode, the plan is to use this data board to feed our game tree search.
Have fun tinkering around with what we have so far until next time. As always, feel free to leave your thoughts, feedback, and suggestions below.
Matthew Russell is a computer scientist from middle Tennessee; and serves Digital Reasoning Systems as the Director of Advanced Technology. Hacking and writing are two activities essential to his renaissance man regimen.
Return to the Mac DevCenter.
Showing messages 1 through 9 of 9.
-
EQUAL_NSPOINTS
2007-01-10 00:14:21 QwertyDenzel [Reply | View]
How come NSEqualPoints wasn't used instead of EQUAL_NSPOINTS? -
EQUAL_NSPOINTS
2007-01-10 05:17:24 Matthew Russell |
[Reply | View]
Great question, thanks for asking. One other person e-mailed me about this wondering the same thing.
The reason I chose to go with a macro is because in the next piece, we're going to refactor from using NSPoints for identifying spots on the board to using a custom struct that holds two integer values. I went ahead and introduced the macros to facilitate this process a bit.
Why do this at all? Because we'll be inspecting the contents of the board[][] array millions of times during our game tree search, and the cost of casting the floats contained in NSPoints to int values that can index the array is expensive. The cost of casting integer values to floats whenever we do drawing, however, is not so significant since there's going to be a very little bit of drawing going on compared the amount of searching and manipulating board[][] (not to mention that the code reads cleaner when you don't have a bunch of casts cluttering things up.) Even now, there's very little drawing going on, and it's certainly not a performance factor.
Your follow on question might be why I didn't just do this in the first place. No great answer here aside from the fact that this enhancement didn't seem obvious to me until after the first piece in this series had been published and I got more focused on thinking about the search that's coming up.
-
Incorrect Boolean login in NOTEQUAL_NSPOINTS?
2007-01-05 20:36:09 Joe Jones [Reply | View]
It would seem to me that the equation there should be p1.x != p2.x ||p1.y != p2.y. This would be the equivalent of !EQUALS_NPOINTS(p1, p2). (1,2) and (3,4) would resolve to TRUE as expected but (1,2) and (3,2) would resolve to FALSE but are obviously not equal. -
Incorrect Boolean login in NOTEQUAL_NSPOINTS?
2007-01-06 07:15:35 Matthew Russell |
[Reply | View]
Joe, That's a great catch. According to DeMorgan's law, you're absolutely correct. The reason this typo (from where I'd cut/pasted the previous macro) didn't surface in the execution of the program as presented is that in all cases valid NSPoints were being compared to NIL_POINT. Thus, the sub-expressions always evaluate to NO. i.e. (NO && NO == NO) and (NO || NO == NO) and it didn't make a difference as far as the end result was concerned. When you compare a valid point to another valid point, however, there would have certainly been a noticeable bug in place.
Anyway, we'll have the article and project files updated shortly, along with one other small change to the opponentInLineBetweenCoord:andCoord: method . Thanks for pointing that out! -
FIXED: Incorrect Boolean login in NOTEQUAL_NSPOINTS?
2007-01-08 08:43:30 Matthew Russell |
[Reply | View]
Just wanted to close the loop on this. Both the article and sample files have now been updated with the fix mentioned above.
Looking forward to next time, where we'll finish this series off! -
FIXED: Incorrect Boolean login in NOTEQUAL_NSPOINTS?
2007-01-12 06:52:49 wootest [Reply | View]
I'm getting errors when building:
[..]/BoardGame/GameBoard.h:77: error: array type has incomplete element type
In file included from [..]/BoardGame/AppController.m:5:
[..]/BoardGame/AppController.h:12: error: array type has incomplete element type
[..]/BoardGame/AppController.m:11: error: array type has incomplete element type
[..]/BoardGame/AppController.m: In function '-[AppController validLOAMovesFromSpot:withBoard:]':
[..]/BoardGame/AppController.m:13: error: type of formal parameter 4 is incomplete
[..]/BoardGame/AppController.m: In function '-[AppController awakeFromNib]':
[..]/BoardGame/AppController.m:38: error: type of formal parameter 4 is incomplete
In file included from [..]/BoardGame/GameBoard.m:7:
[..]/BoardGame/GameBoard.h:77: error: array type has incomplete element type
[..]/BoardGame/GameBoard.m:601: error: array type has incomplete element type -
FIXED: Incorrect Boolean login in NOTEQUAL_NSPOINTS?
2007-01-12 17:09:37 Matthew Russell |
[Reply | View]
It appears that the zip file that contains the project has gotten corrupted. I got an error from StuffIt as well as a CRC error from "unzip" when I downloaded it, so I believe this is your problem. I've notified the folks that produce the articles and they're working to get it updated.
If you want to work on this in the meantime, you should be able to take the .h and .m files given at the beginning of the article and import them into a new Xcode project, connect the outlet that's mentioned and be good to go.
I'll check back in and notify everyone once the zip archive is updated on O'Reilly's server. -
FIXED: Incorrect Boolean login in NOTEQUAL_NSPOINTS?
2007-01-15 11:23:07 Matthew Russell |
[Reply | View]
The project files are now downloadable in a non-corrupted format. Sorry about that. We're not quite sure how it happened, but it's fixed now.
As for the errors mentioned about the arrays having incomplete element types, Ginkgo_52 is absolutely correct. I've been building with GCC 3.3, so I didn't notice those problems. The fix mentioned does indeed work. The only caveat I'd add is to use DIMENSION instead of hard coding 8 in those places where you have arrays defined as int[][].
Anyhow, just for informational purposes, you can also go into terminal and type "sudo gcc_select 3.3" (or whatever version you want) if you want to explicitly change the default version of gcc used.
I'll make these changes to the final project file for next time. Thanks again, Ginkgo_52, for the quick assist...I had initially thought that the corrupted project files might have somehow been the issue. -
FIXED: Incorrect Boolean login in NOTEQUAL_NSPOINTS?
2007-01-15 09:55:49 Ginkgo_52 [Reply | View]
I also receive the "array type has incomplete element type" errors when building using the GCC 4.0 compiler, but not with GCC 3.3 (the project compiles with no errors using GCC 3.3). This appears to be due to changes that were made in the GCC compiler in version 4 - the GCC compiler no longer allows incomplete (undimensioned) array types. A quick web search indicates that others have obtained this error in GCC 4.0 from code that compiled with GCC 3.x.
The project will compile without errors in GCC 4.0 if the board array is explicitly dimensioned by changing the four occurrences of 'withBoard:(int[][])board' to 'withBoard:(int[8][8])board' (one each in GameBoard.h, GameBoard.m, AppController.h and AppController.m).





