main
No variables
Part 3’s game was about dice and scoring. This part uses a different setting and a different data problem: Campus Courier.
You move a courier around a small campus map. Each package names a destination, such as the Library or Design Lab. Pressing Space on a destination records the attempt. The screen then shows the recent deliveries and a grouped summary.
Your data functions connect the map, packages, delivery history, and report. The supplied movement and drawing code makes their results visible.
The campus map is a nested list. Each inner list is one row:
campus_map = [ ["#", "#", "#", "#", "#"], ["#", "P", ".", "L", "#"], ["#", ".", ".", "D", "#"], ["#", "#", "#", "#", "#"]]"#" means a wall, "." an open tile, and "P" the player’s starting tile. Destination symbols connect the grid to a dictionary containing display information:
destinations = { "L": {"name": "Library", "color": 10}, "D": {"name": "Design Lab", "color": 12}}Packages form a list of records. Their identifiers are unique even when names repeat:
packages = [ { "id": "P17", "name": "Camera case", "destination": "D", "tags": ["fragile", "keep dry"] }, { "id": "P20", "name": "Camera case", "destination": "D", "tags": ["tripod inside"] }]A dictionary keyed by ID makes the next package direct to find. Its values should be the existing package records, because the list and lookup are two views of the packages that exist now:
packages_by_id = { "P17": packages[0], "P20": packages[1]}This is intentional sharing. It differs from a delivery history, which describes what happened at a particular time.
Run this smaller example. It first creates a lookup that shares a package, then a delivery record that copies the known nested shape.
The lookup shows the new name because it refers to the existing package. The delivery retains the old name and tags because it owns a package dictionary and a tags list made for that event.
This is a shape-specific copy. You know that the package dictionary contains strings and one mutable list, so you can copy exactly the levels that must become independent.
Follow the same example one step at a time. Compare the dictionary reached through lookup with the one reached through delivery, then follow their tags lists when the original package changes.
Code
Step 1 of 9
State
No variables
Objects
No objects yet
Output
No output yet
Each attempt is appended to a history list. That preserves attempts at the same destination and keeps their order:
history = [ { "package": { "id": "P17", "name": "Camera case", "destination": "D", "tags": ["fragile", "keep dry"] }, "delivered_to": "D", "correct": True }, { "package": { "id": "P20", "name": "Camera case", "destination": "D", "tags": ["tripod inside"] }, "delivered_to": "L", "correct": False }]The sidebar needs a different view. It groups the attempts by their requested destination:
{ "D": {"attempts": 2, "correct": 1}}The grouped dictionary is good for counting. A report then turns its items into records, sorts those records by destination name with a named key function, and formats lines such as Design Lab: 1/2 correct.
Notice the pipeline:
ordered delivery history -> grouped counts -> destination-name order -> display linesNo one structure has to do every job.
The starter contains three supplied classes. You are not asked to define or edit them in this part:
CampusMapView holds the map and destination information and draws the map.CourierSession keeps the courier’s position, current package, and delivery history, and calls your data functions to update them.CourierGame receives key presses and connects the session and map to the screen.Start with record_delivery(self.history, package, delivered_to) inside CourierSession.deliver. The arguments are the session’s history list, the current package record, and the destination symbol. Follow how your function changes that history. Treat the other methods in the same way: locate a function call, identify its arguments, and follow its returned result or documented change.
When tracing an event through the framework, separate the method that receives the event, the object whose state changes, and the later drawing code that reads that state. You can understand that route without learning the class syntax yet.
This is a preview of Part 6, where you will examine classes, methods, self, and object ownership directly. Here, concentrate on the Part 4 data contracts.
As in Part 3, collect related assertions in a function and call it to run the checks. The starter includes test_data_functions() in main.py, with a few checks for the map-query functions:
def test_data_functions(): grid = [["#", "L"], [], ["P", "."]] assert find_tile(grid, "P") == {"row": 2, "column": 0} assert find_tile(grid, "missing") == None assert can_enter(grid, 2, 1) == True assert can_enter(grid, 0, 0) == False assert can_enter(grid, 1, 0) == False assert can_enter(grid, -1, 0) == False assert grid == [["#", "L"], [], ["P", "."]]
# Add checks here as you complete the later steps. print("The current data checks passed.")Select main.py and Run in the course editor. The supplied launch block calls test_data_functions(). Initially, an assertion fails because the map-query functions are unfinished. A failed assertion stops the run at that check; use the reported line to compare the result with the expected value. When all current checks pass, the message appears in the output.
Add assertions inside the test function as you complete each step. Create small records and lists there so that the tests can change their own data without changing the game’s supplied packages. Check empty inputs, repeated package names, wrong destinations, and what happens when you change an original or returned record. A passing run checks only the cases you included.
At the bottom of main.py, the two game-launch lines start as comments:
if __name__ == "__main__": test_data_functions() # Uncomment these two lines when you are ready to play. # import pyxel # CourierGame()Keep those two lines commented while checking functions. To play, remove their # markers, keep their indentation, and Run again. The course editor opens the Pyxel panel, and the assertions run before the game starts. To return to the ordinary assertion output, comment both lines again. Keep the test function in main.py and its call inside the supplied launch block when submitting; the grader can import your functions without running your checks or starting the game.
The exercise card contains the staged contracts, checking workflow, and playing instructions. Establish the data functions’ behavior with deterministic checks before using the graphical result as additional feedback.
0 / 70 points
Complete the data functions in the supplied Campus Courier starter. The Pyxel framework is already divided into CampusMapView, CourierSession, and CourierGame. Leave those supplied classes and the game data in place. Your data functions must not import Pyxel, print, read input, or modify an input unless the contract explicitly says to append to history. Use the separate test_data_functions() function in main.py for your assertions.
The grader awards points by completed step. Each step has one marked test and earns its stated points only when that complete step passes. A later unfinished step does not prevent an earlier completed step from earning its points. All five steps together are worth 70 points.
All supplied grids and records satisfy the shapes described below. Rows may have different lengths in tests.
Implement find_tile(grid, symbol). Search rows from top to bottom and each row from left to right. Return {"row": row_index, "column": column_index} for the first matching symbol, or None when it is absent. Empty and ragged grids are permitted. Do not change the grid.
Implement can_enter(grid, row, column). Return True exactly when the indexes select an existing cell whose value is not "#". Return False for a wall, a negative index, a row outside the grid, or a column outside the selected row. Do not change the grid.
Implement packages_by_id(packages). Return a new dictionary mapping each package’s unique "id" to that existing package record. Package names may repeat. The records are deliberately shared: changing a record through the lookup would also be visible through the input list. Changing the returned outer dictionary, such as adding a key, must not change the input list. Empty input returns {}. Do not print or change the packages.
Implement make_delivery(package, delivered_to). Return exactly this shape:
{ "package": { "id": package["id"], "name": package["name"], "destination": package["destination"], "tags": an independent copy of package["tags"] }, "delivered_to": delivered_to, "correct": delivered_to == package["destination"]}The nested package dictionary and its tags list must be independent of the input package. Do not change the package.
Implement record_delivery(history, package, delivered_to). Use make_delivery to append one new record to history, then return None. Preserve the package and all earlier history records.
Implement delivery_summary(history). Group records by the requested destination in delivery["package"]["destination"]. Return a dictionary whose value for each occurring destination is an independent {"attempts": ..., "correct": ...} record. Count every delivery once; increment "correct" only when its Boolean field is True. Preserve first-occurrence key order. Empty history returns {}. Do not change the history.
Implement summary_lines(history, destinations). Use delivery_summary(history) and return one line per occurring destination, ordered by the destination’s "name" using Python’s default string order. Each line must have exactly this format:
Design Lab: 1/2 correctEmpty history returns []. Do not change either input. Keep the supplied summary_name, the three framework classes, and the guarded game launch in place. The final step checks that the ordered report drives the sidebar and that map movement, lookup, snapshot recording, and drawing work together through the supplied classes.
Work entirely in main.py in the course editor. The starter’s test_data_functions() contains assertions for Step 1. Select main.py and Run. An assertion initially fails because the map-query functions are unfinished. Use the reported line to compare the returned value with the expected result, repair the function, and Run again.
Add assertions inside test_data_functions() as you complete each step, before its final message. Create small test records and lists inside that function. Include empty inputs, repeated names, ragged grids, wrong destinations, and mutation checks that distinguish shared records from independent snapshots. The message means that the current assertions passed; add cases when a contract introduces another requirement.
Keep import pyxel and CourierGame() commented out in the supplied launch block while testing. The block already calls test_data_functions(), so the course editor runs the assertions and displays their output. Keep the test function in main.py and its call inside the launch block when submitting. Importing your submission for grading runs neither the assertions nor the game.
After your checks pass, remove the comment markers from import pyxel and CourierGame() at the bottom of main.py, keeping their indentation. Run main.py again. The course editor opens the Pyxel panel; your assertions run before the game starts. To return to the ordinary assertion output, comment both game-launch lines again.
Use the arrow keys to move one tile, Space to deliver the current package at a destination tile, and R to restart. The sidebar shows the current package, recent delivery records, and grouped summary lines. Deliver every package to its requested destination for a perfect route.
Choose a test that checks whether a delivery snapshot remains independent, and explain why you can check the data functions separately from the display.
Explain how the three supplied objects divide the work, then trace a delivery from the Space press to the updated history and screen.
0 / 10 points
Review the completed Campus Courier program. Explain the separate responsibilities of CampusMapView, CourierSession, and CourierGame, and trace what happens from a Space press on a destination tile until a new delivery appears in the history and on the screen.
Then compare two copying decisions. Why does packages_by_id deliberately keep references to the existing package records, while make_delivery builds a new package dictionary and copies its tags list? Give one concrete test, including its expected result, that checks a data function without opening Pyxel.
test_data_functions() to check delivery_summary with an empty history and a wrong-destination delivery?