@lernapps/app-templates 0.0.1 → 0.1.1
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.
- package/LICENSE +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-Dy359O1O.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-CxtCPAa5.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-CteIFN96.mjs +254 -0
- package/dist/content-DO6oEJxj.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-CU0n_unV.mjs +89 -0
- package/dist/engine-CArxqizW.mjs +136 -0
- package/dist/explainer-e2e.mjs +1311 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2132 -0
- package/dist/explainer.mjs +1246 -0
- package/dist/html-mffkHGHh.mjs +89 -0
- package/dist/image-CspJVfyQ.mjs +21 -0
- package/dist/interactive-e2e.mjs +140 -0
- package/dist/interactive-plugin.mjs +242 -0
- package/dist/interactive.mjs +75 -0
- package/dist/load-RosvaPZf.mjs +16976 -0
- package/dist/needs-BzEahJ6X.mjs +97 -0
- package/dist/pages-9RaaomoO.mjs +81 -0
- package/dist/questions-e2e.mjs +1 -0
- package/dist/questions-engine.mjs +3 -0
- package/dist/questions.mjs +6 -0
- package/dist/quiz-DnW-xtCh.mjs +70 -0
- package/dist/quiz-e2e.mjs +174 -0
- package/dist/quiz-plugin.mjs +266 -0
- package/dist/quiz.mjs +246 -0
- package/dist/render-D5kwkNt1.mjs +507 -0
- package/dist/render-DqHOJyHK.mjs +146 -0
- package/dist/render-GS1Htlu8.mjs +545 -0
- package/dist/result-D_mmCVwN.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-BDwL7Su3.mjs +1076 -0
- package/dist/validate-Bxy9NfB8.mjs +146 -0
- package/dist/vue-DO1m6q1R.mjs +40 -0
- package/dist/vue-code-BkJjW_I_.mjs +320 -0
- package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
- package/docs/arc42/02-constraints.arc42.md +228 -0
- package/docs/arc42/03-system-context.arc42.md +118 -0
- package/docs/arc42/04-solution-strategy.arc42.md +47 -0
- package/docs/arc42/05-building-blocks.arc42.md +307 -0
- package/docs/arc42/06-runtime-view.arc42.md +98 -0
- package/docs/arc42/07-deployment-view.arc42.md +160 -0
- package/docs/arc42/08-concepts.arc42.md +123 -0
- package/docs/arc42/09-decisions.arc42.md +246 -0
- package/docs/arc42/10-quality-requirements.arc42.md +155 -0
- package/docs/arc42/11-risks.arc42.md +113 -0
- package/docs/arc42/12-glossary.arc42.md +162 -0
- package/docs/arc42/architecture-evidence.md +395 -0
- package/explainer/.arc42ignore +3 -0
- package/explainer/README.md +54 -0
- package/explainer/_gitignore +5 -0
- package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
- package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
- package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
- package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
- package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
- package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
- package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
- package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
- package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
- package/explainer/e2e/explainer.e2e.ts +3 -0
- package/explainer/index.html +11 -0
- package/explainer/package.json +18 -0
- package/explainer/playwright.config.ts +1 -0
- package/explainer/src/app.yaml +20 -0
- package/explainer/src/components/ProzentFeld.test.ts +30 -0
- package/explainer/src/components/ProzentFeld.ts +121 -0
- package/explainer/src/components/ProzentFeld.vue +84 -0
- package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
- package/explainer/src/main.ts +4 -0
- package/explainer/src/prozent/chapter.yaml +18 -0
- package/explainer/src/prozent/grundwert.yaml +115 -0
- package/explainer/src/prozent/prozentsatz.yaml +99 -0
- package/explainer/src/prozent/prozentwert.yaml +119 -0
- package/explainer/tsconfig.json +1 -0
- package/explainer/vite.config.ts +4 -0
- package/interactive/.arc42ignore +5 -0
- package/interactive/README.md +28 -0
- package/interactive/_gitignore +4 -0
- package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
- package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
- package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
- package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
- package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
- package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
- package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
- package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
- package/interactive/e2e/interactive.e2e.ts +3 -0
- package/interactive/index.html +11 -0
- package/interactive/package.json +18 -0
- package/interactive/playwright.config.ts +1 -0
- package/interactive/src/app.yaml +28 -0
- package/interactive/src/core/index.ts +101 -0
- package/interactive/src/data/lessons.json +4 -0
- package/interactive/src/env.d.ts +6 -0
- package/interactive/src/main.ts +6 -0
- package/interactive/src/messages/de.json +12 -0
- package/interactive/src/playthroughs/grundreihe.yaml +33 -0
- package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
- package/interactive/src/ui/App.vue +85 -0
- package/interactive/tsconfig.json +1 -0
- package/interactive/vite.config.ts +4 -0
- package/package.json +143 -4
- package/quiz/README.md +31 -0
- package/quiz/_gitignore +5 -0
- package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
- package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
- package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
- package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
- package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
- package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
- package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
- package/quiz/e2e/quiz.e2e.ts +3 -0
- package/quiz/index.html +11 -0
- package/quiz/package.json +18 -0
- package/quiz/playwright.config.ts +1 -0
- package/quiz/src/images/berge-ordnen.jpg +0 -0
- package/quiz/src/images/everest-hoehe.jpg +0 -0
- package/quiz/src/images/galapagos.jpg +0 -0
- package/quiz/src/images/grand-canyon.jpg +0 -0
- package/quiz/src/images/totes-meer.jpg +0 -0
- package/quiz/src/images/uluru.jpg +0 -0
- package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
- package/quiz/src/main.ts +4 -0
- package/quiz/src/quiz.json +312 -0
- package/quiz/tsconfig.json +1 -0
- package/quiz/vite.config.ts +4 -0
- package/runtime/explainer/assessment-client.ts +478 -0
- package/runtime/explainer/assessment-page.ts +448 -0
- package/runtime/explainer/assessment.ts +393 -0
- package/runtime/explainer/check-page.ts +520 -0
- package/runtime/explainer/components/exercise.ts +409 -0
- package/runtime/explainer/components/props.ts +230 -0
- package/runtime/explainer/components/read.ts +428 -0
- package/runtime/explainer/content.ts +277 -0
- package/runtime/explainer/e2e.ts +1488 -0
- package/runtime/explainer/exercises.ts +326 -0
- package/runtime/explainer/explainer.css +1283 -0
- package/runtime/explainer/explainer.ts +838 -0
- package/runtime/explainer/explainer.v1.schema.json +1165 -0
- package/runtime/explainer/expression.ts +407 -0
- package/runtime/explainer/fraction.ts +107 -0
- package/runtime/explainer/generated/comparator.ts +31 -0
- package/runtime/explainer/generated/question.ts +165 -0
- package/runtime/explainer/generated/soundness.ts +280 -0
- package/runtime/explainer/generated/texts.ts +257 -0
- package/runtime/explainer/generated/types.ts +103 -0
- package/runtime/explainer/generated/values.ts +136 -0
- package/runtime/explainer/islands.ts +281 -0
- package/runtime/explainer/load.ts +457 -0
- package/runtime/explainer/messages/de.json +200 -0
- package/runtime/explainer/messages/en.json +200 -0
- package/runtime/explainer/messages.ts +13 -0
- package/runtime/explainer/needs.ts +122 -0
- package/runtime/explainer/open-task.ts +235 -0
- package/runtime/explainer/outline.ts +306 -0
- package/runtime/explainer/plugin.ts +488 -0
- package/runtime/explainer/position.ts +62 -0
- package/runtime/explainer/prompt.ts +75 -0
- package/runtime/explainer/render.ts +686 -0
- package/runtime/explainer/result.ts +238 -0
- package/runtime/explainer/routes.ts +119 -0
- package/runtime/explainer/schema.ts +140 -0
- package/runtime/explainer/scope.ts +87 -0
- package/runtime/explainer/stuck.ts +57 -0
- package/runtime/explainer/tasks.ts +109 -0
- package/runtime/explainer/tutor.ts +469 -0
- package/runtime/explainer/tutorpage.ts +669 -0
- package/runtime/explainer/video.ts +123 -0
- package/runtime/interactive/contract.ts +121 -0
- package/runtime/interactive/e2e.ts +190 -0
- package/runtime/interactive/interactive.css +82 -0
- package/runtime/interactive/interactive.ts +115 -0
- package/runtime/interactive/interactive.v1.schema.json +146 -0
- package/runtime/interactive/load.ts +188 -0
- package/runtime/interactive/messages/de.json +14 -0
- package/runtime/interactive/messages/en.json +14 -0
- package/runtime/interactive/messages.ts +11 -0
- package/runtime/interactive/playthrough.v1.schema.json +143 -0
- package/runtime/interactive/plugin.ts +266 -0
- package/runtime/interactive/render.ts +126 -0
- package/runtime/quiz/e2e.ts +233 -0
- package/runtime/quiz/engine.ts +242 -0
- package/runtime/quiz/messages/de.json +23 -0
- package/runtime/quiz/messages/en.json +23 -0
- package/runtime/quiz/plugin.ts +150 -0
- package/runtime/quiz/quiz.css +154 -0
- package/runtime/quiz/quiz.ts +56 -0
- package/runtime/quiz/quiz.v1.schema.json +72 -0
- package/runtime/quiz/render.ts +173 -0
- package/runtime/quiz/validate.ts +108 -0
- package/runtime/shared/appname.ts +16 -0
- package/runtime/shared/blocks/block.ts +162 -0
- package/runtime/shared/blocks/blocks.css +190 -0
- package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
- package/runtime/shared/blocks/code.ts +26 -0
- package/runtime/shared/blocks/figures.ts +49 -0
- package/runtime/shared/blocks/image.ts +25 -0
- package/runtime/shared/blocks/index.ts +13 -0
- package/runtime/shared/blocks/messages/de.json +4 -0
- package/runtime/shared/blocks/messages/en.json +4 -0
- package/runtime/shared/blocks/render.ts +134 -0
- package/runtime/shared/blocks/validate.ts +165 -0
- package/runtime/shared/content.ts +197 -0
- package/runtime/shared/examples.ts +20 -0
- package/runtime/shared/html.ts +10 -0
- package/runtime/shared/languages.ts +94 -0
- package/runtime/shared/page.css +101 -0
- package/runtime/shared/pages.ts +114 -0
- package/runtime/shared/questions/e2e.ts +115 -0
- package/runtime/shared/questions/engine.ts +209 -0
- package/runtime/shared/questions/index.ts +39 -0
- package/runtime/shared/questions/messages/de.json +26 -0
- package/runtime/shared/questions/messages/en.json +26 -0
- package/runtime/shared/questions/question.ts +433 -0
- package/runtime/shared/questions/question.v1.schema.json +346 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +255 -0
- package/runtime/shared/questions/validate.ts +518 -0
- package/runtime/shared/schema.ts +44 -0
- package/runtime/shared/text/check.ts +68 -0
- package/runtime/shared/text/formula.ts +69 -0
- package/runtime/shared/text/markdown.ts +185 -0
- package/runtime/shared/text/speech.ts +39 -0
- package/runtime/shared/text/text.v1.schema.json +34 -0
- package/runtime/shared/vue-code.ts +395 -0
- package/runtime/shared/vue.ts +38 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Architecture Decisions
|
|
2
|
+
|
|
3
|
+
The interactive archetype's decisions, besides the generic ones; decisions the runtime made when it was built
|
|
4
|
+
(commit c5e7285, 2026-10-16) are accepted because they are in force. An app adds its own below these (for example
|
|
5
|
+
canvas or DOM).
|
|
6
|
+
|
|
7
|
+
## One Archetype for All Kinds
|
|
8
|
+
|
|
9
|
+
Typing game, Kniffel trainer and proofreading app run differently (frame loop, turns, tasks) but share frame and
|
|
10
|
+
rules. One archetype carries them all, and its skill gives the principles they share. Rejected: one archetype per kind. An
|
|
11
|
+
app that needs explanation pages is an explainer with a component instead.
|
|
12
|
+
|
|
13
|
+
```arc42
|
|
14
|
+
:::decision
|
|
15
|
+
id: dec-one-archetype
|
|
16
|
+
title: One interactive archetype for every kind of app
|
|
17
|
+
status: accepted
|
|
18
|
+
date: 2026-10-16
|
|
19
|
+
addresses: con-runtime-unchanged, qg-understandable
|
|
20
|
+
:::
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## The Runtime Owns the State and the Clock
|
|
24
|
+
|
|
25
|
+
The core is pure TypeScript without DOM; the shell holds its state, starts it, steps it and calls `tick(state, 20)`
|
|
26
|
+
every 20 ms when the core has `tick`, and the components only show the state. So the core plays the same in Node and
|
|
27
|
+
in the browser, and a real-time app needs no loop of its own in the core. Rejected: state kept in components, a
|
|
28
|
+
variable frame time. The fixed tick is the runtime's choice; the sketch names only `tick(state, dt)`.
|
|
29
|
+
|
|
30
|
+
```arc42
|
|
31
|
+
:::decision
|
|
32
|
+
id: dec-runtime-owns-state
|
|
33
|
+
title: The runtime holds the core's state and ticks it every 20 ms
|
|
34
|
+
status: accepted
|
|
35
|
+
date: 2026-10-16
|
|
36
|
+
addresses: qg-checkable, con-runtime-unchanged
|
|
37
|
+
:::
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Keys Are the Core's Input
|
|
41
|
+
|
|
42
|
+
Superseded by `dec-typed-input`. The sketch named `step(state, input)`; the runtime made the input a key as the browser
|
|
43
|
+
names it (`KeyboardEvent.key`), pointer input reaching the core as a key the root component emits as `input`, and a
|
|
44
|
+
key the core ignores keeping its default (Tab moves the focus).
|
|
45
|
+
|
|
46
|
+
```arc42
|
|
47
|
+
:::decision
|
|
48
|
+
id: dec-keys-as-input
|
|
49
|
+
title: Keys as the core's only input
|
|
50
|
+
status: superseded
|
|
51
|
+
date: 2026-10-16
|
|
52
|
+
addresses: qg-checkable, qg-accessibility
|
|
53
|
+
:::
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## A Typed Input for the Core
|
|
57
|
+
|
|
58
|
+
Interactive apps will use more than keys and mouse: touch, gestures and other input later. So the core's
|
|
59
|
+
`step(state, input)` takes a typed input (a key, a pointer or touch action, …), extensible by new kinds; playthroughs
|
|
60
|
+
record such inputs, and the components translate any device input into inputs, so that the build and the browser play
|
|
61
|
+
the same steps. Rejected: keys as the only input, with pointer and touch disguised as keys. Built: a key input and an
|
|
62
|
+
action input; a core returns the state unchanged for a kind it does not know.
|
|
63
|
+
|
|
64
|
+
```arc42
|
|
65
|
+
:::decision
|
|
66
|
+
id: dec-typed-input
|
|
67
|
+
title: The core's input is typed and extensible (key, pointer, touch, …)
|
|
68
|
+
status: accepted
|
|
69
|
+
date: 2026-10-16
|
|
70
|
+
supersedes: dec-keys-as-input
|
|
71
|
+
addresses: qg-checkable, qg-accessibility
|
|
72
|
+
:::
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## start(seed, route)
|
|
76
|
+
|
|
77
|
+
The sketch names `start(seed)`; the runtime also passes the route of the address, so that each mode or level has an
|
|
78
|
+
address an entry's topics can point to and the core starts there. The seed comes from `?seed=<n>`, else at random, so
|
|
79
|
+
a round can be replayed and tested at its address.
|
|
80
|
+
|
|
81
|
+
```arc42
|
|
82
|
+
:::decision
|
|
83
|
+
id: dec-start-seed-route
|
|
84
|
+
title: The core starts from a seed and a route of the address
|
|
85
|
+
status: accepted
|
|
86
|
+
date: 2026-10-16
|
|
87
|
+
addresses: qg-checkable
|
|
88
|
+
:::
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Playthroughs as Data
|
|
92
|
+
|
|
93
|
+
A playthrough is a seed, a route and steps of inputs with the state or announcement expected after them, written as
|
|
94
|
+
YAML under `src/playthroughs/`. The build plays each against the core twice and the browser plays the same inputs, so
|
|
95
|
+
determinism and behaviour are checked without test code per app. A step can let n ticks pass; the build calls `tick`
|
|
96
|
+
n times, and the browser steps the shell's tick through the tests' clock (`if-test-clock`) rather than Playwright's
|
|
97
|
+
fake clock, which would also freeze the announcement's timer. The core's property tests over fixed seeds are not
|
|
98
|
+
built; until then every playthrough played twice checks determinism. Rejected: free test code as the only behaviour
|
|
99
|
+
check.
|
|
100
|
+
|
|
101
|
+
```arc42
|
|
102
|
+
:::decision
|
|
103
|
+
id: dec-playthroughs-as-data
|
|
104
|
+
title: Playthroughs as data, played in the build and in the browser
|
|
105
|
+
status: accepted
|
|
106
|
+
date: 2026-10-16
|
|
107
|
+
addresses: qg-checkable, risk-no-core-property-tests
|
|
108
|
+
:::
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Libraries in the Dependencies
|
|
112
|
+
|
|
113
|
+
An interactive app may use libraries: its components and modules may import the packages listed in the dependencies
|
|
114
|
+
of its package.json, which the build bundles; their licences are checked by `lernapps check` (`dependency-licence`).
|
|
115
|
+
An import from a URL, network APIs, `eval`, `innerHTML` and `v-html` stay forbidden. The explainer's components keep
|
|
116
|
+
their narrower scope. Rejected: only Vue, the runtime and the tooling helpers, which would make every app write again
|
|
117
|
+
what a library already does well.
|
|
118
|
+
|
|
119
|
+
```arc42
|
|
120
|
+
:::decision
|
|
121
|
+
id: dec-libraries
|
|
122
|
+
title: An interactive app may import the libraries in its dependencies
|
|
123
|
+
status: accepted
|
|
124
|
+
date: 2026-10-16
|
|
125
|
+
addresses: qg-privacy
|
|
126
|
+
:::
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
<!-- app: begin -->
|
|
130
|
+
<!-- app: end -->
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Quality Requirements
|
|
2
|
+
|
|
3
|
+
The interactive archetype's scenarios, besides the generic ones.
|
|
4
|
+
|
|
5
|
+
<!-- at the top of the generic chapter -->
|
|
6
|
+
```arc42
|
|
7
|
+
:::ignore W006 two high-priority goals is the owner's decision (2026-10-16): privacy and checkable high; the interactive archetype adds no goal of its own
|
|
8
|
+
:::
|
|
9
|
+
```
|
|
10
|
+
<!-- end -->
|
|
11
|
+
|
|
12
|
+
## The same inputs play the same twice
|
|
13
|
+
|
|
14
|
+
At build, every playthrough is played against the core twice, with only the ticks its steps name; a core that draws
|
|
15
|
+
from `Math.random` or
|
|
16
|
+
the clock gives different states.
|
|
17
|
+
|
|
18
|
+
```arc42
|
|
19
|
+
:::quality-scenario
|
|
20
|
+
id: qs-deterministic-core
|
|
21
|
+
title: The same inputs give the same states
|
|
22
|
+
quality: qg-checkable
|
|
23
|
+
stimulus: The build plays a playthrough's seed, route and inputs against the core twice
|
|
24
|
+
response: The build fails with one line naming the first step whose state differs, or whose state differs from the expected one
|
|
25
|
+
metric: Both plays give identical states at every step and match every expected state, else the build fails
|
|
26
|
+
:::
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Playthroughs in the browser
|
|
30
|
+
|
|
31
|
+
`interactiveTests()` plays every playthrough at its address in the browser, at desktop width and at 360 px.
|
|
32
|
+
|
|
33
|
+
```arc42
|
|
34
|
+
:::quality-scenario
|
|
35
|
+
id: qs-playthrough-in-browser
|
|
36
|
+
title: Playthroughs play the same in the browser
|
|
37
|
+
quality: qg-checkable
|
|
38
|
+
stimulus: lernapps check --pre-push runs the e2e tests on the built app
|
|
39
|
+
response: Each playthrough opens app/?seed=<seed>#<route> on the test clock, gives its inputs and ticks and compares the state and the polite announcement after each step
|
|
40
|
+
metric: Every expected state and announcement matches, with no console error, at desktop width and 360 px
|
|
41
|
+
:::
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
<!-- app: begin -->
|
|
45
|
+
<!-- app: end -->
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Risks and Technical Debt
|
|
2
|
+
|
|
3
|
+
The interactive archetype's risks, besides the generic ones, highest first.
|
|
4
|
+
|
|
5
|
+
## Core Property Tests Not Built
|
|
6
|
+
|
|
7
|
+
The sketch has the build run the core's property tests over fixed seeds; the build plays only the playthroughs, so a
|
|
8
|
+
core is checked for the seeds its playthroughs name and no others.
|
|
9
|
+
|
|
10
|
+
```arc42
|
|
11
|
+
:::risk
|
|
12
|
+
id: risk-no-core-property-tests
|
|
13
|
+
title: Core property tests over fixed seeds not built
|
|
14
|
+
severity: low
|
|
15
|
+
mitigation: Planned; until then the build checks determinism with every playthrough played twice.
|
|
16
|
+
:::
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
<!-- app: begin -->
|
|
20
|
+
<!-- app: end -->
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Glossary
|
|
2
|
+
|
|
3
|
+
The interactive archetype's terms, besides the generic ones.
|
|
4
|
+
|
|
5
|
+
## Core
|
|
6
|
+
|
|
7
|
+
The app's logic in `src/core/index.ts`. Called "core" rather than "engine" or "model" because it holds no state of its
|
|
8
|
+
own: the shell keeps it.
|
|
9
|
+
|
|
10
|
+
```arc42
|
|
11
|
+
:::glossary-term
|
|
12
|
+
id: term-core
|
|
13
|
+
title: Core
|
|
14
|
+
definition: The app's logic, pure TypeScript without DOM, exporting start(seed, route), step(state, input) and optionally tick(state, dt); deterministic by its seed, its state plain JSON.
|
|
15
|
+
:::
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## App Page
|
|
19
|
+
|
|
20
|
+
The one page on which the app runs; without JavaScript it shows only a note and a link back to the start page.
|
|
21
|
+
|
|
22
|
+
```arc42
|
|
23
|
+
:::glossary-term
|
|
24
|
+
id: term-app-page
|
|
25
|
+
title: App page
|
|
26
|
+
definition: The page that loads the app's client module, into whose element the shell mounts the app.
|
|
27
|
+
:::
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Route
|
|
31
|
+
|
|
32
|
+
A mode or level of the app, declared in `src/app.yaml`. The core receives the route's id in `start`.
|
|
33
|
+
|
|
34
|
+
```arc42
|
|
35
|
+
:::glossary-term
|
|
36
|
+
id: term-route
|
|
37
|
+
title: Route
|
|
38
|
+
definition: A mode or level of the app with its own address on the app page (app/#<id>); the first route when the address names none.
|
|
39
|
+
:::
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Tick
|
|
43
|
+
|
|
44
|
+
The runtime's constant `TICK` is 20. A learner's browser ticks in real time; the build and the end-to-end tests tick
|
|
45
|
+
only as a playthrough step's `ticks` say.
|
|
46
|
+
|
|
47
|
+
```arc42
|
|
48
|
+
:::glossary-term
|
|
49
|
+
id: term-tick
|
|
50
|
+
title: Tick
|
|
51
|
+
definition: A call of tick(state, 20) that the shell makes every 20 ms when the core has tick, so the state moves with time.
|
|
52
|
+
:::
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Playthrough
|
|
56
|
+
|
|
57
|
+
A YAML file in `src/playthroughs/`. Not a free-form test: it holds only data the runtime plays.
|
|
58
|
+
|
|
59
|
+
```arc42
|
|
60
|
+
:::glossary-term
|
|
61
|
+
id: term-playthrough
|
|
62
|
+
title: Playthrough
|
|
63
|
+
definition: A seed, a route and a list of steps, each inputs (keys as their short form) and ticks followed by the state (a part of it) or the announcement expected after them; played by the build against the core and by the browser test.
|
|
64
|
+
:::
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Input
|
|
68
|
+
|
|
69
|
+
What the core's `step` takes: a key input or an action input (`dec-typed-input`).
|
|
70
|
+
|
|
71
|
+
```arc42
|
|
72
|
+
:::glossary-term
|
|
73
|
+
id: term-input
|
|
74
|
+
title: Input
|
|
75
|
+
definition: A typed, extensible value the core's step takes: a key as the browser names it, a pointer or touch action, and other kinds later; the components translate any device input into inputs, and playthroughs record them.
|
|
76
|
+
:::
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
<!-- app: begin -->
|
|
80
|
+
<!-- app: end -->
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="de">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<script type="module" src="/src/main.ts"></script>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<!-- interactive -->
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "interactive",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vp dev",
|
|
7
|
+
"build": "vp build",
|
|
8
|
+
"check": "lernapps check"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@lernapps/app-templates": "file:..",
|
|
12
|
+
"@lernapps/tooling": "^0.1.2",
|
|
13
|
+
"vite-plus": "1.4.0"
|
|
14
|
+
},
|
|
15
|
+
"overrides": {
|
|
16
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "@lernapps/tooling/playwright";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../node_modules/@lernapps/app-templates/runtime/interactive/interactive.v1.schema.json
|
|
2
|
+
title: Buchstaben tippen
|
|
3
|
+
description: |
|
|
4
|
+
Ein Buchstabe erscheint, du tippst ihn auf der Tastatur. Für jeden hast du fünf Sekunden.
|
|
5
|
+
Zum Üben der Grundreihe und der oberen Reihe, ab Klasse 3.
|
|
6
|
+
howto: |
|
|
7
|
+
- Tippe den Buchstaben, der groß in der Mitte steht.
|
|
8
|
+
- Richtig getippt gibt einen Punkt, und der nächste Buchstabe kommt.
|
|
9
|
+
- Ein falscher Buchstabe oder zu langes Warten zählt als Fehler.
|
|
10
|
+
- Nach zehn Buchstaben ist die Runde vorbei. Mit Enter spielst du die nächste.
|
|
11
|
+
competencies:
|
|
12
|
+
- Ich kann die Buchstaben der Grundreihe auf der Tastatur finden.
|
|
13
|
+
- Ich kann die Buchstaben der oberen Reihe tippen.
|
|
14
|
+
languages: [de]
|
|
15
|
+
routes:
|
|
16
|
+
- id: grundreihe
|
|
17
|
+
title: Grundreihe
|
|
18
|
+
description: a s d f j k l
|
|
19
|
+
- id: obere-reihe
|
|
20
|
+
title: Obere Reihe
|
|
21
|
+
description: q w e r t z u i o p
|
|
22
|
+
sections:
|
|
23
|
+
- id: zehn-finger
|
|
24
|
+
title: Warum die Grundreihe?
|
|
25
|
+
text: |
|
|
26
|
+
Auf der Grundreihe liegen deine Finger, wenn du nicht tippst: links auf a, s, d, f, rechts auf j, k, l.
|
|
27
|
+
Von dort erreicht jeder Finger seine Tasten. Wer die Grundreihe sicher findet, muss beim Tippen nicht mehr auf
|
|
28
|
+
die Tastatur schauen.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The game's logic: a round of letters from the lesson of the route, drawn from the seed; each letter typed right
|
|
2
|
+
// scores, a wrong key or a letter not typed in time is a miss; after the round, Enter or the action "again" (the button
|
|
3
|
+
// of the component) starts the next. Pure TypeScript without DOM; the runtime keeps the state and calls start, step
|
|
4
|
+
// and tick (@lernapps/app-templates/interactive).
|
|
5
|
+
import type { Input } from "@lernapps/app-templates/interactive";
|
|
6
|
+
import lessons from "../data/lessons.json" with { type: "json" };
|
|
7
|
+
|
|
8
|
+
/** A lesson of src/data/lessons.json: the letters it draws from, how many a round has, the seconds for each. */
|
|
9
|
+
export interface Lesson {
|
|
10
|
+
letters: string;
|
|
11
|
+
count: number;
|
|
12
|
+
seconds: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface State {
|
|
16
|
+
seed: number;
|
|
17
|
+
route: string;
|
|
18
|
+
/** The letters of the round, in order. */
|
|
19
|
+
letters: string;
|
|
20
|
+
/** The position of the letter to type now. */
|
|
21
|
+
index: number;
|
|
22
|
+
score: number;
|
|
23
|
+
misses: number;
|
|
24
|
+
/** Milliseconds left for the letter to type now. */
|
|
25
|
+
left: number;
|
|
26
|
+
/** What the last change was. */
|
|
27
|
+
last: "start" | "hit" | "miss" | "late";
|
|
28
|
+
done: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const LESSONS: Readonly<Record<string, Lesson>> = lessons;
|
|
32
|
+
|
|
33
|
+
/** The lesson of a route. */
|
|
34
|
+
export function lessonOf(route: string): Lesson {
|
|
35
|
+
const lesson = LESSONS[route];
|
|
36
|
+
if (lesson === undefined) throw new Error(`no lesson ${route} in src/data/lessons.json`);
|
|
37
|
+
return lesson;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A random number from 0 to 1 for each call, the same sequence for the same seed (mulberry32). */
|
|
41
|
+
function random(seed: number): () => number {
|
|
42
|
+
let value = seed >>> 0;
|
|
43
|
+
return () => {
|
|
44
|
+
value = (value + 0x6d2b79f5) >>> 0;
|
|
45
|
+
let mixed = Math.imul(value ^ (value >>> 15), 1 | value);
|
|
46
|
+
mixed = (mixed + Math.imul(mixed ^ (mixed >>> 7), 61 | mixed)) ^ mixed;
|
|
47
|
+
return ((mixed ^ (mixed >>> 14)) >>> 0) / 4_294_967_296;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function start(seed: number, route: string): State {
|
|
52
|
+
const lesson = lessonOf(route);
|
|
53
|
+
const next = random(seed);
|
|
54
|
+
const letters = Array.from(
|
|
55
|
+
{ length: lesson.count },
|
|
56
|
+
() => lesson.letters[Math.floor(next() * lesson.letters.length)] ?? "",
|
|
57
|
+
).join("");
|
|
58
|
+
return {
|
|
59
|
+
seed,
|
|
60
|
+
route,
|
|
61
|
+
letters,
|
|
62
|
+
index: 0,
|
|
63
|
+
score: 0,
|
|
64
|
+
misses: 0,
|
|
65
|
+
left: lesson.seconds * 1000,
|
|
66
|
+
last: "start",
|
|
67
|
+
done: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The state with the next letter to type, after a hit or a late one. */
|
|
72
|
+
function advance(state: State, change: Partial<State>): State {
|
|
73
|
+
const index = state.index + 1;
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
...change,
|
|
77
|
+
index,
|
|
78
|
+
left: lessonOf(state.route).seconds * 1000,
|
|
79
|
+
done: index >= state.letters.length,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether `input` asks for the next round: Enter, or the component's action "again". */
|
|
84
|
+
const again = (input: Input): boolean =>
|
|
85
|
+
(input.kind === "key" && input.key === "Enter") || (input.kind === "action" && input.name === "again");
|
|
86
|
+
|
|
87
|
+
export function step(state: State, input: Input): State {
|
|
88
|
+
if (state.done) return again(input) ? start(state.seed + 1, state.route) : state;
|
|
89
|
+
// only a typed letter counts: no Shift, Tab or other named key, no action
|
|
90
|
+
if (input.kind !== "key" || input.key.length !== 1) return state;
|
|
91
|
+
const key = input.key;
|
|
92
|
+
if (key.toLowerCase() === state.letters[state.index]) return advance(state, { score: state.score + 1, last: "hit" });
|
|
93
|
+
return { ...state, misses: state.misses + 1, last: "miss" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function tick(state: State, dt: number): State {
|
|
97
|
+
if (state.done) return state;
|
|
98
|
+
const left = state.left - dt;
|
|
99
|
+
if (left > 0) return { ...state, left };
|
|
100
|
+
return advance(state, { misses: state.misses + 1, last: "late" });
|
|
101
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"game": "Spiel",
|
|
3
|
+
"type": "Tippe {letter}.",
|
|
4
|
+
"hit": "Richtig. Tippe {letter}.",
|
|
5
|
+
"miss": "Falsch. Tippe {letter}.",
|
|
6
|
+
"late": "Zu spät. Tippe {letter}.",
|
|
7
|
+
"start": "Neue Runde. Tippe {letter}.",
|
|
8
|
+
"score": "Punkte: {score} von {total}",
|
|
9
|
+
"misses": "Fehler: {misses}",
|
|
10
|
+
"done": "Fertig: {score} von {total} richtig. Mit Enter spielst du noch eine Runde.",
|
|
11
|
+
"again": "Noch eine Runde"
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/interactive/playthrough.v1.schema.json
|
|
2
|
+
# A round of the Grundreihe with seed 1, whose letters are j a f l l s j k d l: a hit, a wrong key, two seconds that
|
|
3
|
+
# pass, three more that make the letter late, a capital letter that counts, the rest of the round; the button for the
|
|
4
|
+
# next round (the action "again"), that round typed through, and Enter for the one after it.
|
|
5
|
+
route: grundreihe
|
|
6
|
+
seed: 1
|
|
7
|
+
steps:
|
|
8
|
+
- keys: [j]
|
|
9
|
+
state: { index: 1, score: 1, misses: 0 }
|
|
10
|
+
announce: Richtig. Tippe a.
|
|
11
|
+
- keys: [x]
|
|
12
|
+
state: { index: 1, score: 1, misses: 1 }
|
|
13
|
+
announce: Falsch. Tippe a.
|
|
14
|
+
- ticks: 100
|
|
15
|
+
state: { index: 1, misses: 1, left: 3000 }
|
|
16
|
+
- ticks: 150
|
|
17
|
+
state: { index: 2, misses: 2, left: 5000, last: late }
|
|
18
|
+
announce: Zu spät. Tippe f.
|
|
19
|
+
- keys: [F]
|
|
20
|
+
state: { index: 3, score: 2 }
|
|
21
|
+
announce: Richtig. Tippe l.
|
|
22
|
+
- keys: [l, l, s, j, k, d, l]
|
|
23
|
+
state: { score: 9, misses: 2, done: true }
|
|
24
|
+
announce: "Fertig: 9 von 10 richtig. Mit Enter spielst du noch eine Runde."
|
|
25
|
+
- inputs: [{ kind: action, name: again }]
|
|
26
|
+
state: { seed: 2, letters: kdsfljfdka, index: 0, score: 0, misses: 0, done: false }
|
|
27
|
+
announce: Neue Runde. Tippe k.
|
|
28
|
+
- keys: [k, d, s, f, l, j, f, d, k, a]
|
|
29
|
+
state: { score: 10, misses: 0, done: true }
|
|
30
|
+
announce: "Fertig: 10 von 10 richtig. Mit Enter spielst du noch eine Runde."
|
|
31
|
+
- keys: [Enter]
|
|
32
|
+
state: { seed: 3, letters: kafakffssf, index: 0, score: 0, done: false }
|
|
33
|
+
announce: Neue Runde. Tippe k.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/interactive/playthrough.v1.schema.json
|
|
2
|
+
# The obere Reihe with seed 1, whose letters start with u q z p: three hits, then the space bar and ü, wrong keys.
|
|
3
|
+
route: obere-reihe
|
|
4
|
+
seed: 1
|
|
5
|
+
steps:
|
|
6
|
+
- keys: [u, q, z]
|
|
7
|
+
state: { route: obere-reihe, index: 3, score: 3 }
|
|
8
|
+
announce: Richtig. Tippe p.
|
|
9
|
+
- keys: [" "]
|
|
10
|
+
state: { index: 3, misses: 1 }
|
|
11
|
+
announce: Falsch. Tippe p.
|
|
12
|
+
- keys: [ü]
|
|
13
|
+
state: { index: 3, misses: 2 }
|
|
14
|
+
announce: Falsch. Tippe p.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The game: the letter to type, the time left for it, the score; after the round the result and a button for the
|
|
3
|
+
// next round. It announces every change for screen readers. The runtime gives it the core's state and steps every
|
|
4
|
+
// key typed on the page; the button emits the action "again" as input.
|
|
5
|
+
import { computed, watch } from "vue";
|
|
6
|
+
import { announce } from "@lernapps/tooling/a11y";
|
|
7
|
+
import { translator } from "@lernapps/tooling/i18n";
|
|
8
|
+
import type { Input } from "@lernapps/app-templates/interactive";
|
|
9
|
+
import { lessonOf, type State } from "../core/index.ts";
|
|
10
|
+
import de from "../messages/de.json";
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{ state: State; lang: string }>();
|
|
13
|
+
const emit = defineEmits<{ input: [input: Input] }>();
|
|
14
|
+
const t = translator(de);
|
|
15
|
+
|
|
16
|
+
const letter = computed(() => props.state.letters[props.state.index] ?? "");
|
|
17
|
+
const total = computed(() => props.state.letters.length);
|
|
18
|
+
const timeLeft = computed(() => (100 * props.state.left) / (lessonOf(props.state.route).seconds * 1000));
|
|
19
|
+
|
|
20
|
+
watch(
|
|
21
|
+
() => [props.state.index, props.state.misses, props.state.done, props.state.seed] as const,
|
|
22
|
+
() => {
|
|
23
|
+
const { state } = props;
|
|
24
|
+
if (state.done) announce(t("done", { score: state.score, total: total.value }));
|
|
25
|
+
else announce(t(state.last, { letter: letter.value }));
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<section class="game" :aria-label="t('game')">
|
|
32
|
+
<template v-if="!state.done">
|
|
33
|
+
<p class="letter" aria-hidden="true">{{ letter }}</p>
|
|
34
|
+
<p class="prompt">{{ t("type", { letter }) }}</p>
|
|
35
|
+
<div class="time" aria-hidden="true"><div class="time-left" :style="{ width: `${timeLeft}%` }"></div></div>
|
|
36
|
+
</template>
|
|
37
|
+
<template v-else>
|
|
38
|
+
<p class="result">{{ t("done", { score: state.score, total }) }}</p>
|
|
39
|
+
<button type="button" class="again" @click="emit('input', { kind: 'action', name: 'again' })">
|
|
40
|
+
{{ t("again") }}
|
|
41
|
+
</button>
|
|
42
|
+
</template>
|
|
43
|
+
<p class="score">{{ t("score", { score: state.score, total }) }} · {{ t("misses", { misses: state.misses }) }}</p>
|
|
44
|
+
</section>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<style scoped>
|
|
48
|
+
.game {
|
|
49
|
+
padding: 1rem;
|
|
50
|
+
border-radius: 0.5rem;
|
|
51
|
+
background: var(--page-surface);
|
|
52
|
+
text-align: center;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.letter {
|
|
56
|
+
margin: 0;
|
|
57
|
+
font-size: 5rem;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
line-height: 1.2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.time {
|
|
63
|
+
height: 0.5rem;
|
|
64
|
+
margin: 0.5rem auto;
|
|
65
|
+
max-width: 20rem;
|
|
66
|
+
border-radius: 0.25rem;
|
|
67
|
+
background: var(--page-border);
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.time-left {
|
|
72
|
+
height: 100%;
|
|
73
|
+
background: var(--page-accent);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.again {
|
|
77
|
+
padding: 0.5rem 1.25rem;
|
|
78
|
+
border: 0;
|
|
79
|
+
border-radius: 0.375rem;
|
|
80
|
+
background: var(--page-accent);
|
|
81
|
+
color: var(--page-on-accent);
|
|
82
|
+
font: inherit;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "@lernapps/tooling/tsconfig.json", "include": ["src"] }
|