Coding a Game of Memory in Delphi – OOP Model

  

Memory, Match Up, Concentration … there are many names for a simple card game I’m certain you’ve been playing with your friends at some point in your childhood. I’m also certain you are still playing it from time to time (at least I do with my kids). Just a few months ago, I’ve tried my “luck” against a robot in CosmoCaixa, Barcelona (image).
The rules of the game are simple: cards are laid face down on a surface and two, per turn, are flipped face up. If the flipped cards are a match pair (same looking, same rank, save value) the player claims (wins) the pair and plays again. If they are not a match, cards are flipped face down again, and the next player takes turn. The game ends when all the pairs have been claimed and the player with the most claimed pairs is the winner. If all players have the same number of claimed pairs we can agree to have a tie, or to have the last player be the winner.
I’ve always been a fan of such simple games – from my point of view they are a perfect pick if you want to start learning programming – have fun and sharpen your developer skills at the same time.
While there are Delphi implementations of the game you can find online – most of them have heavily mixed the visual presentation of the game (user interface) with the model (implementation of the game logic).
In my version of Memory, I’d like to separate the user interface (front end) from the game logic (back end) as much as possible. I want to create a game model in OOP style – where the game logic does not interact (or as less as possible) with the front end.

TMemoryGame = class(TObject)
In a game of Memory, as the rules state, we would have a number of pairs of cards having the same value. We can think of each card as a game field. Pairs of fields would have the same value. For example: if we are to have 10 fields (cards), that would be 5 pairs and the values could be: 1, 1, 2, 2, 3, 3, 4, 4, 5, 5.
Before I move on with “the Field” and since this is a game, we would have players. At least two would be needed (even if you are to play against yourself). Each player would have a name. Also, as the players take turn, if a pair is matched – the number of claimed pairs (during the game) should be stored for each player.
Therefore, first class: TPlayer:

TPlayer = class
private
fName: string;
fClaimedPairs: integer;
public
property Name : string read fName write fName;
property ClaimedPairs : integer read fClaimedPairs;
private
constructor Create(const name : string);
end;

The implementation part is for the constructor:

constructor TPlayer.Create(const name : string);
begin
fName := name;
end;

Ok, we can now go back to “Field” implementation…
When a player claims a field, we want to have this info stored within the field. Also, we will at some point create the UI (front end) as the value for the field is to be somehow presented/displayed to the user (player). The field needs a host – a visual control you will use to present the “card” to the player.
Now, since we are only working on the OOP model, it is up to you to decide how the user interface will look and if you will go with the VCL or FireMonkey – so you can target Windows, Mac, mobile – up for you to decide. One great thing about Delphi is that all controls (and everything) inherits from TObject – so why not simply use TObject for the host.
Therefore, second class: TMField:

TMField = class
private
fValue: integer;
fHost: TObject;
fPlayer: TPlayer;
public
property Value : integer read fValue;
property Host : TObject read fHost write fHost;
property Player : TPlayer read fPlayer;
constructor Create(const value : integer);
end;

Again, only the constructor needs the implementation:

constructor TMField.Create(const value: integer);
begin
fValue := value;
fPlayer := nil;
end;

Ok, so we have players and we have fields. Let’s do the main class: TMemoryGame – to implement the game logic.
Here’s the interface part and we later see the implementation:

TMemoryGamePlayerEvent = procedure(const player : TPlayer) of object;
TMemoryGameFieldEvent = procedure(const mField : TMField) of object;
TMemoryGameFieldPairEvent = procedure(const mField1, mField2 : TMField) of object;

TGridSize = record
X,Y : integer;
end;

TMemoryGame = class
private
fPlayersCount: integer;
fClaimedPairs : integer;
fPairsCount: integer;
fFields: TObjectList<TMField>;
fPlayers: TObjectList<TPlayer>;
fOpenFirst: boolean;
fOpenedField: TMField;
fCurrentPlayer: TPlayer;
fOnFieldsPaired: TMemoryGameFieldPairEvent;
fOnFieldClaimed: TMemoryGameFieldEvent;
fOnOpenField: TMemoryGameFieldEvent;
fOnCloseField: TMemoryGameFieldEvent;
fOnGameOver: TMemoryGamePlayerEvent;
fOnNextPlayer: TMemoryGamePlayerEvent;
fOnGameStart: TMemoryGamePlayerEvent;
fOnPlayerCreated : TMemoryGamePlayerEvent;
private
property OpenFirst : boolean read fOpenFirst write fOpenFirst;
property OpenedField : TMField read fOpenedField write fOpenedField;

property ClaimedPairs : integer read fClaimedPairs;
function AllPairsClaimed : boolean;
public
constructor Create;
destructor Destroy; override;
public
property PairsCount : integer read fPairsCount;
property PlayersCount : integer read fPlayersCount;

function NewGame(const numberOfPairs, numberOfPlayers: integer) : TGridSize;

procedure FieldHostAction(Sender : TObject);

property Fields : TObjectList<TMField> read fFields;
property Players : TObjectList<TPlayer> read fPlayers;
property CurrentPlayer : TPlayer read fCurrentPlayer;

property OnFieldClaimed : TMemoryGameFieldEvent read fOnFieldClaimed write fOnFieldClaimed;
property OnOpenField : TMemoryGameFieldEvent read fOnOpenField write fOnOpenField;
property OnCloseField : TMemoryGameFieldEvent read fOnCloseField write fOnCloseField;
property OnFieldsPaired : TMemoryGameFieldPairEvent read fOnFieldsPaired write fOnFieldsPaired;
property OnGameOver : TMemoryGamePlayerEvent read fOnGameOver write fOnGameOver;
property OnNextPlayer : TMemoryGamePlayerEvent read fOnNextPlayer write fOnNextPlayer;
property OnPlayerCreated : TMemoryGamePlayerEvent read fOnPlayerCreated write fOnPlayerCreated;
property OnGameStart : TMemoryGamePlayerEvent read fOnGameStart write fOnGameStart;
end;

I want to be able to create a memory game having an arbitrary number of players and (pairs of) fields. Standardly, the cards (fields) would be presented in some kind of rectangular form: having a number of rows and a number of columns, so that rows * columns = number of pairs.
Hence, the NewGame function:

function TMemoryGame.NewGame(const numberOfPairs, numberOfPlayers: integer) : TGridSize;
var
i, rnd : integer;
aField : TMField;
newPlayer : TPlayer;

procedure CalcGridSize;
begin
//look for: Quick Algorithm: Get Ideal Size (Square like)
// For a Board Game Having an Arbitrary (but Even) Number of Fields
end;

begin
fPairsCount := numberOfPairs; if fPairsCount < 1 then fPairsCount := 1;
fPlayersCount := numberOfPlayers; if fPlayersCount < 1 then fPlayersCount := 1;

CalcGridSize();

//players
Players.Clear;
for i := 1 to PlayersCount do
begin
newPlayer := TPlayer.Create(‘player ‘ + i.ToString());
if Assigned(fOnPlayerCreated) then fOnPlayerCreated(newPlayer);
Players.Add(newPlayer);
end;
fCurrentPlayer := Players.First;

//fields
Fields.Clear;
for i := 0 to -1 + 2 * PairsCount do
begin
// value would be 1,1,2,2,3,3…
aField := TMField.Create(1 + i DIV 2);

Fields.Add(aField);
end;

//randomize field positions
Randomize;
Fields.Sort(TComparer<TMField>.Construct(
function(const Left, Right : TMField) : integer
begin
result := -1 + Random(3);
end
));

//let’s start…
if Assigned(fOnGameStart) then fOnGameStart(CurrentPlayer);
end;

I’m hoping the code is self-explanatory, hehe. In essence, the needed number of players and number of field pairs are sent as arguments to the function and the function creates the players, creates the fields, sets their value and finally randomizes field positions in the Fields list.
Given the number of pairs the NewGame would also calculate the Ideal Size (Square like) For a Board Game Having an Arbitrary (but Even) Number of Fields.
Now, as you can see there are a number of events being raised by the game: when the game starts, when the next players turn is, when we have a winner and so on.
Do note the “FieldHostAction” procedure. When developing the front end (the user interface) you would allow the user to do some action to open a field. If a field is displayed on a TButton or a TImage – that would be OnClick. So, let’s see what happens when the user tries to open a card – click a field to (first) open or (second) claim:

procedure TMemoryGame.FieldHostAction(Sender: TObject);
var
actionOnField: TMField;
winner, aPlayer : TPlayer;

function FieldByHost(const host : TObject) : TMField;
var
mf : TMField;
begin
result := nil;
for mf in Fields do
if mf.Host = host then Exit(mf);
end; (*FieldByHost*)
begin
actionOnField := FieldByHost(sender);
if actionOnField = nil then Exit;

if actionOnField.Player = nil then
begin
if OpenFirst then
begin
OpenedField := actionOnField;

if Assigned(fOnOpenField) then fOnOpenField(actionOnField);
end
else //open second
begin
if Assigned(fOnOpenField) then fOnOpenField(actionOnField);

if OpenedField.Value = actionOnField.Value then
begin
OpenedField.fPlayer := CurrentPlayer;
actionOnField.fPlayer := CurrentPlayer;

Inc(fClaimedPairs);
CurrentPlayer.fClaimedPairs := 1 + CurrentPlayer.ClaimedPairs;

if Assigned(fOnFieldsPaired) then fOnFieldsPaired(OpenedField, actionOnField);

if AllPairsClaimed then
begin
winner := CurrentPlayer; //even if there are other players with the same namuber of claimed pairs
for aPlayer in Players do
if aPlayer.ClaimedPairs > winner.ClaimedPairs then winner := aPlayer;

if Assigned(fOnGameOver) then fOnGameOver(winner);
end;
end
else
begin
Sleep(500);
if Assigned(fOnCloseField) then fOnCloseField(OpenedField);
if Assigned(fOnCloseField) then fOnCloseField(actionOnField);

if CurrentPlayer = Players.Last then
fCurrentPlayer := Players.First
else
fCurrentPlayer := Players[1 + Players.IndexOf(CurrentPlayer)];

if Assigned(fOnNextPlayer) then fOnNextPlayer(CurrentPlayer);
end;
end;

OpenFirst := not OpenFirst;
end
else
begin
//already opened and claimed
if Assigned(fOnFieldClaimed) then fOnFieldClaimed(actionOnField);
end;
end;

The above is actually the memory game implementation in full.
When the player tries to open a card (action on Field’s Host):

Find the field by host (“actionOnField”)
If already claimed – raise the OnFieldClaimed event
If not already claimed

If first card open – remember the opened card (OpenedField) and raise the OnOpenField event – so the front end (UI) can react and change the state of the Field’s Host (control displaying the fields value)
If second card open

If OpenedField = actionOnField a player has matched a pair. If AllPairsClaimed raise the OnGameOver (and find the winner)
If OpenedField actionOnField raise events to close fields and move to next player.

And folks, that’s it, believe it or not.
Ok, some implementations are missing, so here goes:

function TMemoryGame.AllPairsClaimed: boolean;
begin
result := ClaimedPairs = PairsCount;
end;

constructor TMemoryGame.Create;
begin
OpenFirst := true;

fFields := TObjectList<TMField>.Create(true);
fPlayers := TObjectList<TPlayer>.Create(true);
end;

destructor TMemoryGame.Destroy;
begin
FreeAndNil(fFields);
FreeAndNil(fPlayers);

inherited;
end;

Next time, as always is the case when things start to get interesting, I go on to create the front-end (the user interface). To make it simple, it will look like:

Stay tuned…

Comments are closed.