{
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: Decode the Starship Signal Vault
Preserve a damaged signal archive while building keyed, unique, grouped, ranked, and ordered views that reveal the vault code NOVA.
python-foundations
collections-iteration
unit-challenge
Course progress
0%
1. Challenge outcome
The research starship Wayfinder has recovered five signals from a damaged archive. Four active transmissions contain the pieces of a vault code. One inactive transmission is interference, two signals have equal strength, several symbols repeat, and the records arrived in an order that must remain evidence.
Build several deliberate views of the archive and use the tuple clues to unlock the final word NOVA. The puzzle is solved only when the source remains intact, every progressive check passes unchanged, and the final artifact is produced from the derived collections.
NoteMission rule
Run one assertion group at a time. Repair the first failed promise before moving on. Do not edit expected values, sort the source list, replace sets with display lists, or hard-code the final vault word.
The challenge uses simple for and if scaffolding because Unit 4 owns complete loop design. Your work is to choose the collection operations, preserve their relationships, and explain the state each derived view represents.
2. Understand the acceptance example
The final artifact must be exactly:
The code is not written directly in source order. Each active record contains a tuple (code_position, letter). Sort complete clue tuples by position and then unpack their letters.
Collection contracts
source_signalsremains the ordered source of truth.signals_by_idmaps every unique signal ID to its complete record.active_idspreserves transmission order while excluding inactive records.active_symbolscontains unique symbols from active records only.required_symbolsremains afrozensetand determinesvault_ready.sector_countscounts all records, including the inactive one.clue_pairspreserves active transmission order before sorting.ranked_signalsis a new list ordered by strength descending and ID ascending for equal strengths.sorted_cluesorders the tuple clues by code position.vault_wordis joined from derived letters, never typed as a solution value.
Constraints
- Keep every public variable name in the starter contract.
- Do not mutate any dictionary, set, tuple, or the outer source list.
- Do not use functions, comprehensions, exception handlers, classes, or nested loops.
- One straightforward traversal may build the lookup, active views, counts, and clue list together.
- Use tuple unpacking for every clue.
- Use set operations for symbol readiness.
- Use
operator.itemgetter()and stable sorting for ranking. - Sort only derived lists, never
source_signalsin place.
3. Start from the contract
Copy this complete starter into a fresh cell. source_snapshot is supplied so the checks can detect accidental nested mutation.
from operator import itemgetter
source_signals = [
{
"id": "SIG-N",
"sector": "Lyra",
"strength": 88,
"status": "active",
"clue": (0, "N"),
"symbols": {"star", "moon"},
},
{
"id": "SIG-X",
"sector": "Orion",
"strength": 99,
"status": "inactive",
"clue": (9, "X"),
"symbols": {"comet"},
},
{
"id": "SIG-O",
"sector": "Lyra",
"strength": 94,
"status": "active",
"clue": (1, "O"),
"symbols": {"moon", "key"},
},
{
"id": "SIG-A",
"sector": "Vela",
"strength": 82,
"status": "active",
"clue": (3, "A"),
"symbols": {"sun", "key"},
},
{
"id": "SIG-V",
"sector": "Orion",
"strength": 94,
"status": "active",
"clue": (2, "V"),
"symbols": {"star", "sun"},
},
]
source_snapshot = [
{
"id": "SIG-N",
"sector": "Lyra",
"strength": 88,
"status": "active",
"clue": (0, "N"),
"symbols": {"star", "moon"},
},
{
"id": "SIG-X",
"sector": "Orion",
"strength": 99,
"status": "inactive",
"clue": (9, "X"),
"symbols": {"comet"},
},
{
"id": "SIG-O",
"sector": "Lyra",
"strength": 94,
"status": "active",
"clue": (1, "O"),
"symbols": {"moon", "key"},
},
{
"id": "SIG-A",
"sector": "Vela",
"strength": 82,
"status": "active",
"clue": (3, "A"),
"symbols": {"sun", "key"},
},
{
"id": "SIG-V",
"sector": "Orion",
"strength": 94,
"status": "active",
"clue": (2, "V"),
"symbols": {"star", "sun"},
},
]
required_symbols = frozenset({"key", "moon", "star", "sun"})
# Stage A: archive views.
signals_by_id = {}
active_ids = []
active_symbols = set()
sector_counts = {}
clue_pairs = []
# Stage B: readiness, ranking, and decoding.
missing_symbols = None
vault_ready = None
ranked_signals = None
ranked_ids = []
sorted_clues = None
vault_letters = []
vault_word = None
# Stage C: final display values.
sector_text = None
symbol_text = None
artifact = None4. Build in small stages
Stage A: create archive views in one traversal
Use this supplied structure. Replace each None with the relevant collection operation; do not add another nested loop.
for signal in source_signals:
signal_id = signal["id"]
sector = signal["sector"]
# Map the stable ID to the complete signal record.
signals_by_id[signal_id] = None
# Count every record in its sector.
sector_counts[sector] = None
if signal["status"] == "active":
# Preserve active transmission order.
active_ids.append(None)
# Combine the members of this record's symbol set.
active_symbols.update(None)
# Unpack the fixed clue tuple, then preserve it as one tuple.
code_position, code_letter = signal["clue"]
clue_pairs.append(None)Run assertion group A. If a value has the wrong nesting level, inspect one signal record and the affected derived collection before changing another line.
Stage B: compare, rank, and decode
- Calculate symbols missing from the required frozen set.
- Calculate readiness with a subset relationship.
- Rank all signals by ID ascending first, then strength descending. The second stable pass makes strength primary while retaining ID order among ties.
- Traverse the ranked records to preserve their IDs in
ranked_ids. - Sort the clue tuples. Because their first values are unique integer positions, ordinary tuple ordering gives the intended sequence.
- Traverse
sorted_clues, unpack each tuple, and append only its letter. - Join the letters into
vault_word.
Run group B before formatting any display text.
Stage C: produce the vault artifact
The expected sector order is alphabetical. The expected symbol display is also alphabetical. Build:
Use an adjacent multi-line f-string for artifact. Derive the status without an if expression by using the supplied Boolean tuple lookup:
Do not type NOVA anywhere in the implementation.
5. Run progressive assertions
Group A: source, lookup, active order, counts, and clues
assert source_signals == source_snapshot
assert len(signals_by_id) == 5
assert list(signals_by_id) == ["SIG-N", "SIG-X", "SIG-O", "SIG-A", "SIG-V"]
assert signals_by_id["SIG-O"]["strength"] == 94
assert active_ids == ["SIG-N", "SIG-O", "SIG-A", "SIG-V"]
assert "SIG-X" not in active_ids
assert sector_counts == {"Lyra": 2, "Orion": 2, "Vela": 1}
assert clue_pairs == [(0, "N"), (1, "O"), (3, "A"), (2, "V")]Group B: unique membership, ranking, and decoded values
assert active_symbols == {"key", "moon", "star", "sun"}
assert type(required_symbols) is frozenset
assert missing_symbols == set()
assert vault_ready is True
assert ranked_ids == ["SIG-X", "SIG-O", "SIG-V", "SIG-N", "SIG-A"]
assert source_signals == source_snapshot
assert sorted_clues == [(0, "N"), (1, "O"), (2, "V"), (3, "A")]
assert vault_letters == ["N", "O", "V", "A"]
assert vault_word == "".join(vault_letters)Group C: final artifact
expected_artifact = (
"=== WAYFINDER SIGNAL VAULT ===\n"
"Status: OPEN\n"
"Code: NOVA\n"
"Signals: 4 active / 5 total\n"
"Sectors: Lyra=2, Orion=2, Vela=1\n"
"Symbols: key, moon, star, sun"
)
assert sector_text == "Lyra=2, Orion=2, Vela=1"
assert symbol_text == "key, moon, star, sun"
assert artifact == expected_artifact
print(artifact)
WarningA plausible result can still violate the contract
Typing vault_word = "NOVA", sorting source_signals in place, or adding inactive "comet" to the active symbol set may make one visible line look plausible while earlier evidence is wrong. The unchanged progressive checks are part of the artifact.
6. Use the hint ladder only when needed
Hint 1
Match each question to one collection behavior:
- ID lookup: dictionary assignment with
signal_idas key; - active transmission order: list append inside the active branch;
- unique active symbols: set update from each active symbol set;
- sector frequency:
.get(sector, 0) + 1; and - fixed clue meaning: unpack
(code_position, code_letter)and append that pair.
Run group A before attempting ranking.
Hint 2
The central Stage B operations have these shapes:
Use separate straightforward traversals to append ranked IDs and unpacked clue letters.
Hint 3
One complete final assembly has this structure:
vault_letters = []
for code_position, code_letter in sorted_clues:
vault_letters.append(code_letter)
vault_word = "".join(vault_letters)
vault_status = ("SEALED", "OPEN")[vault_ready]
artifact = (
"=== WAYFINDER SIGNAL VAULT ===\n"
f"Status: {vault_status}\n"
f"Code: {vault_word}\n"
f"Signals: {len(active_ids)} active / {len(source_signals)} total\n"
f"Sectors: {sector_text}\n"
f"Symbols: {symbol_text}"
)If the text still differs, compare repr(artifact) with repr(expected_artifact) to expose hidden spaces or newlines.
7. Keep debugging evidence
Preserve one real failed assertion or exception. A useful record connects the wrong collection decision to the observed value rather than saying only “the code did not work.”
| Failure | Exact evidence | One hypothesis | Controlled change | Verified rerun |
|---|---|---|---|---|
| What was the first blocked check? | Include actual/expected values or the final exception line. | Which key, position, mutation, or collection operation explains it? | What one line changed? | Which unchanged group now passes? |
Good puzzle evidence could include:
- appending a symbol set and creating a nested list instead of updating a set;
- counting only active signals instead of all sector records;
- directly traversing a dictionary and expecting complete records;
- applying only the strength sort and losing the explicit tie policy;
- unpacking clue fields in the wrong order; or
- mutating a source symbol set while combining membership.
8. Test the echo-signal variation
After the ordinary challenge passes from a clean state, append this record to a new outer list—not to source_signals:
Copy your implementation to a new section, use variation_signals as its input, and recalculate every derived collection from empty state. Verify:
assert source_signals == source_snapshot
assert len(variation_signals) == 6
assert "SIG-ECHO" in signals_by_id
assert active_ids == ["SIG-N", "SIG-O", "SIG-A", "SIG-V"]
assert active_symbols == {"key", "moon", "star", "sun"}
assert sector_counts == {"Lyra": 2, "Orion": 2, "Vela": 2}
assert sorted_clues == [(0, "N"), (1, "O"), (2, "V"), (3, "A")]
assert vault_word == "NO" + "VA"
assert ranked_ids[-1] == "SIG-ECHO"Explain why the lookup, total record count, Vela count, and ranking change while the active order, required-symbol readiness, clue order, and decoded word do not.
9. Compare with a solution path
Reveal after your checks pass or all three hints have been used
from operator import itemgetter
# Use source_signals, source_snapshot, and required_symbols from the starter.
signals_by_id = {}
active_ids = []
active_symbols = set()
sector_counts = {}
clue_pairs = []
for signal in source_signals:
signal_id = signal["id"]
sector = signal["sector"]
signals_by_id[signal_id] = signal
sector_counts[sector] = sector_counts.get(sector, 0) + 1
if signal["status"] == "active":
active_ids.append(signal_id)
active_symbols.update(signal["symbols"])
code_position, code_letter = signal["clue"]
clue_pairs.append((code_position, code_letter))
missing_symbols = required_symbols - active_symbols
vault_ready = required_symbols <= active_symbols
ranked_signals = sorted(source_signals, key=itemgetter("id"))
ranked_signals = sorted(
ranked_signals,
key=itemgetter("strength"),
reverse=True,
)
ranked_ids = []
for signal in ranked_signals:
ranked_ids.append(signal["id"])
sorted_clues = sorted(clue_pairs)
vault_letters = []
for code_position, code_letter in sorted_clues:
vault_letters.append(code_letter)
vault_word = "".join(vault_letters)
vault_status = ("SEALED", "OPEN")[vault_ready]
sector_text = (
f"Lyra={sector_counts['Lyra']}, "
f"Orion={sector_counts['Orion']}, "
f"Vela={sector_counts['Vela']}"
)
symbol_text = ", ".join(sorted(active_symbols))
artifact = (
"=== WAYFINDER SIGNAL VAULT ===\n"
f"Status: {vault_status}\n"
f"Code: {vault_word}\n"
f"Signals: {len(active_ids)} active / {len(source_signals)} total\n"
f"Sectors: {sector_text}\n"
f"Symbols: {symbol_text}"
)
print(artifact)The source list provides authoritative order. The lookup and counts include all records; the active list, symbol set, and clue tuples include only active records. Two stable ranking passes preserve an explicit ID tie rule. The word is derived from sorted fixed-position clues, and display sorting is applied only to a new symbol list.
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 progressive checks pass unchanged and the artifact displays exactly. |
| Collection design | You can explain why each derived view is a list, tuple, dictionary, set, or frozenset. |
| Source preservation | The source snapshot passes before and after ranking and variation work. |
| Debugging | One failure record connects exact evidence, one hypothesis, one change, and a passing rerun. |
| Reproducibility | The ordinary challenge and 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
- One ordered source can support keyed, grouped, unique, ranked, and display views when every view has a named purpose.
- Progressive assertions reveal the first broken collection contract before a plausible final string hides it.
- Stable sorting and complete tuple/record movement preserve relationships among tied or repositioned values.
- Sets answer unique-membership questions; frozen sets can state immutable requirements; lists preserve the orders the puzzle actually needs.
- A clean rerun, boundary variation, and debugging record are part of the solved artifact.