Lab Exercise 3 (Whac-A-Mole: Terminal version)
Note
Please see the patch notes at /lab03patch.
Context
Whac-A-Mole is a Japanese arcade game that was created in 1975 by the amusements manufacturer TOGO in Japan. A typical Whac-A-Mole arcade machine has a play area and display screen, and a large mallet. Five to eight holes in the play area are occupied by plastic, cartoonish moles (or other characters), which pop up at random. Points are scored by whacking each mole as they appear. The faster the reaction, the higher the score.
For this lab exercise, you are to implement a terminal-based, turn-based version of this arcade game, in which several types of moles could appear, and hitting different types of moles earn different amounts of points, and could even trigger strange new behaviors!
Overview
You will be structuring your code using the Model-View-Controller design pattern, and applying some Object-Oriented Programming principles you learned so far.
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.
For succeeding phases of the lab exercise, more mole types will be available via subtyping, and custom behavior for the game's randomization and end game condition will be injected.
Task
The game will be running for a set amount of turns. Moles are arranged in a circle, indexed 0 to n - 1, where index i is adjacent to index i + 1, and index n - 1 is adjacent to index 0.
Moles will pop up randomly each turn, just like the arcade game version.
Each mole will stay active for a certain number of turns and then hide. If a mole gets hit and loses all its hit points, it will be forced to hide in the same turn. The player will gain points upon forcing a mole to hide.
See the Scoring section below for more details on the behavior of the moles.
At the start of each turn, the game will ask for an integer input, indicating the index of the hole that the player wants to hit with the hammer/mallet.
The game will be developed using the following implementation specifications:
When we say "formal argument", we mean the x and y in def f(x: int, y: int) -> int:.
For the base game, you must implement a SimpleMole class, which will follow the Mole interface, implemented as a Protocol. The Mole Protocol will also follow the MoleInfo Protocol.
The MoleInfo Protocol contains the following attributes and methods:
-
def __str__(self) -> str(Python dunder method)- Returns a
strrepresentation of the object.
- Returns a
-
def base_hit_points(self) -> int(@propertyattribute)- Returns an
intdenoting the default hit points of a mole when created or when it pops up
- Returns an
-
def base_active_turns(self) -> int(@propertyattribute)- Returns an
intdenoting the default number of turns a mole staysACTIVEbefore hiding.
- Returns an
-
def hit_points(self) -> int(@propertyattribute)- Returns an
intdenoting the current hit points of the mole
- Returns an
-
def points(self) -> int(@propertyattribute)- Returns an
int, which is the amount of points the player gains if thisMole's hit points is reduced to zero (or less).
- Returns an
-
def state(self) -> MoleState(@propertyattribute)- Returns a
MoleStatevalue, which is anEnumthat describes the state of theMolewith the following values:ACTIVEINACTIVEHIT
- Returns a
You are free to specify the parameters of the initializer of a MoleInfo.
The Mole Protocol contains the following attributes and methods (note that it also contains the attributes in MoleInfo):
-
def __str__(self) -> str(Python dunder method) -
def base_hit_points(self) -> int(@propertyattribute) -
def base_active_turns(self) -> int(@propertyattribute) -
def hit_points(self) -> int(@propertyattribute) -
def points(self) -> int(@propertyattribute) -
def state(self) -> MoleState(@propertyattribute) -
def prepare(self) -> None(method)- Initializes or resets the state of a mole to
INACTIVE, as well as its hit points and active turns - Can contain additional behavior
- Initializes or resets the state of a mole to
-
def pop_up(self) -> None(method)- Sets the state of the mole to
ACTIVE, and does any additional behavior.
- Sets the state of the mole to
-
def hide(self) -> None(method)- Sets the state of the mole to
INACTIVE, and does any additional behavior.
- Sets the state of the mole to
-
def receive_hit(self, damage: int) -> None(method)- Sets the state of the mole to
HIT, and does any additional behavior.
- Sets the state of the mole to
-
def elapse_turn(self) -> None(method)- Signals the end of turn for the mole and updates the mole state accordingly.
-
def affect_moles(self, moles: Sequence[Mole]) (method)- Interact with other
Moles.
- Interact with other
All Mole implementations must follow this interface/Protocol. You are free to specify the __init__() (initializer) method of Mole classes.
In addition, you will need to implement a model class, as well as some additional helper classes.
Your model should be called WhacAMoleModel, and it should have the following attributes and methods:
-
def __init__(self, moles: Sequence[Mole], rng: Random, hammer_damage: int, mole_popup_plan: MolePopupPlan, win_condition: GameOverCondition)- Takes in
moleswhich is aSequenceofMoleinstances to be used by the game. Adding or removing elements from themoleslist should not affect the state of the model. - Takes in an
rngRandominstance, to be used for all random number generation. - Takes in an
inthammer_damage, which is the damage thatMoles take when hit, in this game. - Takes in a
MolePopupPlaninstance, which is used to configure how the moles pop up each turn (see details below) - Takes in a
GameOverConditioninstance, which specifies when the game ends (see details below)
- Takes in
-
def moles_info(self) -> Sequence[MoleInfo](@propertyattribute)- Returns a
Sequence[MoleInfo]containing theMoleInfos for the game instance.
- Returns a
-
def current_turn(self) -> int(@propertyattribute)- Returns an
int, denoting the current turn.
- Returns an
-
def total_points(self) -> int(@propertyattribute)- Returns an
int, denoting the current number of points the player has accumulated.
- Returns an
-
def rng(self) -> Random(@propertyattribute)- Returns the
Randominstance used by this model.
- Returns the
-
def is_game_over(self) -> bool(@propertyattribute)- Returns a
booldenoting if the game is over.
- Returns a
-
def process_hit(self, idx: int) -> None(method)- Takes in an
intidx, the 0-based index of the mole to be hit - Processes the hit and updates the game state accordingly.
- Takes in an
-
def start_turn(self) -> None(method)- Initializes the turn, and chooses some or all
INACTIVEMoles, and makes them pop up, based onMolePopupPlan(see the Scoring section below for more details).
- Initializes the turn, and chooses some or all
-
def finish_turn(self) -> None(method)- Updates the state of the
Moles and game state.
- Updates the state of the
The MolePopupPlan Protocol supports the following methods:
-
def __init__(self, ...)- You are free to specify all its parameters
-
def choose_moles_to_popup(self, moles: Sequence[MoleInfo], current_turn: int, rng: Random) -> list[int]- Returns a
list[int]indicating the indices of moles to pop up.
- Returns a
The GameOverCondition Protocol supports the following methods:
-
def __init__(self, ...)- You are free to specify all its parameters
-
def is_game_over(self, moles: Sequence[MoleInfo], current_turn: int, points: int) -> bool- Determines if the game is over based on the mole information and game state.
You must have unit tests (runnable via pytest) to check the properties and methods of MoleInfo, Mole, and the WhacAMole model, as well as the MolePopupPlan and GameOverCondition.
There must be unit tests that verify the state of the model related to the attributes and methods above.
Aim for \(100\%\) unit test coverage for WhacAMoleModel model and all the auxiliary classes (Mole classes, WhacAMoleModel, MolePopupPlan, GameOverCondition).
There must also be unit tests that simulate an entire game's worth of play. You should use a fixed random seed for this (Random(seed) where seed is your fixed integer seed). You may set turns and the number of unique Moles to smaller/shorter values to make testing this easier.
Note
To make testing elements that involve randomness deterministic, 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.
This lab will be scored over \(100\) 🔴.
Note
For all phases, your program should be runnable using the command python3 whacamole.py.
Indicate the farthest phase you (think you) got in a README.md file.
Phase 1 (\(60\) 🔴)
For this phase, you will implement the base game functionalities.
There must be exactly six (\(6\)) moles present for the base game.
There will be only one type of Mole for the base game, which is the SimpleMole.
A SimpleMole has the following features:
-
base_hit_pointsis 1 -
base_active_turnsis 2 -
pointsis 1 -
No additional behavior.
At the start of the game, before the first turn, all the Moles are INACTIVE.
The base game will use a SimpleMolePopupPlan, where at start of each turn,
if there are x inactive moles, a random amount of moles from \(0\) to x // 2 (inclusive) will pop up, except for the very first turn, where there should be at least 1 mole that should pop up.
The base game will also use a SimpleGameOverCondition, where the game will end after twenty (\(20\)) turns.
Phase 2 (\(40\) 🔴)
With Phase 1 features still working, the game must support these additional types of Moles:
BombMole
-
base_hit_pointsis \(1\) -
base_active_turnsis \(3\) -
pointsis \(-5\)
LuckyMole
-
base_hit_pointsis \(1\) -
base_active_turnsis \(2\) -
pointsis \(2\) -
When targeted to be hit, has a \(50\%\) chance to dodge the incoming hit.
RichMole
-
base_hit_pointsis \(1\) -
base_active_turnsis \(2\) -
pointsis \(1\) -
Every time the mole goes into hiding without getting hit, its point value increases by 1.
ScaredyMole
-
base_hit_pointsis \(1\) -
base_active_turnsis \(2\) -
pointsis \(1\) -
If a mole is forced into hiding after getting hit, any moles adjacent to it become
INACTIVEand hide.
For Phase 2, spawn three (3) SimpleMoles, and then one (1) of each of the new mole types (BombMole, LuckyMole, RichMole, ScaredyMole).
In addition, at the start of each turn, the positions of INACTIVE Moles will be shuffled randomly (use the rng Random instance).
Phase 2 will also use SimpleMolePopupPlan and SimpleGameOverCondition.
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 unit tests for these additional Moles and their behavior.
Phase 3 (\(40\) 🔴)
For Phase 3, you should have at least \(6\) moles (can have more), of any of the types described above.
In addition to the Phase 2 features, the game should be able to support the following additional GameOverConditions:
GoalPoints
- The game ends when the player accumulates at least
pnumber of points, which is an input to the initializer of thisGameOverCondition.
ThreeNPlusOne
- Let
nbe the number of moles in the game.- If there are an even number of moles, hit at least
n // 2mole into hiding. - If there are an odd number of moles, hit at least \(3n + 1\) moles into hiding.
- If there are an even number of moles, hit at least
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 unit tests for these additional features.
Submission
Via GitHub Classroom (individual submission).
Your submission must have the following files:
whacamole.py
model.py
protocols.py
README.md
Put all your Mole classes, MolePopupPlan classes, GameOverCondition classes, and model implementation in model.py. See the template repository in GitHub Classroom.
Unit test files should also be in the same directory. \(100\%\) code coverage is expected for the model.py file.
GitHub Classroom link: https://classroom.github.com/a/838fXquB
Deadline: February 23, 2026 (M) 11:12 pm