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
    fName: string;
    fClaimedPairs: integer;
    property Name : string read fName write fName;
    property ClaimedPairs : integer read fClaimedPairs;
    constructor Create(const name : string);

The implementation part is for the constructor:

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

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
    fValue: integer;
    fHost: TObject;
    fPlayer: TPlayer;
    property Value : integer read fValue;
    property Host : TObject read fHost write fHost;
    property Player : TPlayer read fPlayer;
    constructor Create(const value : integer);

Again, only the constructor needs the implementation:

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

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;

  TMemoryGame = class
    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;
    property OpenFirst : boolean read fOpenFirst write fOpenFirst;
    property OpenedField : TMField read fOpenedField write fOpenedField;

    property ClaimedPairs : integer read fClaimedPairs;
    function AllPairsClaimed : boolean;
    constructor Create;
    destructor Destroy; override;
    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;

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;
  i, rnd : integer;
  aField : TMField;
  newPlayer : TPlayer;

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

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


  for i := 1 to PlayersCount do
    newPlayer := TPlayer.Create('player ' + i.ToString());
    if Assigned(fOnPlayerCreated) then fOnPlayerCreated(newPlayer);
  fCurrentPlayer := Players.First;

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


  //randomize field positions
    function(const Left, Right : TMField) : integer
      result := -1 + Random(3);

  fClaimedPairs := 0;
  OpenFirst := true;

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

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);
  actionOnField: TMField;
  winner, aPlayer : TPlayer;

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

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

      if Assigned(fOnOpenField) then fOnOpenField(actionOnField);

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

        if OpenedField.Value = actionOnField.Value then //we have a match
          OpenedField.fPlayer := CurrentPlayer;
          actionOnField.fPlayer := CurrentPlayer;

          CurrentPlayer.fClaimedPairs := 1 + CurrentPlayer.ClaimedPairs;

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

          if AllPairsClaimed then
            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);
        else //no mach pair
          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
            fCurrentPlayer := Players[1 + Players.IndexOf(CurrentPlayer)];

          if Assigned(fOnNextPlayer) then fOnNextPlayer(CurrentPlayer);

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

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;
  result := ClaimedPairs = PairsCount;

constructor TMemoryGame.Create;
  OpenFirst := true;

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

destructor TMemoryGame.Destroy;


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.