{
const scripts = Array.from(
document.querySelectorAll("script.fcpython-ojs-quiz-config")
);
const script = scripts.find(
(node) => node.dataset.fcpythonRendered !== "true"
);
if (!script) {
return html`<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>`;
}
script.dataset.fcpythonRendered = "true";
const quiz = JSON.parse(script.textContent);
const container = html`<div class="fcpython-quiz"></div>`;
const title = document.createElement("h3");
title.textContent = quiz.title;
container.appendChild(title);
const instructions = document.createElement("p");
instructions.textContent = quiz.instructions;
container.appendChild(instructions);
const progress = document.createElement("div");
progress.className = "fcpython-quiz-progress";
const counter = document.createElement("p");
counter.className = "fcpython-quiz-counter";
counter.setAttribute("aria-live", "polite");
progress.appendChild(counter);
const tabs = document.createElement("div");
tabs.className = "fcpython-quiz-steps";
tabs.setAttribute("role", "tablist");
tabs.setAttribute("aria-label", "Quiz questions");
progress.appendChild(tabs);
container.appendChild(progress);
const questions = document.createElement("div");
questions.className = "fcpython-quiz-questions";
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0;
function setStepStatus(questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz.questions[questionIndex];
const statuses = {
answered: "answered",
correct: "correct",
incorrect: "incorrect",
unanswered: "not answered",
};
step.classList.remove(
"is-answered",
"is-correct",
"is-incorrect",
"is-unanswered"
);
if (status) {
step.classList.add(`is-${status}`);
}
const statusLabel = status ? `, ${statuses[status]}` : "";
step.setAttribute(
"aria-label",
`Question ${questionIndex + 1}: ${question.prompt}${statusLabel}`
);
}
function showQuestion(questionIndex, focusPanel = false) {
currentQuestion = Math.max(
0,
Math.min(questionIndex, quiz.questions.length - 1)
);
questionPanels.forEach((panel, index) => {
panel.hidden = index !== currentQuestion;
});
stepButtons.forEach((step, index) => {
const isCurrent = index === currentQuestion;
step.classList.toggle("is-current", isCurrent);
step.setAttribute("aria-selected", String(isCurrent));
step.tabIndex = isCurrent ? 0 : -1;
});
counter.textContent = `Question ${currentQuestion + 1} of ${quiz.questions.length}`;
const selected = questionPanels[currentQuestion].querySelector(
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz.questions.length - 1;
previous.hidden = currentQuestion === 0;
next.hidden = isLastQuestion || !selected;
check.hidden = !isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion].focus();
}
}
quiz.questions.forEach((question, questionIndex) => {
const tabId = `${quiz.id}-question-tab-${questionIndex + 1}`;
const panelId = `${quiz.id}-question-panel-${questionIndex + 1}`;
const step = document.createElement("button");
step.type = "button";
step.className = "fcpython-quiz-step";
step.id = tabId;
step.textContent = String(questionIndex + 1);
step.setAttribute("role", "tab");
step.setAttribute("aria-controls", panelId);
step.setAttribute("aria-selected", "false");
step.tabIndex = -1;
step.addEventListener("click", () => showQuestion(questionIndex));
step.addEventListener("keydown", (event) => {
let destination = null;
if (event.key === "ArrowRight") {
destination = (questionIndex + 1) % quiz.questions.length;
} else if (event.key === "ArrowLeft") {
destination =
(questionIndex - 1 + quiz.questions.length) % quiz.questions.length;
} else if (event.key === "Home") {
destination = 0;
} else if (event.key === "End") {
destination = quiz.questions.length - 1;
}
if (destination !== null) {
event.preventDefault();
showQuestion(destination);
stepButtons[destination].focus();
}
});
stepButtons.push(step);
tabs.appendChild(step);
setStepStatus(questionIndex, "");
const panel = document.createElement("div");
panel.className = "fcpython-quiz-panel";
panel.id = panelId;
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", tabId);
panel.tabIndex = -1;
const fieldset = document.createElement("fieldset");
fieldset.className = "fcpython-quiz-question";
const legend = document.createElement("legend");
legend.textContent = question.prompt;
fieldset.appendChild(legend);
question.options.forEach((option, optionIndex) => {
const label = document.createElement("label");
label.className = "fcpython-quiz-option";
const input = document.createElement("input");
input.type = "radio";
input.name = `${quiz.id}-${question.id}`;
input.value = String(optionIndex);
input.addEventListener("change", () => {
setStepStatus(questionIndex, "answered");
feedbackNodes[questionIndex].textContent = "";
feedbackNodes[questionIndex].className = "fcpython-quiz-feedback";
score.textContent = "";
showQuestion(questionIndex);
});
const text = document.createElement("span");
text.textContent = option;
label.appendChild(input);
label.appendChild(text);
fieldset.appendChild(label);
});
const feedback = document.createElement("p");
feedback.className = "fcpython-quiz-feedback";
feedback.setAttribute("aria-live", "polite");
feedbackNodes.push(feedback);
fieldset.appendChild(feedback);
panel.appendChild(fieldset);
questionPanels.push(panel);
questions.appendChild(panel);
});
container.appendChild(questions);
const actions = document.createElement("div");
actions.className = "fcpython-quiz-actions";
const previous = document.createElement("button");
previous.type = "button";
previous.className = "fcpython-quiz-secondary";
previous.textContent = "Previous";
previous.addEventListener("click", () => {
showQuestion(currentQuestion - 1, true);
});
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next question";
next.addEventListener("click", () => {
showQuestion(currentQuestion + 1, true);
});
const check = document.createElement("button");
check.type = "button";
check.textContent = "Check answers";
const reset = document.createElement("button");
reset.type = "button";
reset.className = "fcpython-quiz-secondary fcpython-quiz-reset";
reset.textContent = "Reset";
const score = document.createElement("p");
score.className = "fcpython-quiz-score";
score.setAttribute("aria-live", "polite");
check.addEventListener("click", () => {
let correctCount = 0;
let firstQuestionToReview = null;
quiz.questions.forEach((question, questionIndex) => {
const selected = container.querySelector(
`input[name="${quiz.id}-${question.id}"]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (!selected) {
feedback.textContent = "Choose an answer before checking.";
feedback.className = "fcpython-quiz-feedback";
setStepStatus(questionIndex, "unanswered");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
return;
}
const selectedIndex = Number(selected.value);
if (selectedIndex === question.answer_index) {
correctCount += 1;
feedback.textContent = `✅ Correct. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-correct";
setStepStatus(questionIndex, "correct");
} else {
const answer = question.options[question.answer_index];
feedback.textContent = `❌ Not yet. Correct answer: ${answer}. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-incorrect";
setStepStatus(questionIndex, "incorrect");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
}
});
score.textContent = `Score: ${correctCount}/${quiz.questions.length}`;
if (firstQuestionToReview !== null) {
showQuestion(firstQuestionToReview, true);
}
});
reset.addEventListener("click", () => {
container.querySelectorAll("input[type='radio']").forEach((input) => {
input.checked = false;
});
feedbackNodes.forEach((feedback) => {
feedback.textContent = "";
feedback.className = "fcpython-quiz-feedback";
});
stepButtons.forEach((_, questionIndex) => {
setStepStatus(questionIndex, "");
});
score.textContent = "";
showQuestion(0, true);
});
actions.appendChild(previous);
actions.appendChild(next);
actions.appendChild(check);
actions.appendChild(reset);
container.appendChild(actions);
container.appendChild(score);
showQuestion(0);
return container;
}
FreeCampus Python
Unit Challenge: Escape the Clockwork Maze
Guide Pip through a clockwork maze by combining branch precedence, bounded while-loop progress, nested grid scanning, loop control, state collections, and a final comprehension.
python-foundations
decisions-repetition
unit-challenge
Course progress
0%
1. Challenge outcome
Pip, a palm-sized maintenance robot, wakes inside a clockwork maze. The exit is visible, but its brass door opens only after Pip collects the winding key. A scratched command tape includes useful moves, an unknown instruction, and one move directly into a wall.
Build the controller that scans the maze, follows the tape, spends energy, collects the key, records every event, and stops as soon as Pip escapes. The challenge is solved when the assertions pass unchanged and the final derived artifact says:
NoteMaze rule
Run one assertion group at a time. Repair the first failed promise before moving on. Do not edit expected values, delete awkward commands, hard-code coordinates, or assign the final artifact as one ready-made string.
2. Understand the acceptance example
The map uses these tiles:
| Tile | Meaning |
|---|---|
# |
wall; Pip cannot enter |
. |
open floor |
S |
starting coordinate |
K |
winding key |
E |
exit; escape succeeds only with the key |
! |
gear hazard; entering it costs one additional energy |
The command tape contains NORTH, SOUTH, WEST, and EAST. Any other command is unknown. Every command read—including unknown and blocked commands—uses one unit of energy. This guarantees visible progress even when Pip does not move.
Controller contracts
- Use nested
enumerate()loops to count terrain and discoverstart,key_position, andexit_position; do not type their coordinates into those result names. - The main simulation is a
whileloop controlled by remaining commands, positive energy, andnot escaped. - Advance
command_indexand reduce energy before anycontinuepath. - Translate known commands with one complete branch chain.
- Log an unknown command, then continue without calculating a candidate cell.
- Use short-circuit evaluation so a row is proven in bounds before its length is read.
- Log blocked candidates, then continue without changing position.
- Append every successfully entered coordinate to both an ordered
visitedlist and a membership-orientedvisited_set. - Stop immediately when Pip enters
Ewith the key. - Build path tiles and final artifact words from comprehensions after the loop.
Scope limits
- Keep the supplied public variable names.
- Do not use functions, recursion, exceptions, classes, files, randomness, or third-party packages.
- Do not mutate the maze or command tape.
- Use
breakonly for a successful escape; exhaustion and energy depletion are normal condition-based stops. - Keep blocked and unknown commands in
event_lograther than discarding them.
3. Start from the contract
Copy this starter into a fresh cell:
maze = [
"#########",
"#S..#..E#",
"#.#.#.#.#",
"#.#K!...#",
"#.......#",
"#########",
]
commands = [
"SCAN",
"SOUTH",
"EAST",
"SOUTH",
"SOUTH",
"EAST",
"EAST",
"NORTH",
"SPIN",
"EAST",
"EAST",
"NORTH",
"NORTH",
"EAST",
"EAST",
]
# Stage A: discovered from the nested grid scan.
start = None
key_position = None
exit_position = None
special_tiles = {"S": [], "K": [], "E": []}
terrain_counts = {}
# Stage B: simulation state.
position = None
command_index = 0
energy = 18
has_key = False
escaped = False
exit_reason = None
visited = []
visited_set = set()
event_log = []
# Stage C: derived after the simulation.
path_tiles = None
artifact_words = None
artifact = None4. Build in small stages
Stage A: scan the maze
Use one outer loop for rows and one inner loop for cells. Count every tile. When a cell belongs to special_tiles, append its coordinate. Assign the three public coordinate names from those discovered lists only after scanning. The supplied maze has one of each, but use None for a missing key so the boundary variation can finish normally.
Initialize simulation position and both visit collections with the discovered start. The list records route order; the set records whether a coordinate has already been entered.
Stage B: process one command per iteration
Use this loop skeleton. Replace comments with the branch and state operations from the contract:
while command_index < len(commands) and energy > 0 and not escaped:
command = commands[command_index]
command_index += 1
energy -= 1
# Translate a known command into row_delta and column_delta.
# For an unknown command, append an event and continue.
# Calculate next_row and next_column from position and the deltas.
# Prove the row is safe before reading maze[next_row]'s length.
# For an out-of-bounds or wall candidate, log it and continue.
# Move, record the coordinate, then handle K, !, and E tiles.Every event begins with the one-based command number, which is the updated command_index. Use these exact message shapes:
Ordinary successful movement uses "N: moved to (row, column)". If Pip reaches the exit without the key, use "N: exit locked at (row, column)" and keep processing. Entering ! consumes one additional energy and logs "N: hazard at (row, column)". After the loop choose exit_reason in this precedence: escaped, energy depleted, then commands exhausted.
Stage C: derive the route evidence and artifact
Use a list comprehension over visited to create path_tiles. Use another comprehension to uppercase these supplied words, then join them:
Do not type the final uppercase sentence as the value of artifact.
5. Run progressive assertions
Run each group only after the previous group passes.
Stage A checks: the scan found one of each special tile
Stage B checks: the loop progressed and stopped for success
Event checks: awkward commands remained visible
Route checks: order and membership tell compatible stories
Stage C checks: every final value was derived
That is twenty-four assertions. Do not proceed while an earlier group is failing.
6. Use the hint ladder only when needed
Hint 1: discover and initialize the state
Inside the nested scan, update terrain_counts with .get(tile, 0) + 1. Membership in special_tiles identifies the three special tile kinds. Append (row_index, column_index) to the matching list. Each should hold one coordinate, so the public position can select index zero. Initialize position, visited, and visited_set from start before the while loop.
Hint 2: make progress before every early exit
Read the current command, increment command_index, and reduce energy at the top of the loop. An if/elif chain maps four known strings to (row_delta, column_delta). Its else logs the unknown message and continues. For a candidate, use 0 <= next_row < len(maze) and 0 <= next_column < len(maze[next_row]) so the second length lookup is short-circuit protected.
Hint 3: preserve movement before handling special tiles
After a candidate passes bounds and wall checks, update position, append it to visited, and add it to visited_set. Inspect the entered tile. The key branch sets has_key; the hazard branch spends one more energy; the exit-with-key branch sets escaped, logs success, and breaks; the exit-without-key branch logs a locked door. Derive exit_reason only after the loop.
7. Keep debugging evidence
Retain one complete record from a real failure:
| Field | Your evidence |
|---|---|
| First failing assertion | Paste the exact assertion and observed values. |
| Current state | Record command number, command, position, energy, and latest event. |
| Hypothesis | Name one branch, update, or stopping rule that could explain it. |
| Controlled change | Describe the one change you made. |
| Clean rerun | Record the first assertion group that passed afterward. |
Useful temporary trace:
Remove or disable the trace after the checks pass. Keep the written evidence.
8. Test three changed mazes
Restart from the original starter before each variation.
- Missing key: replace
Kwith.. Predict whether Pip reaches the exit, whether it escapes, and whether the loop ends through exhaustion. Update only assertions whose promised behavior changes. - Low energy: start with energy 7. Predict the final command index, position, and exit reason before running.
- More invalid input: insert
"JUMP"immediately before the finalEAST. Prove it appears in the event log and consumes energy without changing the successful route.
For each variation, record the first condition that prevents another iteration or the exact break that stops it.
After predicting, compare your results with this boundary table and write one assertion for each listed field:
| Variation | Command index | Final position | Energy | Key | Escaped | Exit reason |
|---|---|---|---|---|---|---|
key replaced by . |
15 | (1, 7) |
2 | false | false | commands exhausted |
| starting energy 7 | 7 | (4, 3) |
0 | false | false | energy depleted |
JUMP before final EAST |
16 | (1, 7) |
1 | true | true | escaped |
9. Compare with a solution path
Reveal after your checks pass or all three hints have been used
maze = [
"#########",
"#S..#..E#",
"#.#.#.#.#",
"#.#K!...#",
"#.......#",
"#########",
]
commands = [
"SCAN", "SOUTH", "EAST", "SOUTH", "SOUTH",
"EAST", "EAST", "NORTH", "SPIN", "EAST",
"EAST", "NORTH", "NORTH", "EAST", "EAST",
]
special_tiles = {"S": [], "K": [], "E": []}
terrain_counts = {}
for row_index, row in enumerate(maze):
for column_index, tile in enumerate(row):
terrain_counts[tile] = terrain_counts.get(tile, 0) + 1
if tile in special_tiles:
special_tiles[tile].append((row_index, column_index))
start = special_tiles["S"][0]
key_position = special_tiles["K"][0] if special_tiles["K"] else None
exit_position = special_tiles["E"][0]
position = start
command_index = 0
energy = 18
has_key = False
escaped = False
exit_reason = None
visited = [start]
visited_set = {start}
event_log = []
while command_index < len(commands) and energy > 0 and not escaped:
command = commands[command_index]
command_index += 1
energy -= 1
if command == "NORTH":
row_delta, column_delta = -1, 0
elif command == "SOUTH":
row_delta, column_delta = 1, 0
elif command == "WEST":
row_delta, column_delta = 0, -1
elif command == "EAST":
row_delta, column_delta = 0, 1
else:
event_log.append(f"{command_index}: unknown {command}")
continue
next_row = position[0] + row_delta
next_column = position[1] + column_delta
in_bounds = (
0 <= next_row < len(maze)
and 0 <= next_column < len(maze[next_row])
)
if not in_bounds or maze[next_row][next_column] == "#":
event_log.append(
f"{command_index}: blocked at {(next_row, next_column)}"
)
continue
position = (next_row, next_column)
visited.append(position)
visited_set.add(position)
tile = maze[next_row][next_column]
if tile == "K" and not has_key:
has_key = True
event_log.append(f"{command_index}: key collected at {position}")
elif tile == "!":
energy -= 1
event_log.append(f"{command_index}: hazard at {position}")
elif tile == "E" and has_key:
escaped = True
event_log.append(f"{command_index}: exit reached at {position}")
break
elif tile == "E":
event_log.append(f"{command_index}: exit locked at {position}")
else:
event_log.append(f"{command_index}: moved to {position}")
if escaped:
exit_reason = "escaped"
elif energy <= 0:
exit_reason = "energy depleted"
else:
exit_reason = "commands exhausted"
path_tiles = [maze[row][column] for row, column in visited]
message_parts = ["Pip", "escaped", "the", "clockwork", "maze"]
artifact_words = [word.upper() for word in message_parts]
artifact = " ".join(artifact_words)
print(artifact)The command index and energy change before either continue, so unknown and blocked commands cannot trap the controller. The bounds expression protects the row lookup, and the ordered list plus set preserve two different route questions.
10. Check your understanding
Answer these questions before recording the challenge. The quiz runs directly in your browser.
11. Decide whether the challenge is complete
Evidence rubric
| Evidence | Ready to record when |
|---|---|
| Behavior | All twenty-four supplied assertions pass unchanged. |
| Control flow | You can identify the selected branch, every progress update, each continue, and the successful break. |
| State | The ordered route, unique visited coordinates, event log, energy, and exit reason agree. |
| Boundaries | Missing-key, low-energy, and additional-invalid-command reruns match written predictions. |
| Debugging | One failure record connects observed state, one hypothesis, one change, and a clean rerun. |
| Reproducibility | The original and each variation work from separately initialized clean state. |
Record completion only when every statement is true:
This button stores a self-reported marker only in this browser. It does not submit the artifact, grade it, verify identity, or issue a certificate.
Not yet recorded.
Key points
TipKey points
- Scan structured data instead of hard-coding facts the data already contains.
- Make input, budget, or state progress before a path can continue.
- Order guards so short-circuiting protects operations that are not always safe.
- Keep ordered evidence, membership evidence, and event explanations in the collection shapes that preserve their meaning.
- Stop as soon as the promised success cannot be improved, and preserve an exit reason for every other outcome.