Coding a Game of Memory in Delphi – The User Interface

Delphi Memory game in actionIn 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 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.