{
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: Break the Mirror Vault
Repair a magical cloning spell with independent nested state, hashable room markers, deliberate in-place and returned-copy APIs, and safe snapshots.
python-foundations
mutability-identity-copying
unit-challenge
Course progress
0%
1. Break a cloning spell without hard-coding the escape
The Mirror Vault creates adventurer doppelgängers from one blueprint. Its broken spell copies only names: explorers share inventories, journals, routes, and visited-room state. When Nova collects a rune, Sol’s journal changes too.
Build a small ownership-aware state API that produces independently editable explorers while safely sharing immutable vault rules. Nova must collect three runes and derive this final artifact:
Your implementation must make these behaviors visible:
- spawning creates new outer and nested mutable owners;
- tuple room coordinates work as stable set members;
- one command mutates a named explorer intentionally;
- one transformer returns independent state and preserves its source;
- a snapshot does not expose internal lists;
- omitted history gets fresh per-call storage; and
- a shallow-copy variation reproduces the curse and is then repaired.
NoteKeep the complete solution closed on your first attempt
Work for one or two focused hours. Run one assertion group at a time. Before opening a hint, identify the exact access path whose identity or value differs from the contract.
2. Read the blueprint and ownership rules
The blueprint contains:
- immutable role and difficulty strings;
- an immutable tuple of vault rules that may be shared safely;
- a mutable inventory list;
- a mutable journal dictionary containing rune and route lists; and
- a mutable set of visited tuple coordinates.
Constraints
- Preserve all supplied public names, signatures, and docstrings.
- Do not mutate
blueprintwhile spawning or operating on an explorer. - Give each spawned explorer independent ownership of every mutable descendant.
- Preserve the identity of the immutable
rulestuple deliberately. - Use tuple coordinates such as
(1, 0)as room markers; do not convert printed hashes into stored identifiers. record_rune_in_placemust mutate its supplied explorer and returnNone.with_supplymust return independently editable state and preserve its source.journal_snapshotmust not return either internal list directly.remember_attemptmust not share an omitted mutable default between calls.- Derive the final message from explorer state; do not assign the solved sentence directly in
vault_message. - Keep calculation functions free of printing.
- Use only the standard library. Classes, files, serialization, and exception handlers are outside this challenge.
3. Start from the contract
Run the blueprint unchanged:
Then copy this scaffold. Implement functions in the order shown:
def spawn_explorer(blueprint, *, name):
"""Return an explorer with independent mutable state and shared rules."""
raise NotImplementedError
def identity_report(left, right):
"""Return labeled identity evidence for important explorer paths."""
raise NotImplementedError
def record_rune_in_place(explorer, room, rune):
"""Record room and rune in caller-owned explorer; return None."""
raise NotImplementedError
def with_supply(explorer, supply):
"""Return independent explorer state containing one additional supply."""
raise NotImplementedError
def journal_snapshot(explorer):
"""Return an immutable snapshot of rune and route sequences."""
raise NotImplementedError
def remember_attempt(attempt, history=None):
"""Append attempt to supplied history or a fresh per-call list; return it."""
raise NotImplementedError
def vault_message(explorer):
"""Return the explorer's derived Mirror Vault escape message."""
raise NotImplementedErroridentity_report(left, right) returns exactly these keys:
The values above describe two correctly spawned explorers from the same blueprint.
4. Build the vault state in observable stages
- Write an ownership table for every blueprint field. Mark immutable values as safe to share and mutable descendants as independently owned.
- Implement
spawn_explorerwith selective copying. Check the outer dictionary, then every nested mutable path before performing a mutation. - Implement
identity_reportwithiscomparisons only. It reports relationships; it does not decide whether current values are correct. - Implement the in-place rune command. Add the tuple room to
visited, append it to the journal route, append the rune, and rely on the implicitNoneresult. - Implement
with_supplyby creating independent state first and then changing only the new inventory. - Convert journal lists to tuples for the returned snapshot.
- Use
Noneto create fresh history on omitted-argument calls while documenting that an explicitly supplied history is updated. - Count the explorer’s runes inside
vault_message; do not store the final count or sentence separately. - Run the shallow-copy variation last so its deliberate leak cannot contaminate the main blueprint.
Draw the path nova -> journal -> runes and the matching Sol and blueprint paths. The three final list objects must be distinct even when their initial contents are equal.
5. Run progressive assertions
Run these 24 numbered assertions unchanged. A failed identity assertion points to an ownership boundary; a failed value assertion points to an operation or result contract.
Spawn and identity evidence
from copy import deepcopy
before = deepcopy(blueprint)
nova = spawn_explorer(blueprint, name="Nova")
sol = spawn_explorer(blueprint, name="Sol")
assert nova["name"] == "Nova" # 1
assert sol["name"] == "Sol" # 2
assert blueprint == before # 3
assert identity_report(nova, sol) == { # 4
"same_outer": False,
"same_inventory": False,
"same_journal": False,
"same_runes": False,
"same_route": False,
"same_visited": False,
"same_rules": True,
}
assert nova["rules"] is blueprint["rules"] # 5Deliberate mutation and hashable room evidence
A list coordinate such as [1, 0] is unhashable and cannot be a set member. Keep this direct failure commented during the normal run:
Returned-copy and snapshot evidence
powered = with_supply(nova, "mirror lens")
assert powered["inventory"] == ["lantern", "rope", "mirror lens"] # 12
assert powered is not nova # 13
assert powered["inventory"] is not nova["inventory"] # 14
assert powered["journal"] is not nova["journal"] # 15
assert nova["inventory"] == ["lantern", "rope"] # 16
snapshot = journal_snapshot(nova)
assert snapshot == {"runes": ("SUN",), "route": ((1, 0),)} # 17
assert isinstance(snapshot["runes"], tuple) and isinstance( # 18
snapshot["route"], tuple
)Fresh default and final artifact evidence
first_attempt = remember_attempt("left mirror")
second_attempt = remember_attempt("right mirror")
assert first_attempt == ["left mirror"] # 19
assert second_attempt == ["right mirror"] # 20
assert first_attempt is not second_attempt # 21
record_rune_in_place(nova, (1, 1), "MOON")
record_rune_in_place(nova, (2, 1), "STAR")
artifact = vault_message(nova)
assert ( # 22
artifact == "NOVA BREAKS THE MIRROR VAULT WITH 3 RUNES"
and blueprint == before
and sol["journal"]["runes"] == []
)Reproduce and repair the shallow-copy curse
Use a separate fixture so deliberate failure evidence does not damage the main blueprint:
leaky_source = deepcopy(blueprint)
leaky_clone = leaky_source.copy()
leaky_clone["journal"]["runes"].append("SHADOW")
assert leaky_source["journal"]["runes"] == ["SHADOW"] # 23
repaired = spawn_explorer(leaky_source, name="Echo")
record_rune_in_place(repaired, (9, 9), "LIGHT")
assert ( # 24
leaky_source["journal"]["runes"] == ["SHADOW"]
and repaired["journal"]["runes"] == ["SHADOW", "LIGHT"]
and repaired["journal"]["runes"] is not leaky_source["journal"]["runes"]
)
WarningDo not repair a leak by weakening identity evidence
Two lists can contain equal values and still need independent future ownership. Keep both value and identity assertions when the contract promises isolated mutation.
6. Use the hint ladder only when needed
Hint 1: mark the intended object graph
The new outer dictionary, inventory list, journal dictionary, rune list, route list, and visited set need new identities for every explorer. Immutable strings may be reused, and the supplied rules tuple should remain the exact shared object. Check one access path at a time.
Hint 2: match each function to one ownership boundary
record_rune_in_place follows the supplied explorer and mutates its three relevant descendants. with_supply first calls the same safe spawning/copying logic with the explorer’s existing name, then appends only to the new inventory. The snapshot converts internal lists to tuples. The history helper constructs a list only when its parameter is None.
Hint 3: assemble the selective copy and artifact
Pseudocode:
The shallow variation leaks because its new outer dictionary still holds the source journal reference. spawn_explorer repairs every mutable path.
7. Keep debugging evidence
Preserve one failed assertion from before your repair. Connect it to one exact path rather than writing only “the copy was wrong.”
| Failure | Value and identity evidence | Ownership hypothesis | Controlled change | Verified rerun |
|---|---|---|---|---|
| Which numbered assertion failed? | What were ==, is, and relevant contents? |
Which outer or nested object is shared incorrectly? | Which one construction/copy/mutation changed? | Which earlier and new assertions now pass? |
Useful failures include a shared journal dictionary, independently copied journal with a still-shared rune list, a returned internal list, a reused default history, or a list room marker rejected as unhashable.
8. Compare with a complete solution
Open this only after attempting each assertion group and using hints in order.
Show one complete Mirror Vault solution
def spawn_explorer(blueprint, *, name):
"""Return an explorer with independent mutable state and shared rules."""
explorer = blueprint.copy()
explorer["name"] = name
explorer["inventory"] = blueprint["inventory"].copy()
explorer["journal"] = {
"runes": blueprint["journal"]["runes"].copy(),
"route": blueprint["journal"]["route"].copy(),
}
explorer["visited"] = blueprint["visited"].copy()
explorer["rules"] = blueprint["rules"]
return explorer
def identity_report(left, right):
"""Return labeled identity evidence for important explorer paths."""
return {
"same_outer": left is right,
"same_inventory": left["inventory"] is right["inventory"],
"same_journal": left["journal"] is right["journal"],
"same_runes": left["journal"]["runes"] is right["journal"]["runes"],
"same_route": left["journal"]["route"] is right["journal"]["route"],
"same_visited": left["visited"] is right["visited"],
"same_rules": left["rules"] is right["rules"],
}
def record_rune_in_place(explorer, room, rune):
"""Record room and rune in caller-owned explorer; return None."""
explorer["visited"].add(room)
explorer["journal"]["route"].append(room)
explorer["journal"]["runes"].append(rune)
def with_supply(explorer, supply):
"""Return independent explorer state containing one additional supply."""
updated = spawn_explorer(explorer, name=explorer["name"])
updated["inventory"].append(supply)
return updated
def journal_snapshot(explorer):
"""Return an immutable snapshot of rune and route sequences."""
return {
"runes": tuple(explorer["journal"]["runes"]),
"route": tuple(explorer["journal"]["route"]),
}
def remember_attempt(attempt, history=None):
"""Append attempt to supplied history or a fresh per-call list; return it."""
if history is None:
history = []
history.append(attempt)
return history
def vault_message(explorer):
"""Return the explorer's derived Mirror Vault escape message."""
name = explorer["name"].upper()
rune_count = len(explorer["journal"]["runes"])
return f"{name} BREAKS THE MIRROR VAULT WITH {rune_count} RUNES"9. Predict another ownership change
Suppose the vault adds mutable settings = {"sound": True} to the blueprint. Before editing code, predict:
- whether the current
spawn_explorershares that dictionary; - which identity-report field you would add;
- whether settings should be shared or independently editable;
- the minimum copy change for that ownership decision; and
- which source-preservation assertion should fail before the repair.
This variation checks whether you can extend the object graph rather than merely remember the existing field names.
10. Check your understanding
Answer from the completed object graph and assertions rather than the story alone.
11. Decide whether the challenge is complete
Evidence rubric
| Evidence | Ready to record when |
|---|---|
| Object graph | You can identify every intentionally shared and independently owned path. |
| Behavior | All 24 assertions pass unchanged from a clean state. |
| Mutation API | The in-place command changes only its supplied explorer and returns None. |
| Copy API | The transformer returns independently editable state and preserves its source. |
| Boundaries | Tuple markers, safe snapshots, and fresh default history behave as documented. |
| Debugging | One failed assertion is connected to a precise shared path and verified repair. |
| Artifact | The final sentence is derived from Nova’s name and collected rune count. |
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
- Object graphs make accidental sharing observable before a mutation spreads.
- Selective copying can isolate every mutable descendant while safely reusing an immutable value whose sharing is intentional.
- Hashable tuple coordinates make visited-room membership stable and meaningful.
- In-place commands, returned-copy transformers, snapshots, and optional defaults each need explicit ownership contracts.
- A shallow-copy failure is useful evidence when it identifies the exact nested reference that remained shared.
- The successful escape sentence is derived from independently owned state, not hidden directly in the implementation.