Lab Exercise 2 (Memory Game)
Context
Memory Game is a game in which a set of cards are all laid face down on a surface and two cards are flipped face up over each turn. The goal of the game is to turn over pairs of matching cards. Once a pair of cards match, they remain face-up for the rest of the game. The goal is to reveal all cards, in as few turns as possible.
This game can be played with any number of players or as a solitaire.
For this Lab Exercise, we will be implementing a slight variant of Memory Game.
All cards will come in pairs except for two types of cards, which will appear in trios. To match these trios, the player would have to flip all of them face up consecutively.
Your task is to implement a terminal-based version of Memory Game, making use of separation of concerns as well as subtyping.
Overview
This lab has checkpoint tasks; these tasks will count for bonus must be done during your lab session for your attendance to be counted.
For the purposes of these specs, we will assume that:
- The cards are arranged randomly in a grid.
- There should be at least 2 rows and 2 columns in the grid.
- A card is identified by its row and column number (1-indexed).
- The minimum number of cards for a game is 8 (two trios and one pair).
- The maximum number of cards for a game is 64 (two trios and 29 pairs).
You will be structuring your code using the Model-View-Controller design pattern. The view and the controller will be provided in the Git repo; your job is to create the model, as well as come up with unit tests for it.
Memory Game is played in turns; each turn starts by prompting the player to choose a card to flip face up, and then prompting one or two more times depending if the first card chosen belongs to a pair or trio.
If all chosen cards match (2 or 3), the cards are permanently revealed. Otherwise, they are flipped face down again.
A turn starts with a grid display and text indicating that no card was chosen yet. A prompt then follows asking for a row number and column number:
▮▮▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
Choose a row [1-3]: 1
Choose a column [1-4]: 1
Once (valid) row and column numbers are entered, the game will note the card chosen and display it face up:
%▮▮▮
▮▮▮▮
▮▮▮▮
You are looking for % .
Choose a row [1-3]:
If the card chosen belongs to a pair, the game will prompt the player again to open one more card.
%▮▮▮
▮▮▮▮
▮▮▮▮
You are looking for % .
Choose a row [1-3]: 1
Choose a column [1-4]: 3
The game will then display both cards face up.
%▮▮▮
▮▮▮▮
▮▮▮▮
You are looking for % .
Choose a row [1-3]: 1
Choose a column [1-4]: 3
%▮#▮
▮▮▮▮
▮▮▮▮
You have no active card.
If the second card chosen does not match the first one, the game will flip the cards face down in the next display.
%▮▮▮
▮▮▮▮
▮▮▮▮
You are looking for % .
Choose a row [1-3]: 1
Choose a column [1-4]: 3
%▮#▮
▮▮▮▮
▮▮▮▮
You have no active card.
▮▮▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
If the two cards match, they will remain face up in succeeding displays.
%▮▮▮
▮▮▮▮
▮▮▮▮
You are looking for % .
Choose a row [1-3]: 1
Choose a column [1-4]: 2
%%▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
%%▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
If the card opened belongs to a trio, the game will prompt the player to open up to two more cards.
%%▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
Choose a row [1-3]: 2
Choose a column [1-4]: 1
%%▮▮
!▮▮▮
▮▮▮▮
You are looking for ! .
Choose a row [1-3]: 2
Choose a column [1-4]: 2
If the second card opened does not match the first one, the prompt for a third card will not continue.
%%▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
Choose a row [1-3]: 2
Choose a column [1-4]: 1
%%▮▮
!▮▮▮
▮▮▮▮
You are looking for ! .
Choose a row [1-3]: 2
Choose a column [1-4]: 2
%%▮▮
!@▮▮
▮▮▮▮
You have no active card.
%%▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
If the second card opened matches but the third card does not, all the three cards will be flipped face down again.
%%▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
Choose a row [1-3]: 2
Choose a column [1-4]: 1
%%▮▮
!▮▮▮
▮▮▮▮
You are looking for ! .
Choose a row [1-3]: 2
Choose a column [1-4]: 4
%%▮▮
!▮▮!
▮▮▮▮
You are looking for ! .
Choose a row [1-3]: 4
Choose a column [1-4]: 1
%%▮▮
!▮▮!
@▮▮▮
You have no active card.
%%▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
If all three cards match, then all cards remain face up for the rest of the game.
%%▮▮
▮▮▮▮
▮▮▮▮
You have no active card.
Choose a row [1-3]: 2
Choose a column [1-4]: 1
%%▮▮
!▮▮▮
▮▮▮▮
You are looking for ! .
Choose a row [1-3]: 2
Choose a column [1-4]: 4
%%▮▮
!▮▮!
▮▮▮▮
You are looking for ! .
Choose a row [1-3]: 4
Choose a column [1-4]: 1
%%▮▮
!▮▮!
▮▮!▮
You have no active card.
%%▮▮
!▮▮!
▮▮!▮
You have no active card.
The game ends when all cards are revealed (face up).
%%#▮
!@#!
@@!▮
You have no active card.
Choose a row [1-3]: 1
Choose a column [1-4]: 4
%%#$
!@#!
@@!▮
You are looking for $
%%#$
!@#!
@@!▮
You are looking for $
Choose a row [1-3]: 3
Choose a column [1-4]: 4
%%#$
!@#!
@@!$
Game over! Turns: 12
Task
Ensure that you read the Notes at the end of this section!
When we say "formal argument", we mean the x and y in def f(x: int, y: int) -> int:.
Your model should be called MemoryGameModel, and it should have the following attributes and methods:
rows(attribute)- An
intdenoting the number of rows of the grid. - The value should be at least \(2\). Raise a
ValueErrorif this is violated. - This must also be a formal argument in the initializer.
- An
cols(attribute)- An
intdenoting the number of cols of the grid. - The value should be at least \(2\). Raise a
ValueErrorif this is violated. - \(rows \times cols\) should be even. Raise a
ValueErrorif this is violated. - \(rows \times cols\) should be at least \(6\). Raise a
ValueErrorif this is violated. - \(rows \times cols\) should be at most \(64\). Raise a
ValueErrorif this is violated. - This must also be a formal argument in the initializer.
- An
symbols(attribute)- A
strcontaining the symbols for the cards of the game. - The string must have unique characters.
- The first two characters are the symbols for the cards that appear in trios.
- The length of the string should be \(2 + (rows \times cols - 6) / 2\). Raise a
ValueErrorif this is violated. - This must also be a formal argument in the initializer.
- A
rng(attribute)- A
Randomobject denoting the random number generator used for the game. - This must also be a formal argument in the initializer.
- A
turns(@propertyattribute)- An
intdenoting the number of turns completed so far. - The initial value should be \(0\).
- An
dimensions(@propertyattribute)- Returns a
tuple[int, int]containing the number of rows and columns of the grid
- Returns a
active_card(@propertyattribute)- Returns a
strof length 1 containing the character of the currently chosen card to be matched. If there is no chosen card, returnNone
- Returns a
is_game_over(@propertyattribute)- Returns a
boolindicating if the game is over (all cards matched and face up)
- Returns a
get_display_grid(method)- Returns a
tuple[str, ...]showing the current state of the game grid.
- Returns a
flip_face_up(method)- Takes in two
intargumentsrowandcol. ReturnFalseif the given coordinates are out of bounds. - Attempts to flip the card at row
rowand columncolface up. If the card is face down, the card is flipped faced up, and the method returnsTrue - Otherwise, return
False - Calling the method must update the state of the grid correctly.
- Takes in two
reset(method)- Flips all unmatched cards that are revealed face down.
You may add additional fields and methods to the ones listed above, but your implementation will be tested using the prescribed fields and methods.
You must have unit tests (runnable via pytest) for the following aspects of the MemoryGameModel:
dimensionsactive_cardturnsget_display_grid,flip_face_upis_game_overandwinner
There must be unit tests that verify the state of the model by using both aspects in each pair above. For example, there must be unit tests verifying the value of turn after some number of calls to go_to_next_turn.
There must also be unit tests that simulate an entire game's worth of play. You should use a fixed random seed for the random generation of the grid (Random(seed) where seed is your seed). You may set rows and cols to smaller values to make testing this easier.
All unit tests must be runnable using pytest from the base directory.
Note
To make testing elements that involve randomness easier, you may use a fixed random number generator by making a Random object (passing in an integer as the seed).
For example, the following block of code will always print out the same five lines:
from random import Random
rng = Random(12)
for _ in range(5):
print(rng.randint(1, 10))
Meanwhile, the following block of code will (likely) have different outputs every time you run it:
from random import randint
for _ in range(5):
print(randint(1, 10))
Scoring
This lab exercise will be scored in phases. You can get \(0/20/50/80/100 \%\) of the points in a phase depending on how far/close you are from meeting the phase's requirements. You must get \(\ge 80 \%\) of the points in each of the previous phases to get nonzero points for a certain phase.
For example, if you get Phases 1, 2, \(80\%\) of Phase 3, and \(20\%\) of Phase 4, you get a total of \(30 + 60 + 30 \times 0.8 + 30 \times 0.2 = 120\) points for this lab.
This lab will be scored over \(100\) 🔴.
Note
For all phases, your program should be runnable using the command python3 memory_game.py.
Indicate the farthest phase you (think you) got in a README.md file.
Sample memory_game.py
# pyright: strict
from random import Random
from model import MemoryGameModel
from view import MemoryGameView
from controller import MemoryGameController
if __name__ == '__main__':
r = Random()
model = MemoryGameModel(4, 3, '!@#$%', r)
view = MemoryGameView()
controller = MemoryGameController(model, view)
controller.run()
Phase 1 (\(30\) 🔴)
For this phase, you only need to implement a working version of our modified Memory Game. You do not need to follow the Model-View-Controller design pattern or make unit tests.
You may ignore MemoryGameView and MemoryGameController or use some of their contents if you want to.
This phase is skippable; if you get \(\ge 80\%\) of the points for Phase 2, you immediately get full points for this phase.
Phase 2 (\(70\) 🔴)
For this phase, you must follow everything outlined in the Tasks section; that is, you must follow the Model-View-Controller design pattern, and you must have unit tests.
Phase 3 (\(30\) 🔴)
For this phase, you must have obtained \(\ge 80\%\) of the points in Phase 2, as well as support the following additional features:
- Allow multiple symbols beyond trios.
The maximum number of cards for a game will now be \(100\). The symbols argument of the Model must now have length the same as the number of cards in the grid, and must contain characters that correspond exactly with the number of copies of each card. Cards can now have more than three copies.
For each unique card type, there should be at least two copies.
For example, for a \(4 \times 4\) grid, the symbols argument could be:
$$!$!##*!!%%*%!!
Here, there are 2 #, 2 *, 3 $, 3 %, and 6 !.
Raise a ValueError in the Model initializer if any of the conditions above are violated.
To match a type of card, all cards of the same type must be flipped up consecutively. If a mismatch happens at any point, the cards will not be permanently revealed.
Your modifications must still properly adhere to the Model-View-Controller design pattern. How the UI for these additional features will work is up to you; at the very least, the tester should have little to no difficulty making use of these additional features.
You must also have modified unit tests for these changes.
Submission
Checkpoint
4x4 Standard Memory Game (20 ❤️)
Before the lab session ends, implement a standard Memory Game in Python with a \(4 \times 4\) randomly-generated grid, with only pairs of cards. Your code here does not have to adhere to MVC.
Call the attention of your lab instructor once you are done with the checkpoint task. Once your 4x4 Standard Memory Game game has been verified to work five (5) minutes before the lab session ends, you will gain 20 ❤️ points, and your attendance will be recorded (and you may leave early).
If you are not able to finish the checkpoint task within the lab time, your attendance will be recorded at the very end of the lab session.
Final Submission
Via DCS Yagit (https://yagit.upd-dcs.work/).
Deadline: September 9, 2026 (W) 11:12 pm