@jutge.org/toolkit 4.4.17 → 4.4.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/assets/problems/quizzes/demo-quiz.pbm/README.md +1 -0
  2. package/assets/problems/quizzes/demo-quiz.pbm/ca/award.png +0 -0
  3. package/assets/problems/quizzes/demo-quiz.pbm/ca/quiz.yml +1 -1
  4. package/assets/problems/quizzes/demo-quiz.pbm/ca/single-choice.yml +9 -1
  5. package/assets/problems/quizzes/demo-quiz.pbm/ca/some-drawing.svg +150 -0
  6. package/assets/problems/quizzes/demo-quiz.pbm/ca/some-image.png +0 -0
  7. package/assets/problems/quizzes/demo-quiz.pbm/en/quiz.yml +1 -1
  8. package/assets/problems/quizzes/demo-quiz.pbm/en/single-choice.yml +9 -1
  9. package/assets/problems/quizzes/demo-quiz.pbm/en/some-drawing.svg +150 -0
  10. package/assets/problems/quizzes/demo-quiz.pbm/en/some-image.png +0 -0
  11. package/assets/problems/quizzes/heroes-of-computation.pbm/README.md +11 -0
  12. package/assets/problems/quizzes/heroes-of-computation.pbm/en/handler.yml +1 -0
  13. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q01.yml +14 -0
  14. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q02.yml +14 -0
  15. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q03.yml +14 -0
  16. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q04.yml +15 -0
  17. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q05.yml +32 -0
  18. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q06.yml +12 -0
  19. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q07.yml +23 -0
  20. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q08.yml +14 -0
  21. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q09.yml +14 -0
  22. package/assets/problems/quizzes/heroes-of-computation.pbm/en/q10.yml +23 -0
  23. package/assets/problems/quizzes/heroes-of-computation.pbm/en/quiz.yml +48 -0
  24. package/assets/problems/quizzes/the-answer-is-42.pbm/en/quiz.yml +2 -0
  25. package/dist/index.js +469 -465
  26. package/docs/quiz-anatomy.md +74 -59
  27. package/package.json +2 -1
  28. package/toolkit/quiz.ts +30 -3
@@ -4,17 +4,13 @@ This document describes the anatomy of a **quiz problem** in [Jutge.org](https:/
4
4
 
5
5
  ## Terminology
6
6
 
7
- A **quiz** is a problem whose handler is set to `quiz`. It is made of a **quiz root** and a list of **questions**. The quiz root is defined in `quiz.yml` and holds the quiz title, statement, whether questions are shuffled, and the list of questions with their scores. Each **question** is defined in a separate YAML file (e.g. `single-choice.yml`) and has a **type**: SingleChoice, MultipleChoice, FillIn, Ordering, Matching, or OpenQuestion.
7
+ A **quiz** is a problem whose handler is set to `quiz`. It is made of a **quiz root** and a list of **questions**. The quiz root is defined in `quiz.yml` and holds the quiz title, author, statement, whether questions are shuffled, and the list of questions with their scores. Each **question** is defined in a separate YAML file (e.g. `single-choice.yml`) and has a `type`: SingleChoice, MultipleChoice, FillIn, Ordering, Matching, or OpenQuestion.
8
8
 
9
9
  Quiz content can be **localized**: the same quiz can have different `quiz.yml` and question files per language (e.g. under `en/` and `ca/`). The toolkit runs or lints the quiz for a given directory, so you typically run it from a language-specific subdirectory. Each language should live inside its own folder.
10
10
 
11
- **Variable substitution** allows question text and options to depend on values generated at run time. If a question file is named `example.yml`, the toolkit looks for `example.py` in the same directory. When present, it runs the Python script with a random **seed** and collects the script’s global variables. Those variables can be referenced in the question YAML with `$name` or `${name}`. This makes it possible to have different numbers, strings, or options for each run while keeping the same correct answer logic (e.g. “What is $a + $b?” with `a` and `b` random).
11
+ **Variable substitution** allows question text and options to depend on values generated at run time. If a question file is named `example.yml`, the toolkit looks for `example.py` in the same directory. When present, it runs the Python script with a random `seed` and collects the script’s global variables. Those variables can be referenced in the question YAML with `$name` or `${name}`. This makes it possible to have different numbers, strings, or options for each run while keeping the same correct answer logic (e.g. “What is $a + $b?” with `a` and `b` random).
12
12
 
13
- **Scoring**: Each question has a **score** between 0–100, and the total of all question scores listed in `quiz.yml` must add up to 100. Users earn points for each question based on question type and that question’s **partial_answer** setting. The **partial_answer** option is set per question in the question YAML (not in `quiz.yml`):
14
-
15
- - If `partial_answer` is set to `false` (default), users get full points for that question only when their answer is completely correct; any mistake gives zero points for that question.
16
-
17
- - If `partial_answer` is set to `true`, users can receive partial points for that question when the answer is partially correct (e.g. proportional to how many parts are right), and the response may still be marked as "correct" if at least one part is right.
13
+ **Scoring**: Each question has a `score` between 0–100, and the total of all question scores listed in `quiz.yml` must add up to 100. Users earn points for each question.
18
14
 
19
15
  ## Quiz structure
20
16
 
@@ -49,19 +45,22 @@ Many items are written in Markdown. See [Markdown documentation](https://www.mar
49
45
 
50
46
  The file `quiz.yml` defines the quiz root.
51
47
 
52
- - **title**: Title of the quiz.
53
- - **statement**: Short description or instructions shown for the quiz (Markdown).
54
- - **questions**: List of question entries. Each entry has:
55
- - **title**: Title of the question (e.g. for display in a table of contents).
56
- - **file**: Base name of the question file, without the `.yml` extension (e.g. `question` for `question.yml`).
57
- - **score**: Integer from 0 to 100. The sum of all `score` values in the list must be 100.
58
- - **shuffle** (optional): Whether to shuffle the order of questions when running the quiz. Defaults to `false`.
48
+ - `title`: Title of the quiz.
49
+ - `author`: Author of the quiz.
50
+ - `statement`: Short description or instructions shown for the quiz (Markdown).
51
+ - `questions`: List of question entries. Each entry has:
52
+ - `title`: Title of the question (e.g. for display in a table of contents).
53
+ - `file`: Base name of the question file, without the `.yml` extension (e.g. `question` for `question.yml`).
54
+ - `score`: Integer from 0 to 100. The sum of all `score` values in the list must be 100.
55
+ - `shuffle` (optional): Whether to shuffle the order of questions when running the quiz. Defaults to `false`.
59
56
 
60
57
  ### Example
61
58
 
62
59
  ```yaml
63
60
  title: Demo quiz
64
61
 
62
+ author: Jordi Petit
63
+
65
64
  statement: This quiz showcases the possibilities of the quiz problems at Jutge.org.
66
65
 
67
66
  shuffle: true
@@ -90,16 +89,24 @@ questions:
90
89
 
91
90
  ## Question types
92
91
 
93
- Each question is stored in a YAML file whose name matches the `file` field in `quiz.yml` (e.g. `question.yml`). The file must contain a **type** field that identifies the kind of question. All question types support an optional **hide_score** (default `false`) and an optional **partial_answer** (default `false`); see [Scoring](#terminology) above. Variable substitution applies to text fields and options when a corresponding `.py` file exists.
92
+ Each question is stored in a YAML file whose name matches the `file` field in `quiz.yml` (e.g. `question.yml`). The file must contain a `type` field that identifies the kind of question. Variable substitution applies to text fields and options when a corresponding `.py` file exists. All question types support an optional `hide_score` (default `false`) and an optional `partial_answer` (default `false`).
93
+
94
+ The `partial_answer` option is set per question in the question YAML:
95
+
96
+ - If `partial_answer` is set to `false` (default), users get full points for that question only when their answer is completely correct; any mistake gives zero points for that question.
97
+
98
+ - If `partial_answer` is set to `true`, users can receive partial points for that question when the answer is partially correct (e.g. proportional to how many parts are right), and the response may still be marked as "correct" if at least one part is right.
99
+
100
+ The `hide_score` option is set per question in the question YAML. If set to `true`, the question score is not shown to the user.
94
101
 
95
102
  ### SingleChoice
96
103
 
97
- One correct option among several. Exactly one choice must have `correct: true`. Choices can be shuffled (optional **shuffle**, default `true`). Each choice can have an optional **hint**. Duplicate choice text is not allowed.
104
+ One correct option among several. Exactly one choice must have `correct: true`. Choices can be shuffled (optional `shuffle`, default `true`). Each choice can have an optional `hint`. Duplicate choice text is not allowed.
98
105
 
99
- - **text**: Question text (supports `$var` and `${var}`).
100
- - **choices**: List of `{ text, correct?, hint? }`. One and only one choice must have `correct: true`.
101
- - **shuffle** (optional): Whether to shuffle choices. Defaults to `true`.
102
- - **partial_answer** (optional): Whether to award partial credit for this question. Defaults to `false`.
106
+ - `text`: Question text (supports `$var` and `${var}`).
107
+ - `choices`: List of `{ text, correct?, hint? }`. One and only one choice must have `correct: true`.
108
+ - `shuffle` (optional): Whether to shuffle choices. Defaults to `true`.
109
+ - `partial_answer` (optional): Whether to award partial credit for this question. Defaults to `false`.
103
110
 
104
111
  Example:
105
112
 
@@ -121,12 +128,12 @@ Variables `a`, `b`, `s1`, `s2`, `s3` would be produced by a `single-choice.py` s
121
128
 
122
129
  ### MultipleChoice
123
130
 
124
- Zero or more correct options. Multiple choices can have `correct: true`. Choices can be shuffled (optional **shuffle**, default `true`).
131
+ Zero or more correct options. Multiple choices can have `correct: true`. Choices can be shuffled (optional `shuffle`, default `true`).
125
132
 
126
- - **text**: Question text (supports variables).
127
- - **choices**: List of `{ text, correct?, hint? }`.
128
- - **shuffle** (optional): Whether to shuffle choices. Defaults to `true`.
129
- - **partial_answer** (optional): Whether to award partial credit for this question. Defaults to `false`.
133
+ - `text`: Question text (supports variables).
134
+ - `choices`: List of `{ text, correct?, hint? }`.
135
+ - `shuffle` (optional): Whether to shuffle choices. Defaults to `true`.
136
+ - `partial_answer` (optional): Whether to award partial credit for this question. Defaults to `false`.
130
137
 
131
138
  Example:
132
139
 
@@ -146,19 +153,19 @@ choices:
146
153
 
147
154
  ### FillIn
148
155
 
149
- One or more blanks in a text or code block. Each blank is identified by a placeholder (e.g. `S1`, `XXXX`) and has a correct answer and optional options (dropdown). If **options** are given, the correct answer must be one of them.
156
+ One or more blanks in a text or code block. Each blank is identified by a placeholder (e.g. `S1`, `XXXX`) and has a correct answer and optional options (dropdown). If `options` are given, the correct answer must be one of them.
150
157
 
151
- - **text**: Question or instructions (supports variables).
152
- - **context**: Text containing placeholders (e.g. `S1`, `S2`, `XXXX`). Placeholders are the keys in **items**.
153
- - **items**: Map from placeholder name to an item object:
154
- - **correct**: Correct answer (string).
155
- - **options** (optional): List of strings; if present, the blank is shown as a dropdown and **correct** must be in this list.
156
- - **maxlength** (optional): Max length for the answer. Defaults to 100.
157
- - **placeholder** (optional): Placeholder text in the input (e.g. `"?"`).
158
- - **ignorecase** (optional): Whether to ignore case when checking. Defaults to `true`.
159
- - **trim** (optional): Whether to trim spaces. Defaults to `true`.
160
- - **partial_answer** (optional): Whether this blank contributes to partial credit for the question. Defaults to `false`.
161
- - **partial_answer** (optional, at question level): Whether to award partial credit for this question. Defaults to `false`.
158
+ - `text`: Question or instructions (supports variables).
159
+ - `context`: Text containing placeholders (e.g. `S1`, `S2`, `XXXX`). Placeholders are the keys in `items`.
160
+ - `items`: Map from placeholder name to an item object:
161
+ - `correct`: Correct answer (string).
162
+ - `options` (optional): List of strings; if present, the blank is shown as a dropdown and `correct` must be in this list.
163
+ - `maxlength` (optional): Max length for the answer. Defaults to 100.
164
+ - `placeholder` (optional): Placeholder text in the input (e.g. `"?"`).
165
+ - `ignorecase` (optional): Whether to ignore case when checking. Defaults to `true`.
166
+ - `trim` (optional): Whether to trim spaces. Defaults to `true`.
167
+ - `partial_answer` (optional): Whether this blank contributes to partial credit for the question. Defaults to `false`.
168
+ - `partial_answer` (optional, at question level): Whether to award partial credit for this question. Defaults to `false`.
162
169
 
163
170
  Example with dropdowns:
164
171
 
@@ -217,13 +224,13 @@ items:
217
224
 
218
225
  ### Ordering
219
226
 
220
- User must order a list of items (e.g. chronological order). Items can be shown in shuffled order (optional **shuffle**, default `true`).
227
+ User must order a list of items (e.g. chronological order). Items can be shown in shuffled order (optional `shuffle`, default `true`).
221
228
 
222
- - **text**: Question text (supports variables).
223
- - **label**: Label for the list (e.g. “Programming language”).
224
- - **items**: List of strings in the **correct** order.
225
- - **shuffle** (optional): Whether to show items in random order. Defaults to `true`.
226
- - **partial_answer** (optional): Whether to award partial credit for this question. Defaults to `false`.
229
+ - `text`: Question text (supports variables).
230
+ - `label`: Label for the list (e.g. “Programming language”).
231
+ - `items`: List of strings in the correct order.
232
+ - `shuffle` (optional): Whether to show items in random order. Defaults to `true`.
233
+ - `partial_answer` (optional): Whether to award partial credit for this question. Defaults to `false`.
227
234
 
228
235
  Example:
229
236
 
@@ -245,14 +252,14 @@ items:
245
252
 
246
253
  ### Matching
247
254
 
248
- Two columns: user matches each left item with one right item. Left and right lists can be shuffled (optional **shuffle**, default `true`).
255
+ Two columns: user matches each left item with one right item. Left and right lists can be shuffled (optional `shuffle`, default `true`).
249
256
 
250
- - **text**: Question text (supports variables).
251
- - **labels**: Two strings, e.g. `["Countries", "Capitals"]`.
252
- - **left**: List of strings (e.g. countries).
253
- - **right**: List of strings (e.g. capitals), in the same order as **left** (left[i] matches right[i]).
254
- - **shuffle** (optional): Whether to shuffle left and right columns. Defaults to `true`.
255
- - **partial_answer** (optional): Whether to award partial credit for this question. Defaults to `false`.
257
+ - `text`: Question text (supports variables).
258
+ - `labels`: Two strings, e.g. `["Countries", "Capitals"]`.
259
+ - `left`: List of strings (e.g. countries).
260
+ - `right`: List of strings (e.g. capitals), in the same order as `left` (left[i] matches right[i]).
261
+ - `shuffle` (optional): Whether to shuffle left and right columns. Defaults to `true`.
262
+ - `partial_answer` (optional): Whether to award partial credit for this question. Defaults to `false`.
256
263
 
257
264
  Example:
258
265
 
@@ -284,9 +291,9 @@ right:
284
291
 
285
292
  Free-text answer with no automatic correction. Useful for open-ended or reflective answers.
286
293
 
287
- - **text**: Question text (supports variables).
288
- - **placeholder** (optional): Placeholder for the text area. Defaults to `""`. Supports variables.
289
- - **partial_answer** (optional): Whether to award partial credit for this question. Defaults to `false`.
294
+ - `text`: Question text (supports variables).
295
+ - `placeholder` (optional): Placeholder for the text area. Defaults to `""`. Supports variables.
296
+ - `partial_answer` (optional): Whether to award partial credit for this question. Defaults to `false`.
290
297
 
291
298
  Example:
292
299
 
@@ -304,7 +311,7 @@ The variable `name` can be set by an optional `open-ended.py` (or the same base
304
311
 
305
312
  If a question file is named `example.yml`, the toolkit looks for `example.py` in the same directory. When present:
306
313
 
307
- 1. The Python script is run with a given **seed** (passed as an argument by the toolkit) so that the run is reproducible.
314
+ 1. The Python script is run with a given `seed` (passed as an argument by the toolkit) so that the run is reproducible.
308
315
  2. The script’s global variables (that are JSON-serializable and whose names do not start with `__`) are collected.
309
316
  3. In the question YAML, any string field that supports substitution can use `$name` or `${name}` to be replaced by the value of `name`.
310
317
 
@@ -337,11 +344,11 @@ handler: quiz
337
344
 
338
345
  Other handler options (e.g. `std`, `graphic`) are for non-quiz problems. See [Problem anatomy — handler.yml](problem-anatomy.md#the-handleryml-file) for the full list of handler and option descriptions.
339
346
 
340
- ## Running and linting quizzes
347
+ ## Linting, running and playing quizzes
341
348
 
342
349
  From the toolkit CLI:
343
350
 
344
- - **Lint** a quiz (validate `quiz.yml` and all referenced question YAML files):
351
+ - `jtk quiz lint` — lint a quiz (validate `quiz.yml` and all referenced question YAML files):
345
352
 
346
353
  ```bash
347
354
  jtk quiz lint -d <directory>
@@ -349,12 +356,20 @@ From the toolkit CLI:
349
356
 
350
357
  Use the directory that contains `quiz.yml` (e.g. the `en` subdirectory).
351
358
 
352
- - **Run** a quiz (build questions, apply variables, output JSON or YAML):
359
+ - `jtk quiz run` — run a quiz (build questions, apply variables, output JSON or YAML):
360
+
353
361
  ```bash
354
362
  jtk quiz run -d <directory> [-s <seed>] [-f json|yaml]
355
363
  ```
364
+
356
365
  If no seed is provided, a random one is used. Running the quiz applies variable substitution and, if `shuffle` is true, shuffles question order (and, per question, choices or ordering/matching items when their `shuffle` is true).
357
366
 
367
+ - `jtk quiz play` — play a quiz in the terminal:
368
+ ```bash
369
+ jtk quiz play -d <directory> [-i <input>] [-o <output>] [-s <seed>]
370
+ ```
371
+ If no seed is provided, a random one is used. Playing the quiz applies variable substitution and, if `shuffle` is true, shuffles question order (and, per question, choices or ordering/matching items when their `shuffle` is true).
372
+
358
373
  ## Quick checklist
359
374
 
360
375
  - Use a problem folder with `handler: quiz` in `handler.yml`.
@@ -362,6 +377,6 @@ From the toolkit CLI:
362
377
  - Ensure every `file` in `quiz.yml` has a corresponding `file.yml` in the same directory.
363
378
  - Ensure question scores in `quiz.yml` sum to 100.
364
379
  - For SingleChoice, set exactly one choice with `correct: true`.
365
- - For FillIn with **options**, ensure **correct** is in the **options** list.
366
- - For Matching, ensure **left** and **right** have the same length and are in matching order.
380
+ - For FillIn with `options`, ensure `correct` is in the `options` list.
381
+ - For Matching, ensure `left` and `right` have the same length and are in matching order.
367
382
  - Use variable names in `$name` / `${name}` that are produced by the optional `file.py` script; the script is run with the toolkit’s seed for reproducibility.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jutge.org/toolkit",
3
3
  "description": "Toolkit to prepare problems for Jutge.org",
4
- "version": "4.4.17",
4
+ "version": "4.4.19",
5
5
  "homepage": "https://jutge.org",
6
6
  "author": {
7
7
  "name": "Jutge.org",
@@ -72,6 +72,7 @@
72
72
  "chalk": "^5.6.2",
73
73
  "chokidar": "^5.0.0",
74
74
  "cli-highlight": "^2.1.11",
75
+ "cli-table3": "^0.6.5",
75
76
  "commander": "^14.0.3",
76
77
  "dayjs": "^1.11.19",
77
78
  "env-paths": "^4.0.0",
package/toolkit/quiz.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Command } from '@commander-js/extra-typings'
2
2
  import { runQuiz, lintQuiz } from '../lib/quiz'
3
+ import {
4
+ loadQuizTestInput,
5
+ playQuiz,
6
+ writeQuizTestOutput,
7
+ } from '../lib/play-quiz'
3
8
  import tui from '../lib/tui'
4
9
  import { findRealDirectories } from '../lib/helpers'
5
10
  import { random } from 'radash'
@@ -30,12 +35,11 @@ quizCmd
30
35
  .command('run')
31
36
  .summary('Run a quiz problem')
32
37
  .description(
33
- `Run a quiz problem. This command is work-in-progress and may not work as expected yet.
38
+ `Run a quiz problem.
34
39
 
35
40
  This command will run the quiz problem and print the resulting object to stdout.
36
41
  A random seed will be generated if not provided.
37
- `,
38
- )
42
+ `)
39
43
 
40
44
  .option('-d, --directory <directory>', 'problem directory', '.')
41
45
  .option('-s, --seed <seed>', 'random seed')
@@ -50,3 +54,26 @@ A random seed will be generated if not provided.
50
54
  tui.yaml(object)
51
55
  }
52
56
  })
57
+
58
+
59
+ quizCmd
60
+ .command('play')
61
+ .summary('Play a quiz problem')
62
+ .description(
63
+ `Play an interactive quiz test. Questions are shown in sequence; you can review and change answers before submitting.
64
+ Input is a JSON file from \`quiz run\`; if --input is omitted, a run is generated with a random seed from the given directory.
65
+ If --output is given, the results (your answers, correct answers, and score per question) are written to that file.`)
66
+
67
+ .option('-i, --input <input>', 'input JSON file from quiz run')
68
+ .option('-o, --output <output>', 'output file for results')
69
+ .option('-d, --directory <directory>', 'problem directory (used when --input is not provided)', '.')
70
+ .option('-s, --seed <seed>', 'random seed (used when --input is not provided)')
71
+
72
+ .action(async ({ input, output, directory, seed }) => {
73
+ const seedValue = seed !== undefined ? parseInt(seed, 10) : undefined
74
+ const quizInput = await loadQuizTestInput(input, directory, seedValue)
75
+ const results = await playQuiz(quizInput)
76
+ if (output !== undefined) {
77
+ await writeQuizTestOutput(output, results)
78
+ }
79
+ })