Skip to content

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 str representation of the object.
  • def base_hit_points(self) -> int (@property attribute)

    • Returns an int denoting the default hit points of a mole when created or when it pops up
  • def base_active_turns(self) -> int (@property attribute)

    • Returns an int denoting the default number of turns a mole stays ACTIVE before hiding.
  • def hit_points(self) -> int (@property attribute)

    • Returns an int denoting the current hit points of the mole
  • def points(self) -> int (@property attribute)

    • Returns an int, which is the amount of points the player gains if this Mole's hit points is reduced to zero (or less).
  • def state(self) -> MoleState (@property attribute)

    • Returns a MoleState value, which is an Enum that describes the state of the Mole with the following values:
      • ACTIVE
      • INACTIVE
      • HIT

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 (@property attribute)

  • def base_active_turns(self) -> int (@property attribute)

  • def hit_points(self) -> int (@property attribute)

  • def points(self) -> int (@property attribute)

  • def state(self) -> MoleState (@property attribute)

  • 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
  • def pop_up(self) -> None (method)

    • Sets the state of the mole to ACTIVE, and does any additional behavior.
  • def hide(self) -> None (method)

    • Sets the state of the mole to INACTIVE, and does any additional behavior.
  • def receive_hit(self, damage: int) -> None (method)

    • Sets the state of the mole to HIT, and does any additional behavior.
  • 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.

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 moles which is a Sequence of Mole instances to be used by the game. Adding or removing elements from the moles list should not affect the state of the model.
    • Takes in an rng Random instance, to be used for all random number generation.
    • Takes in an int hammer_damage, which is the damage that Moles take when hit, in this game.
    • Takes in a MolePopupPlan instance, which is used to configure how the moles pop up each turn (see details below)
    • Takes in a GameOverCondition instance, which specifies when the game ends (see details below)
  • def moles_info(self) -> Sequence[MoleInfo] (@property attribute)

    • Returns a Sequence[MoleInfo] containing the MoleInfos for the game instance.
  • def current_turn(self) -> int (@property attribute)

    • Returns an int, denoting the current turn.
  • def total_points(self) -> int (@property attribute)

    • Returns an int, denoting the current number of points the player has accumulated.
  • def rng(self) -> Random (@property attribute)

    • Returns the Random instance used by this model.
  • def is_game_over(self) -> bool (@property attribute)

    • Returns a bool denoting if the game is over.
  • def process_hit(self, idx: int) -> None (method)

    • Takes in an int idx, the 0-based index of the mole to be hit
    • Processes the hit and updates the game state accordingly.
  • def start_turn(self) -> None (method)

    • Initializes the turn, and chooses some or all INACTIVE Moles, and makes them pop up, based on MolePopupPlan (see the Scoring section below for more details).
  • def finish_turn(self) -> None (method)

    • Updates the state of the Moles and game state.

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.

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_points is 1

  • base_active_turns is 2

  • points is 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_points is \(1\)

  • base_active_turns is \(3\)

  • points is \(-5\)

LuckyMole

  • base_hit_points is \(1\)

  • base_active_turns is \(2\)

  • points is \(2\)

  • When targeted to be hit, has a \(50\%\) chance to dodge the incoming hit.

RichMole

  • base_hit_points is \(1\)

  • base_active_turns is \(2\)

  • points is \(1\)

  • Every time the mole goes into hiding without getting hit, its point value increases by 1.

ScaredyMole

  • base_hit_points is \(1\)

  • base_active_turns is \(2\)

  • points is \(1\)

  • If a mole is forced into hiding after getting hit, any moles adjacent to it become INACTIVE and 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 p number of points, which is an input to the initializer of this GameOverCondition.

ThreeNPlusOne

  • Let n be the number of moles in the game.
    • If there are an even number of moles, hit at least n // 2 mole into hiding.
    • If there are an odd number of moles, hit at least \(3n + 1\) moles into hiding.

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