{
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
Turning Rules into Complete Decisions
Convert prose policies into decision tables, find boundary gaps and overlaps, encode precedence, and use simple match statements for discrete choices.
python-foundations
decisions-repetition
decision-tables
Course progress
0%
1. Turn a ticket policy into rows before code
A night festival publishes this policy:
- children under 13 pay 8 credits;
- visitors from 13 through 64 pay 18 credits;
- visitors 65 or older pay 10 credits; and
- volunteers with an active badge enter free.
The words sound clear, but code forces exact answers. A decision table makes the cases and their precedence visible first:
| Priority | Facts | Price | Reason |
|---|---|---|---|
| 1 | active volunteer badge | 0 | volunteer |
| 2 | age below 13 | 8 | child |
| 3 | age from 13 through 64 | 18 | standard |
| 4 | age at least 65 | 10 | senior |
The volunteer row is an override, so it appears first. The age rows are mutually exclusive and together cover every expected non-negative age.
The second age condition can say only age < 65 because reaching it already proves age >= 13. The table preserves the full intended interval.
Questions this lesson will answer
- How can a table reveal missing, overlapping, or unreachable cases?
- Which endpoints belong on each side of a threshold?
- How does precedence differ from an accidental branch-order effect?
- When should a result include an explanation as well as a value?
- When is
matchclearer than a chain of equality tests?
2. Separate mutually exclusive cases from cumulative rules
A single classification should select exactly one row. Ticket price is one classification: one visitor receives one base price. Use an if/elif/else chain.
Cumulative rules may all apply. A visitor could receive several notices:
Both independent facts are useful, so two if statements are correct. A table can identify this distinction:
| Rule type | Expected selected rows | Python shape |
|---|---|---|
| classification | exactly one | if / elif / else |
| optional override followed by classification | first matching precedence row | ordered branch chain |
| cumulative notices or effects | zero, one, or many | independent if statements |
Mixing these purposes is a common source of missing work. Decide whether rows are alternatives or cumulative before translating them.
3. Boundaries belong to one case, not two or none
Suppose a temperature policy says cold is below 10 and warm is above 10:
At exactly 10, neither branch runs. If label was not already defined, later use raises NameError. This is a gap.
Now suppose the policy uses temperature <= 10 and then temperature >= 10. Both conditions include 10. In an elif chain only the first wins, but the table still has an overlap whose outcome depends on row order.
Write interval notation in ordinary words:
| Case | Included values | Boundary checks |
|---|---|---|
| cold | below 10 | 9 |
| mild | 10 through 19 | 10, 19 |
| warm | 20 or above | 20 |
Test one value just below, exactly at, and just above every threshold: 9, 10, 11, 19, 20, and 21. Typical middle values do not prove endpoint behavior.
Checkpoint: complete case design
4. Precedence makes overrides deliberate
Some rows overlap because the policy says one rule overrides another. The volunteer can also be a child or senior, yet the free-price row wins. Record that priority rather than pretending the cases are disjoint.
A weather closure can override every ticket rule:
| Priority | Condition | Result |
|---|---|---|
| 1 | festival closed by weather | unavailable |
| 2 | active volunteer | free |
| 3 | age band | band price |
None communicates that a price is unavailable, not zero. A zero price is a real price for a free admission. This distinction would disappear with a simple truthiness check.
A precedence question should be answerable in words: “weather closure wins over volunteer status, which wins over age.” If you can only explain it as “whatever branch happens to come first,” the design is not finished.
5. Keep a result and its reason together in the trace
A lone result can be hard to diagnose. Produce a reason in the same selected branch:
score = 84
submitted = True
if not submitted:
outcome = "incomplete"
reason = "submission missing"
elif score >= 90:
outcome = "distinction"
reason = "score at least 90"
elif score >= 70:
outcome = "pass"
reason = "score from 70 through 89"
else:
outcome = "retry"
reason = "score below 70"
print(outcome, "—", reason)The two names should be assigned together on every path. A trace table can then record both:
| Input | Conditions reached | Outcome | Reason |
|---|---|---|---|
| not submitted | first true | incomplete | submission missing |
| submitted, 90 | second true | distinction | score at least 90 |
| submitted, 70 | third true | pass | score from 70 through 89 |
| submitted, 69 | fallback | retry | score below 70 |
That table doubles as a compact test plan.
6. Find overlaps mechanically with sample rows
When intervals are complex, enumerate representative values and count matching rules:
Booleans behave like 1 and 0 in this narrow counting use. A classification row with matches == 0 has a gap. A row with matches > 1 has an overlap. Here the value 10 matches two conditions.
This diagnostic does not replace the final branch chain. It helps you inspect whether the written table matches the intended relationship.
Checkpoint: precedence and evidence
7. Use match for clear discrete choices
Python’s match statement can make a menu of literal commands easy to scan:
The cases are checked in order. The vertical bar means either literal pattern. The _ wildcard is the fallback and should come last because it matches anything.
For one or two equality checks, an if statement is often simpler. Use match when several discrete shapes or commands are the natural table. Do not use it to replace ordered numeric ranges:
This lesson uses only literal choices, alternatives, and the wildcard. More advanced structural patterns belong after you have reusable data models.
8. Avoid a wildcard that hides a required error state
A fallback needs a deliberate meaning. For an interactive command, "unknown command" is useful. For internal states that should already be validated, a wildcard that silently substitutes a normal result can hide a bug.
The fallback keeps message defined but does not pretend the unknown state is ready. Unit 8 will introduce raising and handling exceptions. For now, preserve an unmistakable invalid result and assert the allowed states near their source.
Checkpoint: discrete choices
9. Build the festival ticket desk
Create a complete decision for these inputs:
Use this contract:
- If the festival is closed,
pricestaysNone, category is"unavailable", and the reason identifies closure. - Active volunteers enter free before any age rule.
- Under 13 costs 8 credits; ages 13 through 64 cost 18; age 65 or above costs
- A morning time slot subtracts 2 credits from any positive price. Free and unavailable admission do not change.
- Keep the original category reason and add
"; morning discount"only when the discount applies. - Produce
summarycontaining the category, displayed price ("N/A"forNone), and reason.
Run the supplied-case checks:
Create a table and test at least these cases: closed volunteer, ages 12, 13, 64, 65, a morning child, a morning senior, and a morning volunteer.
Hint: make classification and discount separate stages
Use one precedence-ordered branch chain to set price, category, and reason together. Then use an independent condition requiring price is not None and price > 0 before subtracting the morning discount.
Show one complete solution after attempting the lab
festival_open = True
age = 67
active_volunteer = False
time_slot = "evening"
if not festival_open:
price = None
category = "unavailable"
reason = "festival closed"
elif active_volunteer:
price = 0
category = "volunteer"
reason = "active volunteer"
elif age < 13:
price = 8
category = "child"
reason = "age below 13"
elif age < 65:
price = 18
category = "standard"
reason = "age from 13 through 64"
else:
price = 10
category = "senior"
reason = "age at least 65"
if time_slot == "morning" and price is not None and price > 0:
price -= 2
reason += "; morning discount"
price_text = "N/A" if price is None else f"{price} credits"
summary = f"{category} | {price_text} | {reason}"The second stage is cumulative policy, not another admission category. Explicit is not None preserves unavailable versus free.
10. Explain the table
- Which rows are mutually exclusive, and which rule is cumulative?
- Why does a volunteer override need a stated priority?
- How do 12, 13, 64, and 65 expose every age endpoint?
- Why are price and reason assigned in the same branch?
- When is a literal
matchclearer than anifchain, and when is it not?
Key points
TipKey points
- Write facts, priorities, results, and reasons as table rows before translating a multi-rule policy.
- A classification selects one case; cumulative rules require independent checks.
- Test just below, at, and just above every threshold to find gaps and overlaps.
- State override precedence in words and encode the highest priority first.
- Preserve a reason beside a result so the selected path remains observable.
- Use simple
matchcases for discrete literals; use ordered comparisons for numeric ranges.