Coding a Game of Memory in Delphi – The User Interface

  

In my previous post, Coding a Game of Memory in Delphi – OOP Model, I’ve been developing the model, aka the back end, for the Memory (Match Up, Concentration, …) game. The idea was to separate the game logic from the user interface (aka the front end). As a result a few classes were introduced: TPlayer, TField and, of course, the main class/object TMemoryGame implementing all the code required to run the game.
Having only the model does not help us much if we actually want to play the game. Therefore, this time, we go into building the user interface in Delphi.
Since the TMemoryGame class is framework agnostic (and not platform specific), it is up for you to decide if you would like to do a classic Windows VCL application, a FireMonkey Android mobile game or something that works on a Mac. To make it quick and simple, I’ll go old-style VCL school.

TL;DR: download full source code.
Delphi Memory 2D
Have a quick look at the image above – game of Memory in action. I wanted to have TMemoryGame fields displayed in some kind of grid layout – an ideal pick for this would be the TGridPanel control. We also need to allow specifying the number of fields and the number of players, and finally we need a button to start the game.
As the game engine was designed, there are pairs of fields having the same value from 1 to number of pairs. When a “card” is face down, the buttons show a question mark “?”. Upon opening the fields (card goes face up) the value of the field (button set as field’s host) is displayed. If two buttons are clicked having the same field value – we have a pair match.
But, let’s go one step at a time. First, the form has a lazy instantiated (simply my preference: create once really needed and not before) property MGame of TMemoryGame type:

type
TMainForm = class(TForm)

private
fMGame : TMemoryGame;
function GetMGame: TMemoryGame;
property MGame : TMemoryGame read GetMGame;

And we also have declarations for various handlers for events fired by the game:

procedure FieldClaimed(Sender : TObject; const mField : TMField);
procedure OpenField(Sender : TObject; const mField : TMField);
procedure CloseField(Sender : TObject; const mField : TMField);
procedure FieldsPaired(Sender : TObject; const mField1, mField2 : TMField);
procedure NextPlayer(Sender : TObject; const player : TPlayer);
procedure GameOver(Sender : TObject; const player : TPlayer);
procedure GameCreated(Sender : TObject);

You might note I’ve slightly altered the signature of events (when compared to previous post), to include the Sender parameter so the signatures are more Delphi style. Also, I’ve introduced the OnGameCreated event.
Here’s what the getter does (actually nothing special – create the game object and assign event handing procedures):

function TMainForm.GetMGame: TMemoryGame;
begin
if fMGame = nil then
begin
fMGame := TMemoryGame.Create;

fMGame.OnOpenField := OpenField;
fMGame.OnFieldClaimed := FieldClaimed;
fMGame.OnFieldOpened := FieldClaimed;
fMGame.OnCloseField := CloseField;
fMGame.OnFieldsPaired := FieldsPaired;
fMGame.OnNextPlayer := NextPlayer;
fMGame.OnGameStart := NextPlayer;
fMGame.OnGameOver := GameOver;
fMGame.OnGameCreated := GameCreated;

end;
result := fMGame;
end;

Clicking the “New game” buttons calls the MGame.NewGame method:

procedure TMainForm.btnNewGameClick(Sender: TObject);
var
newGamePairs, newGamePlayers : integer;
begin
newGamePairs := StrToInt(ledPairs.Text);
newGamePlayers := StrToInt(ledPlayers.Text);;

MGame.NewGame(newGamePairs, newGamePlayers);
end;

As presented in the previous post, the NewGame method will calculate the ideal square looking grid size (for the requested number of pairs), create the players and fields and assign them randomly pairs of values from 1 to newGamePairs.
The NewGame method would raise the OnGameCreated event so you can prepare the user interface:

procedure TMainForm.GameCreated(Sender: TObject);
var
i : integer;
aButton : TButton;
begin
UpdatePlayerStatistics;

begin //prepare GridPanel UI
gameGrid.RowCollection.BeginUpdate;
gameGrid.ColumnCollection.BeginUpdate;

for i := 0 to -1 + gameGrid.ControlCount do
gameGrid.Controls[0].Free;

gameGrid.RowCollection.Clear;
gameGrid.ColumnCollection.Clear;

for i := 1 to MGame.GameGridColumns do
with gameGrid.RowCollection.Add do
begin
SizeStyle := ssPercent;
Value := 100 / MGame.GameGridColumns;
end;

for i := 1 to MGame.GameGridRows do
with gameGrid.ColumnCollection.Add do
begin
SizeStyle := ssPercent;
Value := 100 / MGame.GameGridRows;
end;

//create playable hosts for fields
for i := 0 to -1 + MGame.Fields.Count do
begin
aButton := TButton.Create(self);
aButton.Parent := gameGrid;
aButton.Visible := true;
aButton.Font.Style := [fsBold];
aButton.Font.Size := 20;
aButton.Caption := ‘?’; //IntToStr(MGame.Fields[i].Value);
aButton.Align := alClient;
aButton.AlignWithMargins := true;

MGame.Fields[i].Host := aButton;
aButton.OnClick := MGame.FieldHostAction;
end;

gameGrid.RowCollection.EndUpdate;
gameGrid.ColumnCollection.EndUpdate;
end; //prepare GridPanel UI
end;

The code above dynamically adds rows, columns and controls to TGridPanel. The two most important lines of code above are:

MGame.Fields[i].Host := aButton;
aButton.OnClick := MGame.FieldHostAction;

A button is set to be the host for the game field and button’s OnClick is set to FieldHostAction – so when the user clicks the button – MGame.FieldHostAction gets called (look for the implementation in the previous post).
Let’s see some of the event handlers. Say “OnOpenField” – when a not-claimed / not-open field is selected / clicked to be opened:

procedure TMainForm.OpenField(Sender: TObject; const mField: TMField);
begin
TButton(mField.Host).Caption := mField.Value.ToString();
end;

Yes, super simple: just update the button caption to show field’s value.
When a field is closed (two opens did not result in a match):

procedure TMainForm.CloseField(Sender: TObject; const mField: TMField);
begin
TButton(mField.Host).Caption := ‘?’;
end;

FieldClaimed and FieldsPaired would simply change the visual state of button.
And so on …
I would not list all the code here, as you can download the full source code and play with it.
Let me know what you think and please do send in your FireMonkey mobile/mac versions

Comments are closed.