# You must make sure to run all cells in sequence using shift + enter or you might encounter errors
from pykubegrader.initialize import initialize_assignment
responses = initialize_assignment("1_PacMan", "week_6", "lab", assignment_points = 61.0, assignment_tag = 'week6-lab')
# Initialize Otter
import otter
grader = otter.Notebook("1_PacMan.ipynb")
๐น๏ธ Pac-Man Python Challenge: Master Code Modularity & Functions! ๐๐ป#
Welcome, brave coder! ๐ Today, you embark on a legendary questโto build your very own Pac-Man using Python! ๐ฉ๐ฑโ๐
In this challenge, youโll learn how to break down complex game mechanics into modular, reusable functionsโjust like Pac-Man breaks down pellets! ๐ฝ๏ธโ
๐พ Challenge Breakdown#
๐ฎ Step 1: Move Pac-Man around the board using functions.
๐ป Step 2: Make the ghosts chase Pac-Man (but donโt let them cheat!).
โก Step 3: Add power pelletsโPac-Manโs moment of glory!
๐ Step 4: Implement scoring
๐ Why is this Fun?#
Eat pellets, chase high scores, and outsmart ghosts!
Write clean, modular code like a true game developer.
See your game come to life with every function you write!
You can modify the code to ensure victory for Pac.
๐ Ready? Get Setโฆ CODE!#
Fire up your Python editor, and letโs make some gaming magic happen! ๐น๏ธโจ
โWaka Waka! Letโs modularize that code!โ โ Pac-Man, probably. ๐๐พ
Imports and Configuration#
import ipywidgets as widgets
from IPython.display import display
import random
from collections import deque
# ---------------------------------------------
# CONFIGURATION
# ---------------------------------------------
PACMAN_LIVES = 3
GHOST_SPAWN_INCREMENT = 0.05 # how much the spawn probability increases each turn
INITIAL_GHOST_SPAWN_PROB = 0.0
BOARD_LAYOUT = [
"###########",
"#....G....#",
"#...###...#",
"#...# #...#",
"#...# #...#",
"#...###...#",
"#.........#",
"#.###.###.#",
"#.# #.#",
"#.###.###.#",
"#.........#",
"#...###...#",
"#...# #...#",
"#...# #...#",
"#...###...#",
"#....P....#",
"###########",
]
ROWS = len(BOARD_LAYOUT)
COLS = len(BOARD_LAYOUT[0])
# ---------------------------------------------
# EMOJI MAP
# ---------------------------------------------
symbol_map = {
"#": "๐งฑ", # Wall
".": "๐", # Pellet
" ": "โซ", # Empty
"P": "๐", # Pac-Man
"G": "๐ป", # Ghost
}
Setup the board and Data Structures#
It is time to create the board and the data structures that will hold the game state. We will provide details in the inline comments to guide you through the implementation.
# Convert board into a 2D list for mutability
# loop through each row in the board layout and convert it to a list
# make a list that is a list of rows, try to do this in a single line
# save the result in a variable `board`
...
# We'll collect all ghost positions in a list
# create an empty list called `ghosts`
# save the result in a variable called ghosts
...
# set the pacman position to [0, 0], the variable `pacman_pos``
...
# set the pacman start position to [0, 0], the variable `pacman_start``
...
# set the total pellets to 0, the variable total_pellets
...
# set the variable pacman_lives to PACMAN_LIVES from the configuration
...
# set the variable `score`` to 0
...
# set the variable `game_over` to False
...
# Probability that a new ghost spawns this turn
# set the variable `ghost_spawn_prob` to INITIAL_GHOST_SPAWN_PROB
...
grader.check("Initial-Parameters")
Question 2 (Points: 5.0): Question 1 (Points: 9.0): Parse the Board#
We want to parse the board and create a data structure that will hold the game state. We will provide details in the inline comments to guide you through the implementation.
# Parse board to find Pac-Man, initial Ghost(s), and count pellets
# loop through each row, typically denoted by j
...
# loop through each column
# typically denoted by i
...
# get the character at the current position, save it in a variable called `ch`
...
# if the character is a pellet ".", increment the `total_pellets` count
...
# if the character is a P, set the pacman_pos to the current position, and the pacman_start to the current position
# Remove the 'P' from the board, and replace with a " ", a space.
...
# if the character is a G, add the current position to the ghosts list
# Remove the 'P' from the board, and replace with a " ", a space.
...
grader.check("Parse-Board")
def valid_move(board, x, y):
"""
Return True if (x, y) is inside the board and not a wall (#).
"""
# if x is less than 0; or x is greater than or equal to COLS; or y is less than 0 or y is greater than or equal to ROWS, return False
# this means the move is not valid
...
# return True if the position is not a wall
# the wall is represented by the character '#' in the board
# You must also check that the position is within the board boundaries
...
grader.check("valid-move-function")
Question 3 (Points: 7.0): Ghost Step#
We have provided you with the function to move the ghosts. The logic is somewhat complex, look at the inline comments to understand how it works.
def ghost_bfs_step(gx, gy, px, py):
"""
Return the next cell (nx, ny) for a ghost at (gx, gy) to move toward
Pac-Man at (px, py) using BFS. If path is found, we return the *next step*.
If no path is found, return None.
"""
# if the ghost position is the same as the pacman position, return the ghost position
if (gx, gy) == (px, py):
return (gx, gy)
# create a queue and add the ghost position to it
# create a set called visited
queue = deque()
visited = set()
parent = {}
# add the ghost position to the queue and the visited set
queue.append((gx, gy))
visited.add((gx, gy))
found = False
# create a list of directions
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
# while the queue is not empty
while queue:
# pop the first element from the queue and save it in cx, cy
cx, cy = queue.popleft()
# if the ghost position is the same as the pacman position, set found to True and break
if (cx, cy) == (px, py):
found = True
break
# loop through each direction in the directions list
for dx, dy in directions:
nx, ny = cx + dx, cy + dy
if valid_move(board,nx, ny) and (nx, ny) not in visited:
visited.add((nx, ny))
parent[(nx, ny)] = (cx, cy)
queue.append((nx, ny))
if not found:
return None
# reconstruct path from Pac-Man back to Ghost
path = []
node = (px, py)
while node != (gx, gy):
path.append(node)
node = parent[node]
path.append((gx, gy))
path.reverse()
if len(path) > 1:
return path[1] # next step
else:
return None
# Define a function called `move_ghost_towards_pacman`` that takes the board ghost position gx, gy and the pacman position px, py as arguments
...
"""
Move a ghost in the board toward Pac-Man using BFS.
First, this function attempts to determine the next step via a BFS-based approach,
using the helper function `ghost_bfs_step(gx, gy, px, py)`. If no valid path is found
(i.e., `ghost_bfs_step` returns None), it falls back to a naive random step.
Parameters
----------
board : list of lists
A 2D board or grid representing the current game state. The exact representation
can vary, but must be compatible with the `valid_move` checks.
gx : int
The current x-coordinate of the ghost.
gy : int
The current y-coordinate of the ghost.
px : int
The x-coordinate of Pac-Man.
py : int
The y-coordinate of Pac-Man.
Returns
-------
(nx, ny) : tuple of int
The new coordinates of the ghost after attempting to move closer to Pac-Man.
If BFS fails or yields an invalid step, a random valid move is attempted;
if no valid random move is found, the ghost remains in place.
Notes
-----
- The BFS step is determined via an external function `ghost_bfs_step`. If this step
is valid (in-bounds and not blocked), it is taken. Otherwise, the function attempts
random neighboring moves (up, down, left, right) in a shuffled order.
- The `valid_move(board, x, y)` helper is used to check the validity of potential moves.
Examples
--------
>>> # Assuming ghost_bfs_step and valid_move are defined:
>>> new_gx, new_gy = move_ghost_towards_pacman(game_board, ghost_x, ghost_y, pacman_x, pacman_y)
>>> print(new_gx, new_gy)
4 3
"""
# get the next step from the ghost_bfs_step function, look at how to call the function
...
# if the step is not None, return the step to nx, ny
# Make sure the step is a valid move, if it is return the step position, if not, return current ghost position (i.e., have the ghost stay in place)
...
# if the step is None, do a random step
...
# we provided this code which defines a list of possible directions
# fallback: random step
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
# Shuffle the directions list use the random.shuffle function, this will shuffle the list in place
...
# this will loop around the directions in order, recall that we already shuffled the list
for dx, dy in directions:
nx, ny = gx + dx, gy + dy
# if the move is valid, return the new position return (nx, ny) the new position
# use the valid_move function to check if the move is valid
...
# If not valid move, return the original ghost position as a tuple
return (gx, gy)
grader.check("move-ghost-forward")
Spawning New Ghosts#
Throughout the game you should randomly spawn a new ghost so that pac is not being chased by just one ghosts all the time. We will provide details in the inline comments to guide you through the implementation.
# Define a function to spawn a new ghost
...
"""
Spawn a new ghost on the board at a random valid position.
This function selects a random (x, y) coordinate within the bounds of the board,
ensuring:
1. The position is valid (not blocked, and presumably not out of bounds).
2. The position is not on the edge of the board (as specified by the subtracted range).
3. The position does not overlap with Pac-Man or any existing ghosts.
Parameters
----------
board : list
A 2D list (or equivalent structure) representing the game board state. The details
of this structure (e.g., wall representation, free cells, etc.) are assumed to be
handled by the `valid_move` function.
Returns
-------
list
A list containing the [x, y] coordinates where a new ghost has been successfully
placed on the board.
Notes
-----
- The function uses an infinite loop because it is theoretically guaranteed that there
is at least one valid position for the ghost to spawn. Once found, the position is returned.
- It relies on the helper function `valid_move(board, x, y)` to check whether a position
is valid, and additional checks ensure no collision with Pac-Man or existing ghosts.
Examples
--------
>>> # Example usage (assuming all required global variables and functions are defined):
>>> new_ghost_position = spawn_new_ghost(game_board)
>>> print(new_ghost_position)
[5, 3]
"""
# loop forever, this is one of the strange cases where it's okay to loop forever, because we know there is a valid move, and we have a return statement.
...
# generate a random x and y position, in the range of 0 to COLS and 0 to ROWS, make sure you cannot spawn on the edge of the board by subtracting 1 from the range
# set this to rx and ry
...
# Now we need to check if the position to spawn a ghost is valid, if it is, return the position
# We have provided this code for you, you can use it as is
# this uses the valid_move function to check if the position is valid
if valid_move(board, rx, ry):
# Also ensure it's not on top of Pac-Man or another ghost
# (optional, but let's do so to avoid instant collisions)
if (rx, ry) != tuple(pacman_pos) and all(
(rx, ry) != tuple(g) for g in ghosts
):
return [rx, ry]
grader.check("spawn-new-ghost-function")
# Write a function called check_collisions, this should take no arguments
...
"""
Check if any ghost is on Pac-Man's current tile.
This function compares the global `pacman_pos` (pacman's current position)
with each ghost's position in the global `ghosts` list. If a match is found
(meaning at least one ghost occupies the same (x, y) coordinates as Pac-Man),
it immediately returns True to indicate a collision; otherwise, it returns False.
Returns
-------
bool
True if at least one ghost collides with Pac-Man, False otherwise.
Notes
-----
- This function assumes the existence of two global variables:
1. `pacman_pos`, a tuple (px, py) representing Pac-Man's position.
2. `ghosts`, a list of tuples [(gx1, gy1), (gx2, gy2), ...] representing
the positions of all ghosts.
- The global scope is used here to simplify the function signature; no
parameters are directly passed in.
Examples
--------
>>> # Suppose pacman_pos = (5, 3) and ghosts = [(1, 1), (2, 2), (5, 3)]
>>> check_collisions()
True
"""
# extract the pacman position into px, py, you can do this by accessing the global variable pacman_pos
...
# loop through each ghost position in the ghosts list, and unpack the position into gx, gy
...
# if the ghost position is the same as the pacman position, return True
# if not, return False
...
grader.check("check-for-collision-function")
Question 6 (Points: 3.0): Question 5 (Points: 3.0): Render the Board#
We now need to convert the text representation of the board into a visual representation. This process is called rendering. We will provide details in the inline comments to guide you through the implementation.
# Define a function to render the board as the variable `render_board`
...
"""
Build and return an HTML-rendered string representing the current game board,
including Pac-Man (if still alive) and all ghosts.
This function operates on several global variables:
- board (list of lists): The base board layout, where each cell typically
contains a character indicating walls, empty spaces, or other structures.
- pacman_lives (int): Indicates the remaining number of lives Pac-Man has.
Pac-Man is only rendered if this is greater than 0 and the game is not over.
- pacman_pos (tuple): A (px, py) tuple indicating Pac-Man's current position.
- ghosts (list): A list of (gx, gy) tuples representing each ghost's position.
- game_over (bool): A flag indicating whether the game has ended.
- symbol_map (dict): A mapping from board symbols (e.g., 'P', 'G', '#') to
associated emoji or character symbols for rendering.
Returns
-------
str
A string containing HTML <pre> markup that renders the board in a monospace
font. Pac-Man is shown as 'P' (mapped via symbol_map) when alive and the game
is not over, while ghosts are shown as 'G'. Any other board symbols are mapped
through the `symbol_map` dictionary.
Examples
--------
>>> # Suppose relevant globals are defined (board, pacman_pos, ghosts, etc.)
>>> html_representation = render_board()
>>> display(html_representation) # in a Jupyter environment, to visualize
"""
# build an empty list called temp
...
# loop through each row in the board
...
# append a copy of the row to the temp list
...
# place Pac-Man if he's alive (lives > 0, game not over)
...
# get the pacman position into px, py
...
# set the temp[py][px] to 'P'
...
# place all ghosts
# Loop through each ghost position in the ghosts list, and set the ghost position `ghosts` to a tuple gx, gy in the loop
...
# set the temp[gy][gx] to 'G' if the position is valid (inside the board)
# Design a logical statement to check that the ghost position is inside the board, if so, set temp[gy][gx] to 'G'
...
# This is a list comprehension that maps the characters in the temp list to the emoji characters
# We have provided this code for you, you can use it as is
# Do not modify this code
lines = []
for row in temp:
row_str = []
for ch in row:
row_str.append(symbol_map.get(ch, ch))
lines.append("".join(row_str))
# This returns an HTML string that displays the board and is highly stylized for the gameplay
return (
'<pre style="font-family:monospace">' +
"\n".join(lines) +
"</pre>"
)
grader.check("render-board-function")
Question 7 (Points: 7.0): Controller Interface#
We need a set of control functions to move Pac around the board. We will provide details in the inline comments to guide you through the implementation
import ipywidgets as widgets # Import the ipywidgets library to create interactive widgets
# WIDGET SETUP
# Define Widget Setup for the game display as game_display, status_label, and buttons for movement
# Create an HTML widget to serve as the game display area, use the widgets.HTML() function, and save it in a variable called `game_display`
...
# Create a label widget to show the current game status (e.g., score, position, or messages), use the widgets.Label() function, and save it in a variable called `status_label`
...
# Create buttons for movement controls:
# Use the widgets.Button() function to create buttons for moving up, down, left, and right
# - A button for moving up (โ)
...
# - A button for moving left (โ)
...
# - A button for moving down (โ)
...
# - A button for moving right (โ)
...
grader.check("controller-interface-function")
Question 8 (Points: 5.0): Display Update Function#
The update_display
function is responsible for rendering the game board and updating the game status. It refreshes the game_display
widget using the render_board
function and updates the status_label
to show relevant game information such as remaining lives, score, and the number of ghosts. If the game is not over, the label provides instructions for movement using the arrow buttons. We will provide details in the inline comments to guide you through the implementation.
# Define a function to update the display as update_display
...
"""
Update the visible game interface by rendering the board and setting
status information.
This function uses two globally accessible UI elements:
1. `game_display`: A display object (such as an HTML output widget)
whose `value` is set to the rendered board returned by `render_board()`.
2. `status_label`: A label or text output widget which is updated to
show the current lives remaining for Pac-Man, the current score
versus total pellets, and the number of ghosts on the board.
It also references global variables such as:
- `pacman_lives` (int): The current number of lives remaining.
- `score` (int): The current score (pellets eaten).
- `total_pellets` (int): The total number of pellets on the board.
- `ghosts` (list): A list of ghost positions.
- `game_over` (bool): Whether the game is marked as over.
Parameters
----------
None
Returns
-------
None
This function does not return a value. Instead, it updates global UI
elements in place.
Examples
--------
>>> # After modifying the game state (movement, collisions, etc.), call:
>>> update_display()
>>> # This will refresh the rendered board and status info in the UI.
"""
# update the game_display value to the render_board function
# This will use a new concept called setters, which will update the value of a class attribute -- we will talk about this soon.
# for now, just know that the syntax is `game_display.value = render_board()`
...
# update the status_label value to the lives, score and number of ghosts followed by Use arrow buttons
if not game_over:
status_label.value = (
f"Lives: {pacman_lives}, Score: {score}/{total_pellets}, "
f"Ghosts: {len(ghosts)}. Use arrow buttons."
)
grader.check("render-ghosts-function")
Question 9 (Points: 1.0): Pac-Man Movement Function#
The move_pacman
function is responsible for handling Pac-Manโs movement and interactions with the game environment. It updates the game state based on user input, checks for collisions, and manages the spawning of ghosts. The function follows these key steps:
Check Game Status: If the game is over or Pac-Man has no remaining lives, the function exits early.
Move Pac-Man: It attempts to move Pac-Man in the specified direction (
dx
,dy
) and updates his position if the move is valid.Eat Pellets: If Pac-Man moves onto a pellet, the score is incremented, and the pellet is removed from the board. If all pellets are eaten, the game ends with a win.
Spawn Ghosts: With a certain probability, a new ghost is spawned and added to the list of ghosts, resetting the spawn probability if successful.
Move Ghosts: All ghosts attempt to move toward Pac-Man using a pathfinding algorithm or a fallback random move.
Check Collisions: If a ghost collides with Pac-Man, a life is lost. If all lives are lost, the game ends with a loss. Otherwise, Pac-Man respawns at the starting position.
Update Display: The game board and status messages are refreshed to reflect the latest game state.
We provide detailed inline instruction for the implementation.
# Define a function to move pacman as `move_pacman` that takes dx and dy as arguments
...
"""
Move Pac-Man in the given direction and handle game mechanics.
This function attempts to move Pac-Man based on the given directional changes (dx, dy),
updates the game state accordingly, and checks for ghost collisions.
Parameters
----------
board : list of lists
A 2D representation of the game board.
dx : int
The horizontal movement (-1 for left, 1 for right, 0 for no change).
dy : int
The vertical movement (-1 for up, 1 for down, 0 for no change).
Returns
-------
tuple
(game_over, score, pacman_lives), where:
- game_over (bool): Whether the game has ended.
- score (int): Pac-Manโs current score (pellets eaten).
- pacman_lives (int): Remaining number of lives.
Notes
-----
- Pac-Man can only move to valid positions (checked using `valid_move`).
- If Pac-Man eats a pellet, the score increases, and if all pellets are eaten,
the game ends with a win.
- A ghost spawns with increasing probability after each move.
- All ghosts move toward Pac-Man using BFS; if BFS fails, they move randomly.
- If Pac-Man collides with a ghost, he loses a life. If all lives are lost,
the game ends with a loss.
Examples
--------
>>> # Assume board and other required variables are defined
>>> move_pacman(board, 1, 0) # Move right
(False, 5, 2) # Example: game is ongoing, score is 5, and 2 lives remain
"""
# Define global variables to be modified in the function scope as `game_over`, `score`, `pacman_lives`, `ghost_spawn_prob`
...
# rdd= random.random()
# if the game is over or pacman has no lives, return
...
# 1) Attempt to move Pac-Man in the given direction
# set nx and ny to the new pacman position by adding dx and dy to the pacman position
...
# If the move is valid, update the pacman position
# You should use the valid_move function to check if the move is valid
...
# If there's a pellet, eat it, update the score and remove the pellet
# you add score by incrementing the variable `score` by 1
# you remove the pellet by setting the board position to a space " "
...
# If the score is equal to the total number of pellets, set game_over to True
...
# If the score is equal to the total number of pellets, update the status label to You Win! Final score: 10.
status_label.value = (
f"You ate all {total_pellets} pellets! You Win! Final score: {score}."
)
update_display()
return
# 2) Spawn a new ghost
# increment the ghost spawn probability by GHOST_SPAWN_INCREMENT, this makes it so there is an increasing chance of spawning a ghost
...
# If the random number is less than the ghost spawn probability, spawn a new ghost
...
# call the spawn_new_ghost function to spawn a new ghost and save it in a variable called newg
# append the new ghost position to the ghosts list
...
# Reset spawn probability if a ghost is spawned to zero
...
# 3) Move all ghosts towards Pac
px, py = pacman_pos
# loop through each ghost position in the ghosts list
...
# get the current ghost position into gx, gy
...
# BFS-chase or random fallback move for the ghost position to do this call the move_ghost_towards_pacman function
...
# update the ghost position to the new position
...
# 4) Check collisions, if there are collisions, update the pacman lives
# call the check_collisions function to check for collisions, and if so decrement the pacman lives by 1
...
# if pacman has no lives, set game_over to True
...
# Update status label as Game Over! The ghosts ate Pac-man. Final score: 10.
status_label.value = (
f"Game Over! The ghosts ate Pac-Man. Final score: {score}."
)
# Else reset the pacman position to the start position
...
# Update the display by calling the update display function
...
# DO NOT MODIFY THE RETURN STATEMENT
return game_over, score, pacman_lives
grader.check("move-pac-man")
# Define event handlers for button clicks
# Each function moves Pac-Man in the specified direction when the corresponding button is clicked
# Define on_up_clicked, on_left_clicked, on_down_clicked, on_right_clicked
# These functions should call move_pacman with the appropriate arguments
# we have provided the first example for you, define the rest of the functions
def on_up_clicked(a):
move_pacman(board, 0, -1) # Move Pac-Man up
...
# Attach the click event handlers to the respective buttons
# As button_up, button_left, button_down, button_right to on_up_clicked, on_left_clicked, on_down_clicked, on_right_clicked
button_up.on_click(on_up_clicked)
button_left.on_click(on_left_clicked)
button_down.on_click(on_down_clicked)
button_right.on_click(on_right_clicked)
# Arrange the movement buttons in a horizontal box layout as controls
controls = widgets.HBox([button_left, button_up, button_down, button_right])
# ---------------------------------------------
# INITIAL RENDER
# ---------------------------------------------
# Update the display to show the initial game state
...
# Display the game board, status label, and movement controls
...
grader.check("event-handlers")
Submitting Assignment#
Please run the following block of code using shift + enter
to submit your assignment, you should see your score.
from pykubegrader.submit.submit_assignment import submit_assignment
submit_assignment("week6-lab", "1_PacMan")