@lernapps/app-templates 0.0.1 → 0.1.0

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 (231) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +195 -2
  3. package/dist/appname-Bh23cUuz.mjs +12 -0
  4. package/dist/assessment-By2GGTJC.mjs +1696 -0
  5. package/dist/block-BK_ohsH1.mjs +82 -0
  6. package/dist/blocks-BmRrD7UY.mjs +149 -0
  7. package/dist/blocks.mjs +4 -0
  8. package/dist/check-BkEl-m-F.mjs +254 -0
  9. package/dist/content-cYBPYg_J.mjs +140 -0
  10. package/dist/contract-PONpxJtd.mjs +52 -0
  11. package/dist/e2e-BiMI-pCL.mjs +89 -0
  12. package/dist/engine-BqF2Vffn.mjs +136 -0
  13. package/dist/explainer-e2e.mjs +1297 -0
  14. package/dist/explainer-islands.mjs +197 -0
  15. package/dist/explainer-plugin.mjs +2131 -0
  16. package/dist/explainer.mjs +1246 -0
  17. package/dist/html-mffkHGHh.mjs +89 -0
  18. package/dist/image-CspJVfyQ.mjs +21 -0
  19. package/dist/interactive-e2e.mjs +140 -0
  20. package/dist/interactive-plugin.mjs +242 -0
  21. package/dist/interactive.mjs +75 -0
  22. package/dist/load-C-0hUaSC.mjs +16947 -0
  23. package/dist/needs-BzEahJ6X.mjs +97 -0
  24. package/dist/pages-9RaaomoO.mjs +81 -0
  25. package/dist/questions-e2e.mjs +1 -0
  26. package/dist/questions-engine.mjs +3 -0
  27. package/dist/questions.mjs +6 -0
  28. package/dist/quiz-BKc8JYtC.mjs +70 -0
  29. package/dist/quiz-e2e.mjs +174 -0
  30. package/dist/quiz-plugin.mjs +266 -0
  31. package/dist/quiz.mjs +246 -0
  32. package/dist/render-BJey0bwl.mjs +540 -0
  33. package/dist/render-BL6fV_NI.mjs +507 -0
  34. package/dist/render-DpRO_RKm.mjs +146 -0
  35. package/dist/result-_eQcamoz.mjs +180 -0
  36. package/dist/tasks-DADUYy52.mjs +74 -0
  37. package/dist/validate-COaZso7u.mjs +1072 -0
  38. package/dist/validate-CxtdeQ5S.mjs +146 -0
  39. package/dist/vue-DO1m6q1R.mjs +40 -0
  40. package/dist/vue-code-BkJjW_I_.mjs +320 -0
  41. package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
  42. package/docs/arc42/02-constraints.arc42.md +228 -0
  43. package/docs/arc42/03-system-context.arc42.md +118 -0
  44. package/docs/arc42/04-solution-strategy.arc42.md +47 -0
  45. package/docs/arc42/05-building-blocks.arc42.md +307 -0
  46. package/docs/arc42/06-runtime-view.arc42.md +98 -0
  47. package/docs/arc42/07-deployment-view.arc42.md +160 -0
  48. package/docs/arc42/08-concepts.arc42.md +123 -0
  49. package/docs/arc42/09-decisions.arc42.md +246 -0
  50. package/docs/arc42/10-quality-requirements.arc42.md +155 -0
  51. package/docs/arc42/11-risks.arc42.md +113 -0
  52. package/docs/arc42/12-glossary.arc42.md +162 -0
  53. package/docs/arc42/architecture-evidence.md +395 -0
  54. package/explainer/.arc42ignore +3 -0
  55. package/explainer/README.md +54 -0
  56. package/explainer/_gitignore +5 -0
  57. package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
  58. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
  59. package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
  60. package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
  61. package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
  62. package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
  63. package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
  64. package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
  65. package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
  66. package/explainer/e2e/explainer.e2e.ts +3 -0
  67. package/explainer/index.html +11 -0
  68. package/explainer/package.json +18 -0
  69. package/explainer/playwright.config.ts +1 -0
  70. package/explainer/src/app.yaml +20 -0
  71. package/explainer/src/components/ProzentFeld.test.ts +30 -0
  72. package/explainer/src/components/ProzentFeld.ts +121 -0
  73. package/explainer/src/components/ProzentFeld.vue +84 -0
  74. package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
  75. package/explainer/src/main.ts +4 -0
  76. package/explainer/src/prozent/chapter.yaml +18 -0
  77. package/explainer/src/prozent/grundwert.yaml +115 -0
  78. package/explainer/src/prozent/prozentsatz.yaml +99 -0
  79. package/explainer/src/prozent/prozentwert.yaml +119 -0
  80. package/explainer/tsconfig.json +1 -0
  81. package/explainer/vite.config.ts +4 -0
  82. package/interactive/.arc42ignore +5 -0
  83. package/interactive/README.md +28 -0
  84. package/interactive/_gitignore +4 -0
  85. package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
  86. package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
  87. package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
  88. package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
  89. package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
  90. package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
  91. package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
  92. package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
  93. package/interactive/e2e/interactive.e2e.ts +3 -0
  94. package/interactive/index.html +11 -0
  95. package/interactive/package.json +18 -0
  96. package/interactive/playwright.config.ts +1 -0
  97. package/interactive/src/app.yaml +28 -0
  98. package/interactive/src/core/index.ts +101 -0
  99. package/interactive/src/data/lessons.json +4 -0
  100. package/interactive/src/env.d.ts +6 -0
  101. package/interactive/src/main.ts +6 -0
  102. package/interactive/src/messages/de.json +12 -0
  103. package/interactive/src/playthroughs/grundreihe.yaml +33 -0
  104. package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
  105. package/interactive/src/ui/App.vue +85 -0
  106. package/interactive/tsconfig.json +1 -0
  107. package/interactive/vite.config.ts +4 -0
  108. package/package.json +142 -4
  109. package/quiz/README.md +31 -0
  110. package/quiz/_gitignore +5 -0
  111. package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
  112. package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
  113. package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
  114. package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
  115. package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
  116. package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
  117. package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
  118. package/quiz/e2e/quiz.e2e.ts +3 -0
  119. package/quiz/index.html +11 -0
  120. package/quiz/package.json +18 -0
  121. package/quiz/playwright.config.ts +1 -0
  122. package/quiz/src/images/berge-ordnen.jpg +0 -0
  123. package/quiz/src/images/everest-hoehe.jpg +0 -0
  124. package/quiz/src/images/galapagos.jpg +0 -0
  125. package/quiz/src/images/grand-canyon.jpg +0 -0
  126. package/quiz/src/images/totes-meer.jpg +0 -0
  127. package/quiz/src/images/uluru.jpg +0 -0
  128. package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
  129. package/quiz/src/main.ts +4 -0
  130. package/quiz/src/quiz.json +312 -0
  131. package/quiz/tsconfig.json +1 -0
  132. package/quiz/vite.config.ts +4 -0
  133. package/runtime/explainer/assessment-client.ts +478 -0
  134. package/runtime/explainer/assessment-page.ts +446 -0
  135. package/runtime/explainer/assessment.ts +393 -0
  136. package/runtime/explainer/check-page.ts +520 -0
  137. package/runtime/explainer/components/exercise.ts +409 -0
  138. package/runtime/explainer/components/props.ts +230 -0
  139. package/runtime/explainer/components/read.ts +428 -0
  140. package/runtime/explainer/content.ts +277 -0
  141. package/runtime/explainer/e2e.ts +1474 -0
  142. package/runtime/explainer/exercises.ts +324 -0
  143. package/runtime/explainer/explainer.css +1283 -0
  144. package/runtime/explainer/explainer.ts +838 -0
  145. package/runtime/explainer/explainer.v1.schema.json +1165 -0
  146. package/runtime/explainer/expression.ts +407 -0
  147. package/runtime/explainer/fraction.ts +107 -0
  148. package/runtime/explainer/generated/comparator.ts +31 -0
  149. package/runtime/explainer/generated/question.ts +165 -0
  150. package/runtime/explainer/generated/soundness.ts +280 -0
  151. package/runtime/explainer/generated/texts.ts +257 -0
  152. package/runtime/explainer/generated/types.ts +103 -0
  153. package/runtime/explainer/generated/values.ts +136 -0
  154. package/runtime/explainer/islands.ts +281 -0
  155. package/runtime/explainer/load.ts +457 -0
  156. package/runtime/explainer/messages/de.json +200 -0
  157. package/runtime/explainer/messages/en.json +200 -0
  158. package/runtime/explainer/messages.ts +13 -0
  159. package/runtime/explainer/needs.ts +122 -0
  160. package/runtime/explainer/open-task.ts +235 -0
  161. package/runtime/explainer/outline.ts +299 -0
  162. package/runtime/explainer/plugin.ts +488 -0
  163. package/runtime/explainer/position.ts +62 -0
  164. package/runtime/explainer/prompt.ts +75 -0
  165. package/runtime/explainer/render.ts +686 -0
  166. package/runtime/explainer/result.ts +238 -0
  167. package/runtime/explainer/routes.ts +119 -0
  168. package/runtime/explainer/schema.ts +140 -0
  169. package/runtime/explainer/scope.ts +87 -0
  170. package/runtime/explainer/stuck.ts +57 -0
  171. package/runtime/explainer/tasks.ts +109 -0
  172. package/runtime/explainer/tutor.ts +469 -0
  173. package/runtime/explainer/tutorpage.ts +669 -0
  174. package/runtime/explainer/video.ts +123 -0
  175. package/runtime/interactive/contract.ts +121 -0
  176. package/runtime/interactive/e2e.ts +190 -0
  177. package/runtime/interactive/interactive.css +82 -0
  178. package/runtime/interactive/interactive.ts +115 -0
  179. package/runtime/interactive/interactive.v1.schema.json +146 -0
  180. package/runtime/interactive/load.ts +188 -0
  181. package/runtime/interactive/messages/de.json +14 -0
  182. package/runtime/interactive/messages/en.json +14 -0
  183. package/runtime/interactive/messages.ts +11 -0
  184. package/runtime/interactive/playthrough.v1.schema.json +143 -0
  185. package/runtime/interactive/plugin.ts +266 -0
  186. package/runtime/interactive/render.ts +126 -0
  187. package/runtime/quiz/e2e.ts +233 -0
  188. package/runtime/quiz/engine.ts +242 -0
  189. package/runtime/quiz/messages/de.json +23 -0
  190. package/runtime/quiz/messages/en.json +23 -0
  191. package/runtime/quiz/plugin.ts +150 -0
  192. package/runtime/quiz/quiz.css +154 -0
  193. package/runtime/quiz/quiz.ts +56 -0
  194. package/runtime/quiz/quiz.v1.schema.json +72 -0
  195. package/runtime/quiz/render.ts +173 -0
  196. package/runtime/quiz/validate.ts +108 -0
  197. package/runtime/shared/appname.ts +16 -0
  198. package/runtime/shared/blocks/block.ts +162 -0
  199. package/runtime/shared/blocks/blocks.css +190 -0
  200. package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
  201. package/runtime/shared/blocks/code.ts +26 -0
  202. package/runtime/shared/blocks/figures.ts +49 -0
  203. package/runtime/shared/blocks/image.ts +25 -0
  204. package/runtime/shared/blocks/index.ts +13 -0
  205. package/runtime/shared/blocks/messages/de.json +4 -0
  206. package/runtime/shared/blocks/messages/en.json +4 -0
  207. package/runtime/shared/blocks/render.ts +134 -0
  208. package/runtime/shared/blocks/validate.ts +165 -0
  209. package/runtime/shared/content.ts +197 -0
  210. package/runtime/shared/examples.ts +20 -0
  211. package/runtime/shared/html.ts +10 -0
  212. package/runtime/shared/languages.ts +94 -0
  213. package/runtime/shared/page.css +101 -0
  214. package/runtime/shared/pages.ts +114 -0
  215. package/runtime/shared/questions/e2e.ts +115 -0
  216. package/runtime/shared/questions/engine.ts +209 -0
  217. package/runtime/shared/questions/index.ts +39 -0
  218. package/runtime/shared/questions/messages/de.json +26 -0
  219. package/runtime/shared/questions/messages/en.json +26 -0
  220. package/runtime/shared/questions/question.ts +430 -0
  221. package/runtime/shared/questions/question.v1.schema.json +342 -0
  222. package/runtime/shared/questions/questions.css +192 -0
  223. package/runtime/shared/questions/render.ts +247 -0
  224. package/runtime/shared/questions/validate.ts +518 -0
  225. package/runtime/shared/schema.ts +44 -0
  226. package/runtime/shared/text/check.ts +68 -0
  227. package/runtime/shared/text/formula.ts +69 -0
  228. package/runtime/shared/text/markdown.ts +185 -0
  229. package/runtime/shared/text/text.v1.schema.json +34 -0
  230. package/runtime/shared/vue-code.ts +395 -0
  231. 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,3 @@
1
+ import { interactiveTests } from "@lernapps/app-templates/interactive/e2e";
2
+
3
+ interactiveTests();
@@ -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": "github:lernapps/tooling#7354ffe7ae7d8079ba5f714d5b402666d27df066",
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,4 @@
1
+ {
2
+ "grundreihe": { "letters": "asdfjkl", "count": 10, "seconds": 5 },
3
+ "obere-reihe": { "letters": "qwertzuiop", "count": 10, "seconds": 5 }
4
+ }
@@ -0,0 +1,6 @@
1
+ // The type of a single-file component, for src/main.ts, which imports the root component.
2
+ declare module "*.vue" {
3
+ import type { Component } from "vue";
4
+ const component: Component;
5
+ export default component;
6
+ }
@@ -0,0 +1,6 @@
1
+ import "@lernapps/app-templates/interactive/style.css";
2
+ import { startInteractive } from "@lernapps/app-templates/interactive";
3
+ import * as core from "./core/index.ts";
4
+ import App from "./ui/App.vue";
5
+
6
+ startInteractive(core, App);
@@ -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"] }
@@ -0,0 +1,4 @@
1
+ import { lernapps } from "@lernapps/tooling/preset";
2
+ import { interactive } from "@lernapps/app-templates/interactive/plugin";
3
+
4
+ export default lernapps({ plugins: [interactive()] });