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:

Code:
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:
Code:
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.


Code:
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).

Code:
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