{
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
Turn a Vague Idea into Testable Examples
Clarify an ambiguous request by defining inputs, outputs, rules, ties, boundaries, constraints, and independently calculated acceptance examples.
python-foundations
problem-solving-algorithms
specifications
acceptance-examples
Course progress
0%
A program can be beautifully formatted, run without errors, and still answer the wrong question. Before choosing a loop or function, you need to know what result would count as correct.
In this lesson, you will clarify a program that chooses a stargazing site. Keep these questions visible while you work:
- What information enters the program, and what exactly comes back?
- Which words in the request hide an unstated decision?
- Which small input would distinguish two competing interpretations?
- What happens at a tie, threshold, empty input, or no-result case?
- Can the expected result be calculated without trusting unfinished code?
1. “Choose the best site” has more than one answer
Start with two site records:
Read each key as a fact the program may use. Pine Ridge has a darker sky, but Moon Lake is closer. Either name could be a sensible answer to “best.” Running more Python will not settle a decision the request never made.
Write two predictions before continuing:
Both predictions agree with the input. They disagree because they implement different rules.
WarningA reasonable assumption is still an assumption
When a rule affects observable output, record it or ask for clarification. Do not bury it inside a comparison and hope every reader would choose the same meaning.
2. Mark the data, action, and missing rules
Consider this fuller request:
From the open, accessible sites, choose the site with the greatest darkness score. If darkness ties, choose the shorter drive. Return its name. If no site qualifies, return
None. Do not change the supplied records.
The nouns suggest data:
- sites: a collection of candidate records;
- open and accessible: eligibility facts;
- darkness and minutes: comparison values;
- name: the returned text.
The verbs and comparison phrases suggest behavior:
- choose one site;
- from only qualifying candidates;
- greatest darkness first;
- shorter drive on a darkness tie;
- return a name or
None; - do not change the caller’s data.
This annotation does not determine the exact loop. It determines what any loop must accomplish.
Separate product rules from implementation choices
These are product rules because changing them changes observable behavior:
These are implementation choices because several versions could preserve the same behavior:
Lesson 3 compares strategies. For now, do not put an implementation detail such as “must sort” into the contract unless there is a real external reason.
3. Ask questions before assumptions become code
A short requirements interview can prevent a long repair. For the site chooser, ask:
| Question | Why the answer matters |
|---|---|
| Is a larger darkness score always better? | Establishes comparison direction |
| Must a site be both open and accessible? | Establishes eligibility logic |
| What decides an exact tie on darkness and travel? | Prevents unstable output |
| Is an empty list supported? | Establishes no-result behavior |
| May the function reorder or edit the list? | Establishes ownership behavior |
| Do all records contain the named keys? | Establishes a precondition |
| About how many sites are expected? | May influence later cost choices |
Assume the answer to the exact-tie question is: keep the site that appears first in the input. That rule matters even if the sample data has no exact tie.
TipAsk the question that could change an assertion
A useful clarification changes an input, an expected result, or a supported behavior. Editor theme, internal variable names, and preferred line count do not belong in this contract.
4. State the contract in observable language
A compact contract can live in prose before it becomes a docstring:
- Input: a list of site dictionaries containing
name,darkness,minutes,accessible, andopen. - Precondition: the fields already contain supported values; detailed validation is outside this function.
- Eligibility: both
openandaccessiblemust be true. - Selection: greatest darkness, then shortest drive, then earliest input position.
- Return: the selected site’s name, or
Nonewhen no site qualifies. - Ownership: reading is allowed; changing the list or its records is not.
A future function interface can express the same promise:
pass is a placeholder, not an implementation. The valuable work at this stage is the promise surrounding it.
5. Calculate one ordinary result by hand
Use candidates with different darkness scores so the main rule decides:
The manual calculation is:
- both records satisfy the two eligibility conditions;
- darkness scores are
7and9; 9is greater, so the travel-time tie rule is not used;- the promised return shape is a name, so the result is
"Pine Ridge".
Later, the acceptance check will be:
The assertion may currently fail because choose_site contains only pass. That is expected. The check describes a target before the implementation exists.
6. Use the smallest case that distinguishes two rules
A one-site input can check return shape, but it cannot distinguish the main rule from the tie rule. Use two equally dark sites:
If an implementation chooses only the greatest darkness value and keeps the first match, it returns "Pine Ridge". This example exposes the missing travel rule with only two records.
Now isolate the final tie rule:
The cases are small because each one answers one design question.
Checkpoint: rules that produce observable results
7. Boundaries appear where behavior can change
A boundary is not merely an unusual value. It is a point near which the rule or result may change. For eligibility, the boundary is the change from False to True. For a numeric threshold such as “darkness at least 7,” check values just below, exactly at, and just above the threshold.
For the current site contract, useful boundaries include:
A no-result input need not be empty. It can contain records that all fail the eligibility rule:
These two no-result cases reach the same output through different input conditions. Both are useful because they can expose different mistakes.
8. Counterexamples challenge an incomplete rule
Suppose someone proposes:
Return the site with the greatest darkness score.
This input is a counterexample:
The greatest darkness belongs to an ineligible site. The input is more useful than saying “remember eligibility” because it can be run against an implementation.
A counterexample should be as small as practical. Extra records can hide the one relationship that disproves the rule.
9. Expected values need an independent source
This check looks official but proves nothing:
If the calculation were wrong for a subtle reason, both sides would repeat the same mistake. Instead, calculate a small expected result separately:
For a more complex rule, keep a short note beside the expected value:
Independent does not mean another large program. It often means arithmetic by hand, a tiny table, an agreed example from a stakeholder, or a simpler reference method used only on small inputs.
10. Constraints must be concrete enough to affect a design
“Make it fast” gives no usable target. Compare these statements:
- The program usually receives fewer than 20 sites.
- The program may receive one million sites.
- The result must be returned without changing the input order.
- The program may use one additional collection proportional to the input.
- The function is called once per evening, not thousands of times per second.
These facts can affect later choices. With 20 sites, a direct, easy-to-explain solution may be preferable even if another strategy has better growth. With one million records, repeated scans deserve closer attention.
Do not confuse a constraint with a premature command:
The useful statement describes observable behavior and scale. The premature statement chooses tools before alternatives have been compared.
Checkpoint: boundaries and trustworthy expectations
11. Build an acceptance table before implementation
Combine the examples into one compact specification:
| Case | Important input feature | Expected | Rule exercised |
|---|---|---|---|
| ordinary | eligible sites with different darkness | "Pine Ridge" |
greatest darkness |
| darkness tie | equal darkness, different travel | "Moon Lake" |
shortest drive |
| exact tie | equal darkness and travel | "North Field" |
earliest input |
| one site | one eligible record | "Solo Hill" |
smallest successful input |
| empty | no records | None |
no candidate |
| all unavailable | records exist but none qualify | None |
eligibility |
| closed but darker | ineligible record would otherwise win | "Open Meadow" |
filter before selection |
Ask whether each row could catch a bug that the others might miss. If not, combine or remove redundant cases. More cases are not automatically better; each should have a reason.
Represent the stable table as data when you are ready to implement:
Lesson 2 will use this collection to guide incremental implementation.
12. Repair specifications that cannot be checked
Rewrite each weak statement before opening the suggested repair.
“Return a useful answer”
Show one checkable repair
Return the selected site’s name string, or None if no site is both open and accessible.
“Choose the first good site”
Show the missing questions
Define what good means, whether the input order is meaningful, and whether a later site can replace the first one. If the intended contract is first-match search, state the exact condition and no-match result.
“The program should be efficient”
Show a measurable replacement
State the expected input range and important resource constraint. For example: “Support up to 100,000 site records without modifying them; compare one-pass and sorted-copy strategies before choosing.”
13. Lab: specify a moon-base supply selector
A moon base receives supply-pod records:
The request is deliberately vague:
Choose the best usable supply pod and show its ID.
Do not implement the selector yet. Produce:
- a list of clarification questions;
- a chosen rule for usability;
- a primary comparison and at least two tie rules;
- an exact return shape and no-result behavior;
- a non-mutation promise;
- an expected input-size statement; and
- at least six acceptance examples covering ordinary, boundary, tie, and counterexample behavior.
NoteOne possible rule set
If you need a starting direction, choose undamaged pods, prefer greater oxygen, then lower mass, then earlier input position. Return the ID or None.
Hint: choose cases that can disagree
Include a damaged pod with the highest oxygen, two undamaged pods with equal oxygen and different mass, an exact tie, one usable pod, no records, and only damaged records.
Show a model acceptance strategy
A strong table states that a pod is usable when damaged is False; greater oxygen wins; lower mass breaks an oxygen tie; earlier input breaks an exact tie; the return value is an ID or None; and input records remain unchanged. Calculate every expected ID by hand and write one sentence naming the rule each case isolates.
14. Check the complete specification
Run this checklist against both the site chooser and your supply selector:
Checkpoint: a specification ready for implementation
Key points
- Clarify inputs, outputs, eligibility, priority, ties, no-result behavior, ownership, and scale before choosing implementation details.
- An acceptance example contains explicit input and an independently calculated observable result.
- Ordinary cases show intended behavior; boundaries and counterexamples expose incomplete rules.
- A small case is powerful when it distinguishes two plausible implementations.
- Constraints should describe supported scale or behavior, not prescribe a favorite tool without evidence.