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:
- 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).
- 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).