FreeCampus Python

Unit Challenge: Repair the Glitched Arcade

Repair Pixel’s command parser, movement rules, and exception boundary so valid moves reach the portal while rejected commands keep useful evidence.
python-foundations errors-exceptions-debugging unit-challenge debugging-puzzle
Open in Colab
  • Level: Python Foundations · Unit 8 challenge
  • Estimated time: 60–120 minutes
  • Outcome assessed: Classify failure evidence, repair exception design and movement logic, and verify one cause at a time without hiding unexpected defects.
  • Evidence: Unchanged progressive assertions, a portal-opening round, three contextual rejection messages, one debugging record, and a clean rerun

1. Get Pixel to the portal without hiding the glitches

An old arcade cabinet controls Pixel, a tiny character stranded behind a glitchy joystick. Every accepted command moves Pixel on a coordinate grid. The portal opens only when the final position is exactly (2, 1).

The cabinet has this command tape:

commands = [
    "UP 2",
    "RIGHT 3",
    "LEFT many",
    "JUMP 1",
    "DOWN 1",
    "LEFT 1",
    "RIGHT 0",
]

portal = (2, 1)

Some commands are valid moves. Others should be rejected with the turn number and a useful reason. The supplied implementation runs, but the portal stays dark. Your job is to use the staged evidence to repair the command boundary and the silent movement bug.

At the end, the game should report:

Pixel stopped at (2, 1).
Rejected commands: 3
Portal opened!
NoteYou have a repair manual, not an answer key

Work on one subsystem at a time. Run the closest checks, state a hypothesis, make one change, and rerun earlier checks. Open hints only after you can name the exact assertion, exception, or value blocking you.

2. Read the command and movement rules

Commands

  • A command is a string containing exactly two whitespace-separated parts.
  • The first part is a direction. Input is case-insensitive and normalizes to UP, RIGHT, DOWN, or LEFT.
  • The second part must convert to a positive integer. Zero is not positive.
  • Every rejected command raises a contextual ValueError containing its 1-based turn number.
  • When unpacking or integer conversion raises the original ValueError, the contextual error preserves it as __cause__ with raise ... from error.
  • Unsupported direction and non-positive steps are explicit rule failures, so they have no invented lower-level cause.

Movement

Pixel begins at (0, 0). Coordinates use (x, y):

Direction Coordinate change Example from (3, 2)
UP add steps to y UP 2(3, 4)
RIGHT add steps to x RIGHT 2(5, 2)
DOWN subtract steps from y DOWN 2(3, 0)
LEFT subtract steps from x LEFT 2(1, 2)

move_pixel receives a validated command and returns a new tuple. It does not mutate the supplied position.

Round recovery

play_round parses each command in encounter order. It catches only an anticipated parser ValueError, records the message, and continues to the next turn. Movement happens outside that narrow try block. If movement code has an unexpected defect, that defect must remain visible instead of being called bad player input.

For the supplied tape:

  • turns 3, 4, and 7 are rejected;
  • the four accepted moves are UP 2, RIGHT 3, DOWN 1, and LEFT 1;
  • accepted moves lead (0, 0) → (0, 2) → (3, 2) → (3, 1) → (2, 1); and
  • the input list and strings remain unchanged.

3. Start from the contract

Run this starter unchanged. It is valid Python and intentionally looks plausible. Let the assertions reveal which promises it violates before editing. Preserve the three public names, signatures, and docstrings.

VALID_DIRECTIONS = {"UP", "RIGHT", "DOWN", "LEFT"}


def parse_command(text, turn):
    """Return a normalized (direction, steps) tuple or raise contextual ValueError."""
    try:
        direction, raw_steps = text.split()
    except ValueError:
        raise ValueError(f"turn {turn}: command needs exactly two parts")

    direction = direction.upper()
    if direction not in VALID_DIRECTIONS:
        raise ValueError(f"turn {turn}: unsupported direction {direction!r}")

    try:
        steps = int(raw_steps)
    except ValueError:
        raise ValueError(f"turn {turn}: steps must be an integer")

    if steps < 0:
        raise ValueError(f"turn {turn}: steps must be positive")
    return direction, steps


def move_pixel(position, command):
    """Return Pixel's new position after one validated command."""
    x, y = position
    direction, steps = command

    if direction == "UP":
        y += steps
    elif direction == "RIGHT":
        x += steps
    elif direction == "DOWN":
        y -= steps
    elif direction == "LEFT":
        x += steps
    return x, y


def play_round(commands, portal):
    """Return final position, rejected messages, and whether the portal opened."""
    position = (0, 0)
    rejected = []

    for turn, text in enumerate(commands, start=1):
        try:
            command = parse_command(text, turn)
            position = move_pixel(position, command)
        except Exception as error:
            rejected.append(str(error))

    return position, rejected, position == portal

Do not list possible repairs yet. First run the parser checks in Section 5 and keep the first failure. A failed check is a map to one contract boundary.

4. Isolate one arcade subsystem at a time

Treat the arcade as three cooperating systems:

Subsystem Input Normal result Anticipated failure Focused evidence
parser text and turn normalized tuple contextual ValueError return value, message, __cause__
movement position and validated command new position none for valid commands before/after coordinates
round runner command sequence and portal final tuple, messages, opened flag parser rejection and continuation accepted path, rejection count, final state

Follow this order:

  1. Normalization: make valid lower/mixed-case commands return uppercase directions and integer steps.
  2. Command shape and conversion: inspect the new error and its original cause separately.
  3. Domain validation: reject unsupported directions, zero, and negatives.
  4. Coordinates: verify one direction at a time from the same starting point.
  5. Minimal regression: reduce the wrong round to one LEFT move.
  6. Recovery boundary: parse inside the protected region, then move outside it.
  7. Integration: run the supplied tape and confirm final position, rejection evidence, input ownership, and portal state.

For one defect, use this investigation loop:

Reproduce -> copy exact evidence -> state one cause -> change one line or boundary
          -> rerun the focused check -> rerun all earlier checks
WarningDo not edit the expected values to create green output

If an assertion surprises you, compare it with the written command or movement rule. Repair an incorrect oracle only when you can demonstrate that the written contract contradicts it.

5. Run progressive assertions

Run each stage after the previous stage passes. The helper below captures the specific exception and fails clearly when no ValueError is raised:

def captured_value_error(action):
    """Run action and return its ValueError, or fail if none is raised."""
    try:
        action()
    except ValueError as error:
        return error
    raise AssertionError("expected ValueError")

Stage A: normalize valid commands

assert parse_command("UP 2", 1) == ("UP", 2)
assert parse_command("left 3", 2) == ("LEFT", 3)
assert parse_command("  Right   4  ", 3) == ("RIGHT", 4)
assert parse_command("dOwN 1", 4) == ("DOWN", 1)

Stage B: preserve a malformed-shape cause

Unpacking one or three parts raises the lower-level ValueError. Add turn context without losing that cause:

shape_error = captured_value_error(lambda: parse_command("UP", 5))
assert "turn 5" in str(shape_error)
assert "two" in str(shape_error) or "parts" in str(shape_error)
assert isinstance(shape_error.__cause__, ValueError)

extra_part_error = captured_value_error(lambda: parse_command("UP 2 NOW", 6))
assert "turn 6" in str(extra_part_error)
assert isinstance(extra_part_error.__cause__, ValueError)

Stage C: preserve an integer-conversion cause

conversion_error = captured_value_error(lambda: parse_command("LEFT many", 3))
assert "turn 3" in str(conversion_error)
assert "integer" in str(conversion_error)
assert isinstance(conversion_error.__cause__, ValueError)
assert "many" in str(conversion_error.__cause__)

Stage D: reject unsupported and non-positive moves

These are direct contract decisions, not translated lower-level failures:

direction_error = captured_value_error(lambda: parse_command("JUMP 1", 4))
assert "turn 4" in str(direction_error)
assert "JUMP" in str(direction_error)
assert direction_error.__cause__ is None

zero_error = captured_value_error(lambda: parse_command("RIGHT 0", 7))
assert "positive" in str(zero_error)
assert zero_error.__cause__ is None

negative_error = captured_value_error(lambda: parse_command("DOWN -2", 8))
assert "turn 8" in str(negative_error)
assert "positive" in str(negative_error)

Stage E: move in all four directions

origin = (3, 2)
assert move_pixel(origin, ("UP", 2)) == (3, 4)
assert move_pixel(origin, ("RIGHT", 2)) == (5, 2)
assert move_pixel(origin, ("DOWN", 2)) == (3, 0)
assert move_pixel(origin, ("LEFT", 2)) == (1, 2)
assert origin == (3, 2)

Keep this smallest regression check even after the full round passes:

assert move_pixel((3, 1), ("LEFT", 1)) == (2, 1)

Stage F: reject bad commands and keep playing

commands_before = commands.copy()
position, rejected, opened = play_round(commands, portal)

assert position == (2, 1)
assert len(rejected) == 3
assert "turn 3" in rejected[0]
assert "turn 4" in rejected[1]
assert "turn 7" in rejected[2]
assert opened is True
assert commands == commands_before
assert portal == (2, 1)

Confirm the round boundary does not hide a movement defect. Temporarily replace move_pixel with a version that raises RuntimeError, call play_round, and observe that the failure propagates. Restore the real function before the full rerun. Do not add a RuntimeError handler merely to make this experiment quiet.

Finally, create a short command list of your own. Predict every accepted move, every rejection, and the final position on paper, then add assertions for its result without changing any supplied expected values.

6. Use the hint ladder only when needed

Hint 1

Let the failing assertion identify the subsystem. __cause__ belongs to parser translation, a wrong coordinate belongs to movement, and a swallowed RuntimeError belongs to the round’s try boundary. Do not repair all three at once.

Hint 2

When Python itself raises during unpacking or int, retain that exception after adding turn context. “Positive” excludes zero. LEFT and RIGHT must use opposite x-coordinate operations. The round can catch parser ValueError without placing movement in the same protected block.

Hint 3

Fill only the missing decisions in these focused shapes:

try:
    direction, raw_steps = text.split()
except ValueError as error:
    raise ValueError(...) from error

if steps <= 0:  # Replace the comparison if your evidence supports another one.
    raise ValueError(...)

elif direction == "LEFT":
    x = ...  # Replace the value with the intended coordinate update.

try:
    command = parse_command(text, turn)
except ValueError as error:
    rejected.append(...)
else:
    position = move_pixel(position, command)

7. Keep debugging evidence

Preserve one failure that changed your understanding. The one-move LEFT check is a good candidate because it removes parsing, the loop, and the portal while retaining the coordinate defect.

Reproduction Actual evidence Hypothesis One change Focused rerun Full rerun Regression check
exact call/check value, message, or cause one mechanism that could be false one line or boundary nearest check result all earlier stages smallest permanent check

Example evidence should be specific:

Reproduction: move_pixel((3, 1), ("LEFT", 1))
Expected: (2, 1)
Actual: (4, 1)
Hypothesis: LEFT uses addition, the same x update as RIGHT.
Prediction: changing only LEFT to subtraction returns (2, 1); other directions stay unchanged.

After every repair, restart the notebook runtime and run from the starter or your final definitions through all progressive assertions. A portal that opens only because an old function remains in memory is not reliable evidence.

8. Compare with a complete solution

Show the complete arcade repair after attempting every stage
VALID_DIRECTIONS = {"UP", "RIGHT", "DOWN", "LEFT"}


def parse_command(text, turn):
    """Return a normalized (direction, steps) tuple or raise contextual ValueError."""
    try:
        direction, raw_steps = text.split()
    except ValueError as error:
        raise ValueError(
            f"turn {turn}: command needs exactly two parts"
        ) from error

    direction = direction.upper()
    if direction not in VALID_DIRECTIONS:
        raise ValueError(
            f"turn {turn}: unsupported direction {direction!r}"
        )

    try:
        steps = int(raw_steps)
    except ValueError as error:
        raise ValueError(
            f"turn {turn}: steps must be an integer; got {raw_steps!r}"
        ) from error

    if steps <= 0:
        raise ValueError(
            f"turn {turn}: steps must be positive; got {steps}"
        )
    return direction, steps


def move_pixel(position, command):
    """Return Pixel's new position after one validated command."""
    x, y = position
    direction, steps = command

    if direction == "UP":
        y += steps
    elif direction == "RIGHT":
        x += steps
    elif direction == "DOWN":
        y -= steps
    elif direction == "LEFT":
        x -= steps
    else:
        raise ValueError(f"unvalidated direction reached movement: {direction!r}")
    return x, y


def play_round(commands, portal):
    """Return final position, rejected messages, and whether the portal opened."""
    position = (0, 0)
    rejected = []

    for turn, text in enumerate(commands, start=1):
        try:
            command = parse_command(text, turn)
        except ValueError as error:
            rejected.append(str(error))
        else:
            position = move_pixel(position, command)

    return position, rejected, position == portal

Why the repaired boundaries matter:

  • unpacking and integer conversion can fail at a lower level, so chaining keeps both Python’s cause and the command’s turn context;
  • unsupported direction and non-positive steps are direct parser rules, so no unrelated cause is manufactured;
  • LEFT subtracts from x and the tuple returned is new;
  • only parsing sits inside the round’s try, so an unexpected movement failure still produces honest developer evidence; and
  • the portal result derives from the final position rather than being hard-coded.

Run every assertion from Section 5 unchanged after loading this solution.

9. Predict a changed arcade rule

The cabinet designer proposes two changes:

  1. STAY 1 becomes a supported command that leaves Pixel in place; and
  2. the portal opens if Pixel finishes at Manhattan distance at most one from its center, not only on the exact coordinate.

Before editing:

  • Which parser constant, movement branch, docstrings, and assertions change?
  • Should STAY 0 become valid, or does the positive-step rule still reject it?
  • For portal (2, 1), which neighboring coordinates now open it?
  • Which behavior belongs in a small helper instead of making play_round harder to read?
  • Which old assertions should remain unchanged as regression evidence?

Implement the changed rule only after writing examples for exact, adjacent, and two-steps-away positions. Do not weaken command validation accidentally while adding a new direction.

10. Check your understanding

11. Decide whether the challenge is complete

Evidence rubric

Evidence Ready to record when
Parser Normalization, shape, conversion, direction, and positive-step checks pass unchanged.
Movement Four directions and the minimal LEFT regression behave as specified without input mutation.
Boundary Three anticipated command failures are recorded, play continues, and an unexpected movement defect would propagate.
Payoff Pixel finishes at (2, 1) and the computed portal flag is true.
Reasoning One debugging record connects exact evidence, one hypothesis, one controlled change, and focused/full reruns.
Reproducibility The definitions and all assertions pass after a clean restart and top-to-bottom run.

This button stores a self-reported marker only in this browser. It does not submit work, grade it, verify identity, or issue a certificate.

Not yet recorded.

Key points

  • Progressive assertions turn a multi-defect program into parser, movement, and boundary investigations.
  • Exception chaining preserves a lower-level cause while adding turn context.
  • Narrow handling continues after anticipated command mistakes without hiding programmer defects.
  • A minimal logic reproduction exposes direction state without needing the whole arcade round.
  • The strongest finish is a clean rerun you can explain, not merely a glowing portal from stale notebook state.
Back to top