Skip to content

Lab Exercise 6 (Whac-A-Mole: Pyxel View)

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 View for an existing Model for Whac-A-Mole, using the pyxel Python library. The game will now be a "real-time" version (not turn-based).

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 Model and the Controller will be provided in the git repo; your job is to create the View using Pyxel, as well as extend the Model for some bonus tasks.

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.

Gameplay and Template

The base game will be running until the specified win condition is met. Moles are arranged roughly in a circle, indexed 0 to n - 1. The game will support six, seven, or eight moles.

Moles will pop up randomly, just like the arcade game version.

Each mole will stay active for a certain amount of time 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.


Model and Mole classes

The game will be developed using the following implementation specifications:

For the base game, an implementation of the SimpleMole class is provided, which will follow the Mole interface, implemented as an abstract base class (ABC). The Mole ABC inherits from the MoleInfo abstract base class.

The MoleInfo abstract base class supports the following interface:

  • @property

    @abstractmethod

    def base_hit_points(self) -> int: ... - Returns an int denoting the default hit points of a mole when created or when it pops up

  • @property

    @abstractmethod

    def base_active_ticks(self) -> int: ... - Returns an int denoting the default number of ticks/frames a mole stays ACTIVE before hiding.

  • @property

    @abstractmethod

    def base_cooldown_ticks(self) -> int: ... - Returns an int denoting the default number of ticks/frames a mole stays INACTIVE before being able to pop out again.

  • @property

    @abstractmethod

    def hit_points(self) -> int: ... - Returns an int denoting the current hit points of the mole

  • @property

    @abstractmethod

    def cooldown_ticks(self) -> int: ... - Returns an int denoting the current number of ticks/frames a mole stays INACTIVE before being able to pop out again.

  • @property

    @abstractmethod

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

  • @property

    @abstractmethod

    def is_active(self) -> bool: ... - Returns True if a mole is ACTIVE

  • @property

    @abstractmethod

    def is_dead(self) -> bool: ... - Returns True if a mole is HIT and its hit points are less than or equal to zero

  • @property

    @abstractmethod

    def state(self) -> MoleState: ... - Returns a MoleState value, which is an Enum that describes the state of the Mole with the following values: - ACTIVE - INACTIVE - HIT

The Mole class inherits from MoleInfo, and has the following additional attributes and methods:

  • @abstractmethod

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

  • @abstractmethod

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

  • @abstractmethod

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

  • @abstractmethod

    def start_tick(self) -> None (method) - Signals the start of the tick for the mole and updates the mole state accordingly.

  • @abstractmethod

    def end_tick(self) -> None (method) - Signals the end of the tick/frame for the mole and updates the mole state accordingly.

  • @abstractmethod

    def affect_moles(self, moles: Sequence[Mole]) (method) - Interact with other Moles.

All Mole implementations must follow this interface. You are free to specify the __init__() (initializer) method of Mole classes.

The given Model class has 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)
  • @property

    def is_game_over(self) -> bool - Returns a bool denoting if the game is over.

  • @property

    def moles_info(self) -> Sequence[MoleInfo] - Returns a Sequence[MoleInfo] containing the MoleInfos for the game instance.

  • @property

    def score total_points(self) -> int - Returns an int, denoting the current number of points the player has accumulated.

  • def update(self, click_idx: None | int) -> None

    • Updates the game state by one tick

The MolePopupPlan Protocol supports the following methods:

  • def __init__(self, ...)

    • You are free to specify all its parameters
  • def choose_moles_to_popup(self, current_tick: int, moles: Sequence[MoleInfo], 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_tick: int, points: int) -> bool

    • Determines if the game is over based on the mole information and game state.

Controller

A sample Controller implementation is provided with the template for this lab exercise.

Task

Main task: View

Your task is to write a View class using Pyxel for the Whac-a-mole game. The class must be able to support 6, 7, or 8 moles. The image below is a screenshot of a six-mole layout.

The seven-mole layout would be presented like the diagram below:

   1
2     6
   0
3     5
   4

And the eight-mole layout would be presented like the diagram below:

    1

2       7
    0
3       6

  4   5

Mouse left clicks should correspond to hammer whacks. A mole should hide once it gets hit and loses all of its hit points.

The current score should be displayed in the upper right corner of the screen.

The game should stop updating once the win condition is met.

The View class should have the following methods:

  • def __init__(self, width, height)

    • Takes two arguments for the width and height of the Pyxel screen.
  • def start_game(self, update_handler: UpdateHandler, draw_handler: DrawHandler) -> None

    • Entry point for the Pyxel runtime; this method will call at least pyxel.init(...) and pyxel.run(...)
  • def get_clicked_mole(self) -> int | None

    • Checks to see if any of the active moles have been clicked, and returns the corresponding index for the mole (based on the layouts). If no active moles were clicked, returns None.
  • def draw_moles(self, moles_info: Sequence[MoleInfo]) -> None:

    • Draws the moles on the Pyxel screen
  • def draw_moles(self, score: int) -> None:

    • Draws the score on the Pyxel screen
  • def reset_screen(self) -> None:

    • Resets the screen to one color

The UpdateHandler Protocol supports the following methods:

  • def update(self)
    • A method/function accepting no parameters, used for the Pyxel runtime

The DrawHandler Protocol supports the following methods:

  • def draw(self)
    • A method/function accepting no parameters, used for the Pyxel runtime

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 pyxel run main.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 functionalities for the Pyxel view, and can only edit view.py.

The view must support 6-8 moles, following the layouts detailed above.

In terms of graphics, we expect you to implement simple shapes only (built-in shapes in Pyxel).

There will be only one type of Mole for this phase, which is the SimpleMole.

A SimpleMole has the following features:

  • base_hit_points is 1

  • 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 every 30 ticks/frames, if there are x inactive moles, a random amount of moles from \(0\) to max(0, 2 // 3 * x) (inclusive) will pop up,

The base game will also use a SimpleGameOverCondition, where the game will end after you get a score of 20.

Phase 2 (\(40\) 🔴)

For Phase 2, you may modify moles.py and model.py (as well as controller.py).

With Phase 1 features still working, the game must support these additional types of Moles:

BombMole

  • base_hit_points is \(1\)

  • points is \(-5\)

LuckyMole

  • base_hit_points is \(1\)

  • points is \(2\)

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

RichMole

  • base_hit_points 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.

  • The adjacency of holes for ScaredyMole is defined as follows, for the three different layouts:
   1
2     5
   0
 3   4

0 adjacent to 1, 2, 3, 4, 5
1 adjacent to 0, 2, 5
2 adjacent to 0, 1, 3
3 adjacent to 0, 2, 4
4 adjacent to 0, 3, 5
5 adjacent to 0, 1, 4
   1
2     6
   0
3     5
   4

0 adjacent to 1, 2, 3, 4, 5, 6
1 adjacent to 0, 2, 6
2 adjacent to 0, 1, 3
3 adjacent to 0, 2, 4
4 adjacent to 0, 3, 5
5 adjacent to 0, 4, 6
6 adjacent to 0, 1, 5
    1

2       7
    0
3       6

  4   5

0 adjacent to 1, 2, 3, 4, 5, 6, 7
1 adjacent to 0, 2, 7
2 adjacent to 0, 1, 3
3 adjacent to 0, 2, 4
4 adjacent to 0, 3, 5
5 adjacent to 0, 4, 6
6 adjacent to 0, 5, 7
7 adjacent to 0, 1, 6

It is up to you to set the colors or indicators for the different mole types. Spawn a maximum of three (3) moles for each mole type.


Sprites bonus (\(20\) ❤️)

For the Sprites bonus, you can create your own sprites for each mole to replace the basic shapes. See https://kitao.github.io/pyxel/web/editor-manual/ for more details, and feel free to consult other resources.

Sound effects bonus (\(20\) ❤️)

For the Sound effects bonus, you can create your own sound effects or game music to play for the game. Add at least two sound effects, (1) for when you click and hit a mole, and (2) for when you click and don't hit a mole. See https://kitao.github.io/pyxel/web/editor-manual/ for more details, and feel free to consult other resources.

Submission

Via GitHub Classroom (by pair submission).

Your submission must have the following files:

controller.py
model.py
moles.py
view.py
main.py
README.md

Put all your Mole classes, MolePopupPlan classes, GameOverCondition classes, in moles.py. For the later phases, you can modify 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/uYc0sd42

Deadline: April 23 (Th), 11:33 PM