PDA

View Full Version : Designing a Checkers Game Class (Long inc 300 lines of code)



cairnswm
24-03-2003, 10:06 AM
Designing a Checkers Game Class

I want to do a tutorial on using DWS2 as a scripting language in a game. For this I need to have a reasonably simple game that lends itself easily to AI. I decided the logical option was to use Checkers as the game, however I couldn't find a nice easy Delphi implementation of the game. This meant I needed to create my own component.

My decision to make the Checkers Game a Class instead of a Component is so that is can be used internally to other classes (e.g. an AI class) and not always as a component. There is no reason that the class cannot have a wrapper created around it that would make it a component. This wrapper could include a display, AI and whatever else you wanted.

In designing my class I needed to implement various functions that would allow another component to query the class and by doing this retrieve information for various purposes - eg Display, AI decision making and load/saving of games. By separating these functions from the Checkers class it would allow many different methods of implementing a checkers game.

To start with I listed the various information that would be required from the component, surprisingly simple...
Board Position
Possible Moves
Game State

Board Position is needed to know what the board looks like, as well as some special options such as Setup etc.
Possible moves would be a List of all possible moves that could be made, this would allow an interface to quickly check if a move was permissible and also allows an AI component to quickly iterate through all the moves that would be possible from the current board position.
Game State is needed to know if the game is in progress, finished (and who the winner is) and to indicate who's turn it is.

A few additional methods would be needed such as
Move which would carry out a move on the board, this would also check that the move was valid.
King() which returns the King character for the input character.

As an AI process would want to test various possible scenarios a testing option would be require. To complement this a Copy method would be needed to copy the state of a board to another.

My final class definition looks as follows:



Type
TCheckers = Class
private
FBoard : Array[0..7,0..7] of Char;
FGameState: Integer;
FTestBoard: TCheckers;
FPossibleMoves: TStringList;
FOnGameStateChange: TNotifyEvent;
function GetBoard(P: TPoint): Char;
procedure SetBoard(P: TPoint; const Value: Char);
procedure SetGameState(const Value: Integer);
procedure SetOnGameStateChange(const Value: TNotifyEvent);
public
Constructor Create;
Destructor Destroy;
Property Board[P : TPoint] : Char read GetBoard write SetBoard;
Property GameState : Integer read FGameState write SetGameState;
Property PossibleMoves : TStringList read FPossibleMoves;
Procedure SetUp;
Procedure Move(FromSquare : TPoint; ToSquare : array of TPoint);
Procedure Copy(OrigBoard : TCheckers);
Property OnGameStateChange : TNotifyEvent read FOnGameStateChange write SetOnGameStateChange;
Procedure DefineMoves;
Function King(Color : Char) : Char;
End;


Before I list the code I’ll do a quick explanation of a number of the more important aspects of the class.

The Board is stored as an Array, while this wastes space (due to more than half the blocks being stored for no practical reason), it does make it easier to do calculations. I always believe in doing it the easy way as it saves time. Access to the Board is done by TPoint reference to make actions shorter especially the Move method once multiple jumps are implemented.

The possible moves list if populated by the DefineMoves method. This method first searches for Jumps and if it finds any it then does not check for normal moves. It is important to remember to test for king’s backward moves at the same time. In my implementation I've tried to keep it generic to make it simple to read. A number of the loops should be moved into separate functions to make code reuse better.

When a board is copied to an alternate board it copies the GameState, Board and then defines the moves internally. If we look at this scenario it questions the use of the TestBoard within the parent structure. There is no reason that a user program cannot just create its own board and then copy the detail from the original to the detail board.

Every action should first check if the GameState is correct. My Constants are as follows:


Const
gsStart = 0;
gsMoveRed = 1;
gsMoveBlack = 2;
gsFinishRed = 3;
gsFinishBlack = 4;
gsFinishNone = 5;

Which implies that before the game can start a color should be allocated to move first. When the GameState is gsStart no moves are possible.

Now the listing on my Checkers Unit, followed by an example of using the Checkers Class in a program.




unit Checkers;

interface

Uses
Classes, SysUtils, Windows;

Const
gsStart = 0;
gsMoveRed = 1;
gsMoveBlack = 2;
gsFinishRed = 3;
gsFinishBlack = 4;
gsFinishNone = 5;
Type
TCheckers = Class
private
FBoard : Array[0..7,0..7] of Char;
FGameState: Integer;
FTestBoard: TCheckers;
FPossibleMoves: TStringList;
FOnGameStateChange: TNotifyEvent;
function GetBoard(P: TPoint): Char;
procedure SetBoard(P: TPoint; const Value: Char);
procedure SetGameState(const Value: Integer);
procedure SetOnGameStateChange(const Value: TNotifyEvent);
public
Constructor Create;
Destructor Destroy;
Property Board[P : TPoint] : Char read GetBoard write SetBoard;
Property GameState : Integer read FGameState write SetGameState;
Property PossibleMoves : TStringList read FPossibleMoves;
Procedure SetUp;
Procedure Move(FromSquare : TPoint; ToSquare : array of TPoint);
Procedure Copy(OrigBoard : TCheckers);
Property OnGameStateChange : TNotifyEvent read FOnGameStateChange write SetOnGameStateChange;
Procedure DefineMoves;
Function King(Color : Char) : Char;
End;

implementation

{ TCheckers }

procedure TCheckers.Copy(OrigBoard: TCheckers);
Var
I,J : Integer;
begin
FGameState := OrigBoard.GameState;
For I := 0 to 8 do
For J := 0 to 8 do
FBoard[I,j] := OrigBoard.Board[Point(I,J)];
DefineMoves;
end;

constructor TCheckers.Create;
Var
I,J : Integer;
begin
For I := 0 to 7 do
For J := 0 to 7 do
Begin
FBoard[I,J] := ' ';
If (I in [0,2,4]) and (Odd(J)) then
FBoard[I,J] := 'o';
If (I = 1) and (Not Odd(J)) then
FBoard[I,J] := 'o';
If (I in [5,7]) and (Not Odd(J)) then
FBoard[I,J] := 'x';
If (I = 6) and (Odd(J)) then
FBoard[I,J] := 'x';
End;
FGameState := gsStart;
FTestBoard := Nil;
FPossibleMoves := TStringList.Create;
end;

procedure TCheckers.DefineMoves;
Var
TDir, Dir, JDir : Integer;
Color : Char;
MoveStart : TPoint;
MoveDest : TPoint;
I,J,K,L : Integer;
PCount : Integer;
begin
PCount := 0;
PossibleMoves.Clear;
// Check if it is Possible to Move
Case GameState of
gsMoveRed : Begin Dir := -1; Color := 'x' End;
gsMoveBlack : Begin Dir := 1; Color := 'o' End;
Else
Begin
Raise Exception.Create('Game not in Movable State');
Exit;
End;
End;
// Check for Jumps
For I := 0 to 7 do
For J := 0 to 7 do
Begin
If Board[Point(I,J)] in [Color,King(Color)] then
Begin
PCount := PCount + 1;
For K := 0 to 1 do
Begin
Case K of
0 : JDir := -1;
1 : JDir := 1;
End;
If Not(Board[Point(I+Dir,J+JDir)] in [' ',Color,King(Color)]) then
Begin
If (I+Dir+Dir in [0..7]) and (J+JDir+JDir in [0..7]) then
If Board[Point(I+Dir+Dir,J+JDir+JDir)] in [' '] then
PossibleMoves.Add(IntToStr(I)+','+IntToStr(J)+' to '+IntToStr(I+Dir+Dir)+','+IntToStr(J+JDir+JDir))
End;
// If this is a king check for backward jumps as well
If Board[Point(I,J)] = King(Board[Point(I,J)]) then
Begin
Case Dir of
-1 : TDir := 1;
1 : TDir := -1;
End;
If Not(Board[Point(I+TDir,J+JDir)] in [' ',Color,King(Color)]) then
Begin
If (I+TDir+TDir in [0..7]) and (J+JDir+JDir in [0..7]) then
If Board[Point(I+TDir+TDir,J+JDir+JDir)] in [' '] then
PossibleMoves.Add(IntToStr(I)+','+IntToStr(J)+' to '+IntToStr(I+TDir+TDir)+','+IntToStr(J+JDir+JDir))
End;
End;
End;
End;
End;
// Check for Normal Moves
If PossibleMoves.Count = 0 then
For I := 0 to 7 do
For J := 0 to 7 do
Begin
If Board[Point(I,J)] in [Color,King(Color)] then
Begin
For K := 0 to 1 do
Begin
Case K of
0 : JDir := -1;
1 : JDir := 1;
End;
If (I+Dir in [0..7]) and (J+JDir in [0..7]) then
If (Board[Point(I+Dir,J+JDir)] in [' ']) then
Begin
PossibleMoves.Add(IntToStr(I)+','+IntToStr(J)+' to '+IntToStr(I+Dir)+','+IntToStr(J+JDir))
End;
// If this is a king check for backward moves as well
If Board[Point(I,J)] = King(Board[Point(I,J)]) then
Begin
Case Dir of
-1 : TDir := 1;
1 : TDir := -1;
End;
If (I+TDir in [0..7]) and (J+JDir in [0..7]) then
If (Board[Point(I+TDir,J+JDir)] in [' ']) then
Begin
PossibleMoves.Add(IntToStr(I)+','+IntToStr(J)+' to '+IntToStr(I+TDir)+','+IntToStr(J+JDir))
End;
End;
End;
End;
End;
If PossibleMoves.Count = 0 then
Begin
If PCount > 0 then
Begin
Case GameState of
gsMoveRed : Begin GameState := gsFinishBlack; End;
gsMoveBlack : Begin GameState := gsFinishRed; End;
End;
End
Else
GameState := gsFinishNone;
end;
end;

destructor TCheckers.Destroy;
begin
If FTestBoard <> Nil then
Begin
FTestBoard.Free;
FTestBoard &#58;= Nil;
End;
FPossibleMoves.Free;
end;

function TCheckers.GetBoard&#40;P&#58; TPoint&#41;&#58; Char;
begin
Result &#58;= FBoard&#91;P.X,P.Y&#93;;
end;

function TCheckers.King&#40;Color&#58; Char&#41;&#58; Char;
begin
Case Color of
'X','x' &#58; Result &#58;= 'X';
'O','o' &#58; Result &#58;= 'O';
Else Result &#58;= ' ';
End;
end;

procedure TCheckers.Move&#40;FromSquare&#58; TPoint; ToSquare&#58; array of TPoint&#41;;
Var
S &#58; String;
begin
// Check if Game in Movable Position
If Not &#40;GameState in &#91;gsMoveRed, gsMoveBlack&#93;&#41; then
Raise Exception.Create&#40;'Game not in Movable State'&#41;;
// Check if Valid
S &#58;= IntToStr&#40;FromSquare.X&#41; + ',' + IntToStr&#40;FromSquare.Y&#41; +
' to ' +
IntToStr&#40;ToSquare&#91;0&#93;.X&#41; + ',' + IntToStr&#40;ToSquare&#91;0&#93;.Y&#41;;
If PossibleMoves.IndexOf&#40;S&#41; = -1 then
Begin
Raise Exception.Create&#40;'That move is not possible'&#41;;
Exit;
End;
// Do Move
Board&#91;ToSquare&#91;0&#93;&#93; &#58;= Board&#91;FromSquare&#93;;
If ToSquare&#91;0&#93;.X in &#91;0,7&#93; then
Begin
Board&#91;ToSquare&#91;0&#93;&#93; &#58;= King&#40;Board&#91;ToSquare&#91;0&#93;&#93;&#41;;
End;

Board&#91;FromSquare&#93; &#58;= ' ';
If Abs&#40;FromSquare.X-ToSquare&#91;0&#93;.X&#41; = 2 then
Begin
FromSquare.X &#58;= &#40;&#40;FromSquare.X+ToSquare&#91;0&#93;.X&#41; Div 2&#41;;
FromSquare.Y &#58;= &#40;&#40;FromSquare.Y+ToSquare&#91;0&#93;.Y&#41; Div 2&#41;;
Board&#91;FromSquare&#93; &#58;= ' ';
End;
// Change GameState
Case GameState of
gsMoveRed &#58; GameState &#58;= gsMoveBlack;
gsMoveBlack &#58; GameState &#58;= gsMoveRed;
End;
// Load Possible Moves
DefineMoves;
end;

procedure TCheckers.SetBoard&#40;P&#58; TPoint; const Value&#58; Char&#41;;
begin
If Value in &#91;'O','X'&#93; then
Begin
// Check if Valid Position for a Piece
If &#40;Odd&#40;P.X&#41; and Odd&#40;P.Y&#41;&#41; or
&#40;Not Odd&#40;P.X&#41; and &#40;Not Odd&#40;P.Y&#41;&#41;&#41; then
Begin
Raise Exception.Create&#40;'A Piece may not be placed there'&#41;;
Exit;
End;
End;
FBoard&#91;P.X,P.Y&#93; &#58;= Value;
end;

procedure TCheckers.SetGameState&#40;const Value&#58; Integer&#41;;
Var
I,J &#58; Integer;
begin
FGameState &#58;= Value;
If FGameState = gsStart then
Begin
For I &#58;= 0 to 7 do
For J &#58;= 0 to 7 do
Begin
FBoard&#91;I,J&#93; &#58;= ' ';
If &#40;I in &#91;0,2&#93;&#41; and &#40;Odd&#40;J&#41;&#41; then
FBoard&#91;I,J&#93; &#58;= 'o';
If &#40;I = 1&#41; and &#40;Not Odd&#40;J&#41;&#41; then
FBoard&#91;I,J&#93; &#58;= 'o';
If &#40;I in &#91;5,7&#93;&#41; and &#40;Not Odd&#40;J&#41;&#41; then
FBoard&#91;I,J&#93; &#58;= 'x';
If &#40;I = 6&#41; and &#40;Odd&#40;J&#41;&#41; then
FBoard&#91;I,J&#93; &#58;= 'x';
End;
End;
If Assigned&#40;OnGameStateChange&#41; then
OnGameStateChange&#40;Self&#41;;
end;

procedure TCheckers.SetOnGameStateChange&#40;const Value&#58; TNotifyEvent&#41;;
begin
FOnGameStateChange &#58;= Value;
end;

procedure TCheckers.SetUp;
begin
GameState &#58;= gsStart;
end;

end.



To use the Checkers class in a program I started a new application and added a StringGrid, a Button and a ListBox. The FormCreate, Listbox.OnClick, StringGrid.OnDrawCell and the Button.OnClick have event handlers declared. The StringGrid is used to display the Game Board, the Button is used to start a new game and the list box is used to display all the available moves.

Run the program and click the start button. A list of all the moves that ‘x’ can do is available in the ListBox. If one of these moves is clicked the move is carried out (by the Checkers class and updated on the board by the display).



unit MainCheckers;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
Grids, StdCtrls, Checkers;

type
TForm1 = class&#40;TForm&#41;
StringGrid1&#58; TStringGrid;
Button1&#58; TButton;
ListBox1&#58; TListBox;
procedure StringGrid1DrawCell&#40;Sender&#58; TObject; ACol, ARow&#58; Integer;
Rect&#58; TRect; State&#58; TGridDrawState&#41;;
procedure Button1Click&#40;Sender&#58; TObject&#41;;
procedure FormCreate&#40;Sender&#58; TObject&#41;;
procedure ListBox1Click&#40;Sender&#58; TObject&#41;;
private
&#123; Private declarations &#125;
Checkers &#58; TCheckers;
Procedure GameStateChange&#40;Sender &#58; TObject&#41;;
public
&#123; Public declarations &#125;
end;

var
Form1&#58; TForm1;

implementation

&#123;$R *.DFM&#125;

procedure TForm1.StringGrid1DrawCell&#40;Sender&#58; TObject; ACol, ARow&#58; Integer;
Rect&#58; TRect; State&#58; TGridDrawState&#41;;
begin
If &#40;Odd&#40;ACol&#41; and Not Odd&#40;ARow&#41;&#41; or
&#40;Not Odd&#40;ACol&#41; and Odd&#40;ARow&#41;&#41; then
StringGrid1.Canvas.Brush.Color &#58;= clSilver
Else
StringGrid1.Canvas.Brush.Color &#58;= clWhite;
StringGrid1.Canvas.FillRect&#40;Rect&#41;;
If gdSelected in State then
Begin
StringGrid1.Canvas.Brush.Color &#58;= clBlue;
StringGrid1.Canvas.FrameRect&#40;Rect&#41;;
End;
If &#40;Odd&#40;ACol&#41; and Not Odd&#40;ARow&#41;&#41; or
&#40;Not Odd&#40;ACol&#41; and Odd&#40;ARow&#41;&#41; then
StringGrid1.Canvas.Brush.Color &#58;= clSilver
Else
StringGrid1.Canvas.Brush.Color &#58;= clWhite;
If StringGrid1.Cells&#91;ARow,ACol&#93; <> ' ' Then
StringGrid1.Canvas.TextOut&#40;Rect.Left+12, Rect.Top + 9,
StringGrid1.Cells&#91;ARow,ACol&#93;&#41;;
end;

procedure TForm1.Button1Click&#40;Sender&#58; TObject&#41;;
begin
Checkers.GameState &#58;= gsMoveRed;
ListBox1.Items.Clear;
Checkers.DefineMoves;
ListBox1.Items.Assign&#40;Checkers.PossibleMoves&#41;;
end;

procedure TForm1.FormCreate&#40;Sender&#58; TObject&#41;;
begin
Caption &#58;= 'CairnsGames S.A. - Checkers';
StringGrid1.RowCount &#58;= 8;
StringGrid1.ColCount &#58;= 8;
StringGrid1.FixedRows &#58;= 0;
StringGrid1.FixedCols &#58;= 0;
StringGrid1.DefaultRowHeight &#58;= 36;
StringGrid1.DefaultColWidth &#58;= 36;
Checkers &#58;= TCheckers.Create;
Checkers.OnGameStateChange &#58;= GameStateChange;
Checkers.SetUp;
end;

procedure TForm1.GameStateChange&#40;Sender&#58; TObject&#41;;
Var
I,J &#58; Integer;
begin
For I &#58;= 0 to 7 do
For J &#58;= 0 to 7 do
StringGrid1.Cells&#91;I,J&#93; &#58;= Checkers.Board&#91;Point&#40;I,J&#41;&#93;;
end;

procedure TForm1.ListBox1Click&#40;Sender&#58; TObject&#41;;
Var
X,Y, X1,Y1 &#58; Integer;
S &#58; String;
begin
S &#58;= ListBox1.Items&#91;ListBox1.ItemIndex&#93;;
X &#58;= StrToInt&#40;Copy&#40;S,1,1&#41;&#41;;
Y &#58;= StrToInt&#40;Copy&#40;S,3,1&#41;&#41;;
X1 &#58;= StrToInt&#40;Copy&#40;S,8,1&#41;&#41;;
Y1 &#58;= StrToInt&#40;Copy&#40;S,10,1&#41;&#41;;
Checkers.Move&#40;Point&#40;X,Y&#41;,&#91;Point&#40;X1,Y1&#41;&#93;&#41;;
ListBox1.Items.Clear;
Checkers.DefineMoves;
ListBox1.Items.Assign&#40;Checkers.PossibleMoves&#41;;
end;

end.



Next step is to implement a DWS2 component that integrates to Checkers to allow the implementation of AI through scripting.

D-Checkers anyone :o