Coding a Game of Memory in Delphi – OOP Model

Memory game vs robot at CosmoCaixa, Barcelona 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 during 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 OnFieldOpened : TMemoryGameFieldEvent read fOnFieldOpened write fOnFieldOpened;
    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
  ));

  fClaimedPairs := 0;
  OpenFirst := true;

  //let's start...
  if Assigned(fOnGameStart) then fOnGameStart(CurrentPlayer);
end;

I’m hoping the code is self-explanatory, hehe. In essence, the wanted 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 via 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;
begin
  actionOnField := FieldByHost(sender);
  if actionOnField = nil then Exit;

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

      if Assigned(fOnOpenField) then fOnOpenField(actionOnField);

      OpenFirst := false;
    end
    else //open second
    begin
      if actionOnField = OpenedField then //cannot double open
      begin
        if Assigned(fOnFieldOpened) then fOnFieldOpened(actionOnField)
      end
      else
      begin
        if Assigned(fOnOpenField) then fOnOpenField(actionOnField);

        if OpenedField.Value = actionOnField.Value then //we have a match
        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 number 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 //no mach pair
        begin
          Sleep(500); //todo: promote interval to property or event
          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;

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

The above is actually the memory game implementation in full.

So, 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:
    1. If first card to 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).
    2. If second card to open:
      • If OpenedField = actionOnField raise the OnFieldOpened as one cannot double open a field.
      • If OpenedField.VALUE = actionOnField.VALUE a player has matched a pair. If AllPairsClaimed raise the OnGameOver (and find the winner).
      • If OpenedField.Value <> actionOnField.Value 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).

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.