{
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: Wake the Clockwork Observatory
Derive an observatory wake-up code by decomposing a signal puzzle into frequency, selection, window-search, and running-total algorithms.
python-foundations
problem-solving-algorithms
unit-challenge
puzzle
Course progress
0%
1. Wake the observatory with a code you derive
The hilltop clockwork observatory has slept through three meteor showers. Its brass dome will move only when a signal calibrator produces a three-part code:
You receive a symbol signal and matching energy readings:
Your program must discover:
- where the first all-distinct symbol window ends;
- when accumulated energy first reaches the target; and
- which symbol occurs most often, with first appearance winning a tie.
For the supplied data, the finished program should reveal:
You will choose the state and conditions that produce that result. The page supplies names, docstrings, staged assertions, three hints, and a hidden solution for use after a serious attempt.
NoteWork independently, but not without support
Plan for one or two focused hours. Read one contract, make the matching small check pass, and rerun earlier checks before moving forward. Open a hint only after writing down the exact value or condition that blocks you.
2. Read the signal rules and acceptance examples
Frequency and anchor rules
- Count every symbol exactly as written.
- The anchor is the symbol with the greatest count.
- If counts tie, the symbol that appears earlier in the signal wins.
- An empty signal has no anchor, represented by
None.
The first assertion only confirms the hand-counted mapping is well formed. Your function checks begin in Section 5.
Synchronization-window rules
A window is a consecutive slice with exactly window_width symbols. It is synchronized when every symbol inside it is distinct. Return the 1-based end position, which is also the number of symbols processed when the window ends. Return None if no such window exists.
For "AABCDEFAC" and width 4:
| End position | Window | All distinct? |
|---|---|---|
| 4 | AABC |
no |
| 5 | ABCD |
yes |
The synchronization result is therefore 5.
Power rules
Add readings from left to right. Return the 1-based step where the running total first becomes greater than or equal to the target. Return None if all readings are consumed without reaching it.
| Step | Reading | Running total | Reached 10? |
|---|---|---|---|
| 1 | 2 | 2 | no |
| 2 | 1 | 3 | no |
| 3 | 3 | 6 | no |
| 4 | 2 | 8 | no |
| 5 | 4 | 12 | yes |
The power result is 5. Stop there because the contract asks for the first reached step.
Input and output constraints
signalis a finite string.readingsis a finite list of non-negative numbers.widthandtargetare positive integers.- Functions must not modify supplied inputs.
- Use the supplied names and returned shapes.
- Do not hard-code
5,5,A, or the final code. - If the anchor, synchronization, or power result is missing, the complete code is
None. - Explain time and extra-space growth in plain language after the checks pass.
Before implementation, add one acceptance example of your own for each:
- a frequency tie;
- a window that never synchronizes; and
- an energy sequence that never reaches its target.
3. Start from the contract
Run this cell unchanged. Replace each pass while preserving the public names and docstrings.
def symbol_frequencies(signal):
"""Return a dictionary that counts every symbol in signal."""
pass
def choose_anchor(signal, frequencies):
"""Return the most frequent symbol; the first appearance wins a tie."""
pass
def first_sync_end(signal, width):
"""Return the 1-based end of the first all-distinct window, or None."""
pass
def first_power_step(readings, target):
"""Return the 1-based step where the running total reaches target, or None."""
pass
def build_wakeup_code(signal, readings, width, target):
"""Return '<sync>-<power>-<anchor>', or None if calibration cannot finish."""
passUse this responsibility table before writing loops:
| Function | Result shape | State to consider | May stop early? |
|---|---|---|---|
symbol_frequencies |
dictionary | counts by symbol | no |
choose_anchor |
symbol or None |
best symbol/count | yes only for empty input |
first_sync_end |
integer or None |
candidate window/end | yes on first match |
first_power_step |
integer or None |
running total/step | yes on reached target |
build_wakeup_code |
string or None |
returned helper results | after either failed result |
You may implement choose_anchor by traversing signal and consulting the frequency mapping. That makes encounter-order tie behavior visible without sorting the dictionary.
4. Calibrate one mechanism at a time
Stage A: count symbols
Start with empty and repeated inputs. Do not work on anchor selection until both frequency checks pass.
A useful trace for "ABACA" has columns for current symbol and mapping after the update.
Stage B: select the anchor
Keep the current anchor when a candidate’s count is equal. Replace only when the candidate count is strictly greater. Repeated visits to the same symbol should not change the result.
Stage C: find synchronization
Name the meaning of end before coding. For an end-exclusive Python slice, signal[end - width:end], the same end is already the promised 1-based count of processed symbols.
Stage D: accumulate power
Use enumerate(readings, start=1) or convert a zero-based index deliberately. Add the current reading before asking whether the target has been reached.
Stage E: assemble the code
Call the four helpers. If the sync result, power result, or anchor is None, return None. Otherwise return one formatted string.
After every stage:
- run its smallest assertion;
- trace actual state if it fails;
- change one condition or update;
- rerun the stage; and
- rerun all earlier stages.
5. Run progressive assertions
Do not change these expected values merely to make a check pass. If code and expectation disagree, compare both with the written rules and hand traces.
Symbol-frequency evidence
Anchor and tie evidence
Synchronization evidence
Running-power evidence
Complete wake-up evidence
Ownership and changed-input evidence
Add one complete assertion with a new signal, new readings, a different width, and a different target. Calculate its expected code on paper first.
6. Use the hint ladder only when needed
Hint 1
Match each helper to a pattern: frequency table, running-best selection, fixed-size window search, running-total search, and function composition.
Hint 2
For frequencies, update frequencies.get(symbol, 0) + 1. For the anchor, scan the original signal and replace only when a symbol’s count is strictly greater than the current anchor’s count. For synchronization, compare len(set(window)) with width. For power, add before checking the target.
Hint 3
Use these incomplete shapes:
Every search also needs a return after the loop for the no-result path.
7. Keep debugging evidence
Keep one failure that helped you understand the puzzle. Record facts before changing code.
| Failure | Actual evidence | Single-cause hypothesis | Controlled change | Verified rerun |
|---|---|---|---|---|
| What failed? | Exact returned value, state, or message | Which one condition/update fits? | What one thing changed? | Which focused and earlier checks pass? |
Useful failures include:
- returning a zero-based start instead of a 1-based end;
- replacing the anchor on an equal frequency and therefore choosing the last tied symbol;
- checking power before adding the current reading;
- returning
Nonefrom inside a loop after the first non-match; or - constructing a code containing the text
Noneinstead of returningNone.
Restart the notebook or Python process and run every cell from the contract through the complete assertions. A result that depends on hidden state is not a finished solution.
8. Compare with a complete solution
Show the complete implementation after attempting every stage
def symbol_frequencies(signal):
"""Return a dictionary that counts every symbol in signal."""
frequencies = {}
for symbol in signal:
frequencies[symbol] = frequencies.get(symbol, 0) + 1
return frequencies
def choose_anchor(signal, frequencies):
"""Return the most frequent symbol; the first appearance wins a tie."""
anchor = None
for symbol in signal:
if anchor is None or frequencies[symbol] > frequencies[anchor]:
anchor = symbol
return anchor
def first_sync_end(signal, width):
"""Return the 1-based end of the first all-distinct window, or None."""
for end in range(width, len(signal) + 1):
window = signal[end - width : end]
if len(set(window)) == width:
return end
return None
def first_power_step(readings, target):
"""Return the 1-based step where the running total reaches target, or None."""
total = 0
for step, reading in enumerate(readings, start=1):
total += reading
if total >= target:
return step
return None
def build_wakeup_code(signal, readings, width, target):
"""Return '<sync>-<power>-<anchor>', or None if calibration cannot finish."""
frequencies = symbol_frequencies(signal)
anchor = choose_anchor(signal, frequencies)
sync_end = first_sync_end(signal, width)
power_step = first_power_step(readings, target)
if anchor is None or sync_end is None or power_step is None:
return None
return f"{sync_end}-{power_step}-{anchor}"Why the main decisions work:
choose_anchorscans in encounter order and replaces only for a strictly larger count, so an equal count keeps the earlier symbol.endbegins atwidth;signal[end - width:end]has the requested width, andendis already the 1-based number of processed symbols.first_power_stepadds the current reading before comparing with the target, so the returned step includes the reading that reaches it.- The composition function returns
Nonerather than formatting an incomplete code.
For a signal of length n, frequency counting and anchor selection each grow linearly. With fixed window width w, the window search checks up to about n windows and builds a set of up to w symbols for each, so its time is O(n × w) and its temporary window space is O(w). Power search is linear in its number of readings in the worst case. The frequency dictionary uses space proportional to the number of distinct symbols.
9. Predict a changed calibration rule
The observatory engineer proposes two changes:
- on an anchor-frequency tie, choose the later symbol; and
- return the zero-based window start instead of the 1-based end.
Before editing code, answer:
- Which acceptance examples change?
- Which comparison changes from strict to inclusive, and why can repeated visits complicate that shortcut?
- How is a start index calculated from the current
endandwidth? - Does the wake-up code for the original input change?
- Which docstrings and cost statements remain valid?
A robust later-wins anchor strategy should compare distinct candidates or track the last position deliberately; blindly replacing on every equal count also replaces an anchor when the loop sees the same symbol again. Write a small tie trace before implementing the changed rule.
10. Check your understanding
11. Decide whether the challenge is complete
Evidence rubric
| Evidence | Ready to record when |
|---|---|
| Behavior | All supplied assertions and one learner-created case pass unchanged. |
| Decomposition | Each public function has one named responsibility and returned shape. |
| Reasoning | Tie, index, stopping, no-result, time, and extra-space decisions can be explained. |
| Debugging | One failure record connects exact evidence to one change and verified reruns. |
| Reproducibility | The complete observatory program works from a clean notebook state. |
This button stores a self-reported marker only in this browser. It does not submit or grade the program.
Not yet recorded.
Key points
- The observatory puzzle becomes manageable when counting, selection, window search, accumulation, and composition have separate contracts.
- Progressive assertions reveal which responsibility is wrong without revealing the implementation.
- Tie rules, index meanings, stopping behavior, and no-result values belong in examples before they become conditions.
- Correct behavior, a clean rerun, and a plain-language cost explanation are all part of the finished solution.