<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P010-quiz/ | updated: 2026-10-03 -->

# P010 Quiz

Take a multiple-choice quiz, get a score, review the questions you missed and retake only those until none are left; every attempt is kept in a score history - from a text menu.

EML-P project `projects/quiz` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/quiz` runs it and `eml project verify projects/quiz` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: simple-quiz-grader (https://efficientnewlanguage.org/cases/041-simple-quiz-grader/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
9
2
3
4
1
e

ab
b
q
4
1
b
b
c
c
d
a
b
a
3
x
q
4
3
c
3
2
4
5
```

Screen:

```text

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 9
Pick a number from 1 to 5.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 2
Take the quiz first.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 3
Take the quiz first.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 4

-- scores --
  (no attempts yet)

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 1

Q1. What gas do plants take in for photosynthesis?
   A) Oxygen
   B) Carbon dioxide
   C) Nitrogen
   D) Helium
answer> e
Answer with A, B, C or D (or q to stop).
answer> 
Answer with A, B, C or D (or q to stop).
answer> ab
Answer with A, B, C or D (or q to stop).
answer> b
Right.

Q2. How many bones are in the adult human body?
   A) 106
   B) 206
   C) 306
   D) 406
answer> q
Quiz stopped; not scored.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 4

-- scores --
  (no attempts yet)

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 1

Q1. What gas do plants take in for photosynthesis?
   A) Oxygen
   B) Carbon dioxide
   C) Nitrogen
   D) Helium
answer> b
Right.

Q2. How many bones are in the adult human body?
   A) 106
   B) 206
   C) 306
   D) 406
answer> b
Right.

Q3. What is the chemical symbol for gold?
   A) Ag
   B) Gd
   C) Au
   D) Go
answer> c
Right.

Q4. Which planet is closest to the Sun?
   A) Venus
   B) Mars
   C) Mercury
   D) Earth
answer> c
Right.

Q5. What is the largest ocean on Earth?
   A) Atlantic
   B) Indian
   C) Arctic
   D) Pacific
answer> d
Right.

Q6. Which river flows through Cairo?
   A) Nile
   B) Amazon
   C) Danube
   D) Ganges
answer> a
Right.

Q7. On which continent is the Sahara desert?
   A) Asia
   B) Africa
   C) Australia
   D) South America
answer> b
Right.

Q8. What is the capital of Canada?
   A) Toronto
   B) Vancouver
   C) Ottawa
   D) Montreal
answer> a
Wrong - it is C) Ottawa.
Score: 7/8 (88%).
1 to review.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 3

Q8. What is the capital of Canada?
   A) Toronto
   B) Vancouver
   C) Ottawa
   D) Montreal
answer> x
Answer with A, B, C or D (or q to stop).
answer> q
Retake stopped; not scored.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 4

-- scores --
  1) quiz     7/8   88%

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 3

Q8. What is the capital of Canada?
   A) Toronto
   B) Vancouver
   C) Ottawa
   D) Montreal
answer> c
Right.
Score: 1/1 (100%).
No mistakes - well done.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 3
No mistakes to retake.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 2
No mistakes to review.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 4

-- scores --
  1) quiz     7/8   88%
  2) retake   1/1  100%

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
B
B
A
C
D
A
A
C
2
3
C
C
2
3
B
2
4
5
```

Screen:

```text

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 1

Q1. What gas do plants take in for photosynthesis?
   A) Oxygen
   B) Carbon dioxide
   C) Nitrogen
   D) Helium
answer> B
Right.

Q2. How many bones are in the adult human body?
   A) 106
   B) 206
   C) 306
   D) 406
answer> B
Right.

Q3. What is the chemical symbol for gold?
   A) Ag
   B) Gd
   C) Au
   D) Go
answer> A
Wrong - it is C) Au.

Q4. Which planet is closest to the Sun?
   A) Venus
   B) Mars
   C) Mercury
   D) Earth
answer> C
Right.

Q5. What is the largest ocean on Earth?
   A) Atlantic
   B) Indian
   C) Arctic
   D) Pacific
answer> D
Right.

Q6. Which river flows through Cairo?
   A) Nile
   B) Amazon
   C) Danube
   D) Ganges
answer> A
Right.

Q7. On which continent is the Sahara desert?
   A) Asia
   B) Africa
   C) Australia
   D) South America
answer> A
Wrong - it is B) Africa.

Q8. What is the capital of Canada?
   A) Toronto
   B) Vancouver
   C) Ottawa
   D) Montreal
answer> C
Right.
Score: 6/8 (75%).
2 to review.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 2

Q3. What is the chemical symbol for gold?
   you said A) Ag; it is C) Au.

Q7. On which continent is the Sahara desert?
   you said A) Asia; it is B) Africa.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 3

Q3. What is the chemical symbol for gold?
   A) Ag
   B) Gd
   C) Au
   D) Go
answer> C
Right.

Q7. On which continent is the Sahara desert?
   A) Asia
   B) Africa
   C) Australia
   D) South America
answer> C
Wrong - it is B) Africa.
Score: 1/2 (50%).
1 to review.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 2

Q7. On which continent is the Sahara desert?
   you said C) Australia; it is B) Africa.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 3

Q7. On which continent is the Sahara desert?
   A) Asia
   B) Africa
   C) Australia
   D) South America
answer> B
Right.
Score: 1/1 (100%).
No mistakes - well done.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 2
No mistakes to review.

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 4

-- scores --
  1) quiz     6/8   75%
  2) retake   1/2   50%
  3) retake   1/1  100%

== Quiz ==
1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P010 quiz: take the quiz, review the questions you missed, and retake only
# those until none are left. Every scored attempt goes into the score
# history, which lives while the program runs.
import bank
import quiz

def report(score, total, mistakes):
    ("Score: " + str(score) + "/" + str(total) + " (" + str(quiz.percent(score, total)) + "%).") ^0
    if len(mistakes) == 0:
        "No mistakes - well done." ^0
    else:
        (str(len(mistakes)) + " to review.") ^0

def review(mistakes):
    for m in mistakes:
        m[0] => q
        "" ^0
        ("Q" + str(q[0]) + ". " + q[1]) ^0
        ("   you said " + m[1] + ") " + bank.option_text(q, m[1]) + "; it is " + q[3] + ") " + bank.option_text(q, q[3]) + ".") ^0

# [kind, score, total] for every scored attempt
[] => history
# None until the quiz has been taken; then the last attempt's mistakes
None => mistakes
True => running
while running:
    "" ^0
    "== Quiz ==" ^0
    "1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit" ^0
    quiz.trim(input("choice> ")) => choice
    if choice == "1":
        quiz.attempt(bank.questions) => r
        if r == None:
            "Quiz stopped; not scored." ^0
        else:
            report(r[0], len(bank.questions), r[1])
            history + [["quiz", r[0], len(bank.questions)]] => history
            r[1] => mistakes
    elif choice == "2":
        if mistakes == None:
            "Take the quiz first." ^0
        elif len(mistakes) == 0:
            "No mistakes to review." ^0
        else:
            review(mistakes)
    elif choice == "3":
        if mistakes == None:
            "Take the quiz first." ^0
        elif len(mistakes) == 0:
            "No mistakes to retake." ^0
        else:
            [m[0] for m in mistakes] => again
            quiz.attempt(again) => r
            if r == None:
                "Retake stopped; not scored." ^0
            else:
                report(r[0], len(again), r[1])
                history + [["retake", r[0], len(again)]] => history
                r[1] => mistakes
    elif choice == "4":
        "" ^0
        "-- scores --" ^0
        if len(history) == 0:
            "  (no attempts yet)" ^0
        0 => n
        for h in history:
            n + 1 => n
            ("  " + str(n) + ") " + ("%-7s" % h[0]) + ("%5s" % (str(h[1]) + "/" + str(h[2]))) + ("%6s" % (str(quiz.percent(h[1], h[2])) + "%"))) ^0
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import bank
import quiz

def report(score, total, mistakes):
    print("Score: " + str(score) + "/" + str(total) + " (" + str(quiz.percent(score, total)) + "%).")
    if len(mistakes) == 0:
        print("No mistakes - well done.")
    else:
        print(str(len(mistakes)) + " to review.")

def review(mistakes):
    for m in mistakes:
        q = m[0]
        print("")
        print("Q" + str(q[0]) + ". " + q[1])
        print("   you said " + m[1] + ") " + bank.option_text(q, m[1]) + "; it is " + q[3] + ") " + bank.option_text(q, q[3]) + ".")

history = []
mistakes = None
running = True
while running:
    print("")
    print("== Quiz ==")
    print("1) take the quiz  2) review mistakes  3) retake mistakes  4) scores  5) quit")
    choice = quiz.trim(input("choice> "))
    if choice == "1":
        r = quiz.attempt(bank.questions)
        if r == None:
            print("Quiz stopped; not scored.")
        else:
            report(r[0], len(bank.questions), r[1])
            history = history + [["quiz", r[0], len(bank.questions)]]
            mistakes = r[1]
    elif choice == "2":
        if mistakes == None:
            print("Take the quiz first.")
        elif len(mistakes) == 0:
            print("No mistakes to review.")
        else:
            review(mistakes)
    elif choice == "3":
        if mistakes == None:
            print("Take the quiz first.")
        elif len(mistakes) == 0:
            print("No mistakes to retake.")
        else:
            again = [m[0] for m in mistakes]
            r = quiz.attempt(again)
            if r == None:
                print("Retake stopped; not scored.")
            else:
                report(r[0], len(again), r[1])
                history = history + [["retake", r[0], len(again)]]
                mistakes = r[1]
    elif choice == "4":
        print("")
        print("-- scores --")
        if len(history) == 0:
            print("  (no attempts yet)")
        n = 0
        for h in history:
            n = n + 1
            print("  " + str(n) + ") " + "%-7s" % h[0] + "%5s" % (str(h[1]) + "/" + str(h[2])) + "%6s" % (str(quiz.percent(h[1], h[2])) + "%"))
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### bank.eml

```eml
# P010 quiz - the question bank. A question is a list
# [number, text, [option A, B, C, D], correct letter].

[
    [1, "What gas do plants take in for photosynthesis?", ["Oxygen", "Carbon dioxide", "Nitrogen", "Helium"], "B"],
    [2, "How many bones are in the adult human body?", ["106", "206", "306", "406"], "B"],
    [3, "What is the chemical symbol for gold?", ["Ag", "Gd", "Au", "Go"], "C"],
    [4, "Which planet is closest to the Sun?", ["Venus", "Mars", "Mercury", "Earth"], "C"],
    [5, "What is the largest ocean on Earth?", ["Atlantic", "Indian", "Arctic", "Pacific"], "D"],
    [6, "Which river flows through Cairo?", ["Nile", "Amazon", "Danube", "Ganges"], "A"],
    [7, "On which continent is the Sahara desert?", ["Asia", "Africa", "Australia", "South America"], "B"],
    [8, "What is the capital of Canada?", ["Toronto", "Vancouver", "Ottawa", "Montreal"], "C"],
] => questions

"ABCD" => letters

def option_text(q, letter):
    # The text of option `letter` of question q.
    for i in [0:3]:
        if letters[i] == letter:
            return q[2][i]
    return ""
```

Python projection of bank.eml:

```python
questions = [[1, "What gas do plants take in for photosynthesis?", ["Oxygen", "Carbon dioxide", "Nitrogen", "Helium"], "B"], [2, "How many bones are in the adult human body?", ["106", "206", "306", "406"], "B"], [3, "What is the chemical symbol for gold?", ["Ag", "Gd", "Au", "Go"], "C"], [4, "Which planet is closest to the Sun?", ["Venus", "Mars", "Mercury", "Earth"], "C"], [5, "What is the largest ocean on Earth?", ["Atlantic", "Indian", "Arctic", "Pacific"], "D"], [6, "Which river flows through Cairo?", ["Nile", "Amazon", "Danube", "Ganges"], "A"], [7, "On which continent is the Sahara desert?", ["Asia", "Africa", "Australia", "South America"], "B"], [8, "What is the capital of Canada?", ["Toronto", "Vancouver", "Ottawa", "Montreal"], "C"]]
letters = "ABCD"

def option_text(q, letter):
    for i in range(0, 4):
        if letters[i] == letter:
            return q[2][i]
    return ""
```

### quiz.eml

```eml
# P010 quiz - asking questions and marking them. An attempt asks a list of
# questions in order and returns [score, mistakes], where each mistake is
# [question, the letter given]; typing q stops the attempt, and a stopped
# attempt is not scored.
import bank

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def answer_letter(typed):
    # "A".."D" for a typed a-d or A-D, "q" to stop, "" for anything else.
    if typed == "q" or typed == "Q":
        return "q"
    "abcd" => small
    if len(typed) == 1:
        for i in [0:3]:
            if typed == bank.letters[i] or typed == small[i]:
                return bank.letters[i]
    return ""

def ask(q):
    # Shows question q and returns the letter answered, or "q".
    "" ^0
    ("Q" + str(q[0]) + ". " + q[1]) ^0
    for i in [0:3]:
        ("   " + bank.letters[i] + ") " + q[2][i]) ^0
    while True:
        answer_letter(trim(input("answer> "))) => a
        if a != "":
            return a
        "Answer with A, B, C or D (or q to stop)." ^0

def attempt(qs):
    # [score, mistakes] for the questions qs, or None if the attempt stopped.
    0 => score
    [] => mistakes
    for q in qs:
        ask(q) => a
        if a == "q":
            return None
        if a == q[3]:
            score + 1 => score
            "Right." ^0
        else:
            ("Wrong - it is " + q[3] + ") " + bank.option_text(q, q[3]) + ".") ^0
            mistakes + [[q, a]] => mistakes
    return [score, mistakes]

def percent(score, total):
    # score / total as a whole percentage, rounded half up.
    return int((200 * score + total - (200 * score + total) % (2 * total)) / (2 * total))
```

Python projection of quiz.eml:

```python
import bank

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def answer_letter(typed):
    if typed == "q" or typed == "Q":
        return "q"
    small = "abcd"
    if len(typed) == 1:
        for i in range(0, 4):
            if typed == bank.letters[i] or typed == small[i]:
                return bank.letters[i]
    return ""

def ask(q):
    print("")
    print("Q" + str(q[0]) + ". " + q[1])
    for i in range(0, 4):
        print("   " + bank.letters[i] + ") " + q[2][i])
    while True:
        a = answer_letter(trim(input("answer> ")))
        if a != "":
            return a
        print("Answer with A, B, C or D (or q to stop).")

def attempt(qs):
    score = 0
    mistakes = []
    for q in qs:
        a = ask(q)
        if a == "q":
            return None
        if a == q[3]:
            score = score + 1
            print("Right.")
        else:
            print("Wrong - it is " + q[3] + ") " + bank.option_text(q, q[3]) + ".")
            mistakes = mistakes + [[q, a]]
    return [score, mistakes]

def percent(score, total):
    return int((200 * score + total - (200 * score + total) % (2 * total)) / (2 * total))
```

## README

# P010 - Quiz

A multiple-choice quiz of eight questions. Taking the quiz gives a score and
a percentage; the questions you got wrong can then be reviewed - your answer
next to the right one - and retaken on their own, again and again, until none
are left. Every scored attempt goes into a score history, which is kept while
the program runs.

- `main.eml` - the menu, the score report, the review of mistakes, and the
  score history
- `bank.eml` - the questions: each has a number, the question, four options
  A to D, and the right letter
- `quiz.eml` - asking a question, reading the answer, marking an attempt, and
  the percentage

An attempt asks its questions in order and keeps the ones answered wrongly,
with the answer given. A retake asks only the last attempt's mistakes, and its
own mistakes replace them, so a question answered right drops out. Typing `q`
at an answer stops the attempt: a stopped attempt is not scored, goes into no
history, and leaves the mistakes as they were. The percentage is worked out in
whole numbers and rounded half up (7/8 shows as 88%).

What is checked: an answer is one letter, A to D in either case, or `q`;
anything else asks the same question again.

Sessions: `sessions/basic.in` takes the quiz with two mistakes, reviews them,
retakes them (one right, one wrong again), reviews and retakes the one that is
left until there are none, and shows the three scores;
`sessions/bad-input.in` types an unknown choice, asks for a review, a retake
and the scores before any quiz, types a letter that is not an option, nothing,
two letters and a lower-case answer, stops a quiz (it is not scored), then
answers in lower case with one mistake, stops the retake, and retakes until
nothing is left to review or retake.

Built on the verified corpus case `simple-quiz-grader` (answers marked against
the right ones, with a score and a percentage).
