@the-inclusionist/engine 6.36.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 +661 -0
- package/README.md +107 -0
- package/app/css/style.css +622 -0
- package/app/public/vendor/fonts/andika-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/andika-400.woff2 +0 -0
- package/app/public/vendor/fonts/andika-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/andika-700.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-400.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-700.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-mono-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-mono-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
- package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-700.woff2 +0 -0
- package/app/public/vendor/fonts/lato-400.woff2 +0 -0
- package/app/public/vendor/fonts/lato-700.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-400.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-700.woff2 +0 -0
- package/app/public/vendor/fonts/literata-400.woff2 +0 -0
- package/app/public/vendor/fonts/literata-700.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
- package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
- package/app/public/vendor/fonts.css +88 -0
- package/dist-pkg/boot/create-game.d.ts +159 -0
- package/dist-pkg/boot/create-game.js +266 -0
- package/dist-pkg/core/a11y-sr.d.ts +4 -0
- package/dist-pkg/core/a11y-sr.js +20 -0
- package/dist-pkg/core/actions.d.ts +100 -0
- package/dist-pkg/core/actions.js +149 -0
- package/dist-pkg/core/anel.d.ts +17 -0
- package/dist-pkg/core/anel.js +31 -0
- package/dist-pkg/core/collision.d.ts +21 -0
- package/dist-pkg/core/collision.js +62 -0
- package/dist-pkg/core/constants.d.ts +84 -0
- package/dist-pkg/core/constants.js +64 -0
- package/dist-pkg/core/contract.d.ts +227 -0
- package/dist-pkg/core/contract.js +184 -0
- package/dist-pkg/core/dom-query.d.ts +9 -0
- package/dist-pkg/core/dom-query.js +25 -0
- package/dist-pkg/core/entity.d.ts +150 -0
- package/dist-pkg/core/entity.js +38 -0
- package/dist-pkg/core/escape-html.d.ts +16 -0
- package/dist-pkg/core/escape-html.js +32 -0
- package/dist-pkg/core/i18n.d.ts +71 -0
- package/dist-pkg/core/i18n.js +236 -0
- package/dist-pkg/core/layers.d.ts +39 -0
- package/dist-pkg/core/layers.js +71 -0
- package/dist-pkg/core/letter-grid.d.ts +55 -0
- package/dist-pkg/core/letter-grid.js +116 -0
- package/dist-pkg/core/loop.d.ts +17 -0
- package/dist-pkg/core/loop.js +36 -0
- package/dist-pkg/core/password.d.ts +34 -0
- package/dist-pkg/core/password.js +205 -0
- package/dist-pkg/core/rng.d.ts +20 -0
- package/dist-pkg/core/rng.js +61 -0
- package/dist-pkg/core/rotulo-acessivel.d.ts +17 -0
- package/dist-pkg/core/rotulo-acessivel.js +41 -0
- package/dist-pkg/core/run-state.d.ts +115 -0
- package/dist-pkg/core/run-state.js +40 -0
- package/dist-pkg/core/scenes.d.ts +68 -0
- package/dist-pkg/core/scenes.js +64 -0
- package/dist-pkg/core/screens.d.ts +16 -0
- package/dist-pkg/core/screens.js +28 -0
- package/dist-pkg/core/state.d.ts +138 -0
- package/dist-pkg/core/state.js +290 -0
- package/dist-pkg/core/tiles.d.ts +12 -0
- package/dist-pkg/core/tiles.js +61 -0
- package/dist-pkg/core/world.d.ts +4 -0
- package/dist-pkg/core/world.js +58 -0
- package/dist-pkg/educational/activities-registry.d.ts +47 -0
- package/dist-pkg/educational/activities-registry.js +95 -0
- package/dist-pkg/i18n/en.d.ts +3 -0
- package/dist-pkg/i18n/en.js +560 -0
- package/dist-pkg/i18n/es.d.ts +3 -0
- package/dist-pkg/i18n/es.js +558 -0
- package/dist-pkg/i18n/pt.d.ts +3 -0
- package/dist-pkg/i18n/pt.js +618 -0
- package/dist-pkg/input/default-bindings.d.ts +24 -0
- package/dist-pkg/input/default-bindings.js +122 -0
- package/dist-pkg/input/devices.d.ts +15 -0
- package/dist-pkg/input/devices.js +31 -0
- package/dist-pkg/input/edges.d.ts +58 -0
- package/dist-pkg/input/edges.js +49 -0
- package/dist-pkg/input/gamepad.d.ts +203 -0
- package/dist-pkg/input/gamepad.js +531 -0
- package/dist-pkg/input/keyboard-runtime.d.ts +61 -0
- package/dist-pkg/input/keyboard-runtime.js +83 -0
- package/dist-pkg/input/keyboard.d.ts +45 -0
- package/dist-pkg/input/keyboard.js +80 -0
- package/dist-pkg/input/keydown.d.ts +270 -0
- package/dist-pkg/input/keydown.js +381 -0
- package/dist-pkg/input/latch.d.ts +30 -0
- package/dist-pkg/input/latch.js +63 -0
- package/dist-pkg/input/pointer-space.d.ts +40 -0
- package/dist-pkg/input/pointer-space.js +51 -0
- package/dist-pkg/input/state.d.ts +11 -0
- package/dist-pkg/input/state.js +12 -0
- package/dist-pkg/input/touch-bindings.d.ts +210 -0
- package/dist-pkg/input/touch-bindings.js +365 -0
- package/dist-pkg/input/touch.d.ts +173 -0
- package/dist-pkg/input/touch.js +314 -0
- package/dist-pkg/input/transports.d.ts +90 -0
- package/dist-pkg/input/transports.js +96 -0
- package/dist-pkg/input/vocabulary-migration.d.ts +58 -0
- package/dist-pkg/input/vocabulary-migration.js +125 -0
- package/dist-pkg/platform/audio-ambient.d.ts +27 -0
- package/dist-pkg/platform/audio-ambient.js +89 -0
- package/dist-pkg/platform/audio-earcons.d.ts +24 -0
- package/dist-pkg/platform/audio-earcons.js +62 -0
- package/dist-pkg/platform/audio-jingles.d.ts +17 -0
- package/dist-pkg/platform/audio-jingles.js +54 -0
- package/dist-pkg/platform/audio-mixer.d.ts +14 -0
- package/dist-pkg/platform/audio-mixer.js +62 -0
- package/dist-pkg/platform/audio-nav.d.ts +51 -0
- package/dist-pkg/platform/audio-nav.js +99 -0
- package/dist-pkg/platform/audio-sonar.d.ts +71 -0
- package/dist-pkg/platform/audio-sonar.js +173 -0
- package/dist-pkg/platform/audio.d.ts +32 -0
- package/dist-pkg/platform/audio.js +191 -0
- package/dist-pkg/platform/interruptible-speech.d.ts +19 -0
- package/dist-pkg/platform/interruptible-speech.js +79 -0
- package/dist-pkg/platform/speech.d.ts +2 -0
- package/dist-pkg/platform/speech.js +35 -0
- package/dist-pkg/platform/storage.d.ts +87 -0
- package/dist-pkg/platform/storage.js +148 -0
- package/dist-pkg/platform/tts.d.ts +33 -0
- package/dist-pkg/platform/tts.js +142 -0
- package/dist-pkg/render/camera.d.ts +58 -0
- package/dist-pkg/render/camera.js +105 -0
- package/dist-pkg/render/canvas.d.ts +13 -0
- package/dist-pkg/render/canvas.js +26 -0
- package/dist-pkg/render/cenario-data.d.ts +127 -0
- package/dist-pkg/render/cenario-data.js +145 -0
- package/dist-pkg/render/city-tex.d.ts +63 -0
- package/dist-pkg/render/city-tex.js +228 -0
- package/dist-pkg/render/city-tiles.d.ts +11 -0
- package/dist-pkg/render/city-tiles.js +140 -0
- package/dist-pkg/render/crt.d.ts +19 -0
- package/dist-pkg/render/crt.js +92 -0
- package/dist-pkg/render/cvd-matrices.d.ts +35 -0
- package/dist-pkg/render/cvd-matrices.js +91 -0
- package/dist-pkg/render/draw.d.ts +149 -0
- package/dist-pkg/render/draw.js +221 -0
- package/dist-pkg/render/fx.d.ts +63 -0
- package/dist-pkg/render/fx.js +123 -0
- package/dist-pkg/render/hc-role-data.d.ts +14 -0
- package/dist-pkg/render/hc-role-data.js +24 -0
- package/dist-pkg/render/high-contrast.d.ts +109 -0
- package/dist-pkg/render/high-contrast.js +254 -0
- package/dist-pkg/render/lq-filter.d.ts +37 -0
- package/dist-pkg/render/lq-filter.js +94 -0
- package/dist-pkg/render/minimap.d.ts +11 -0
- package/dist-pkg/render/minimap.js +94 -0
- package/dist-pkg/render/parallax.d.ts +85 -0
- package/dist-pkg/render/parallax.js +158 -0
- package/dist-pkg/render/player-anim.d.ts +71 -0
- package/dist-pkg/render/player-anim.js +152 -0
- package/dist-pkg/render/port.d.ts +143 -0
- package/dist-pkg/render/port.js +29 -0
- package/dist-pkg/render/recycling-tex.d.ts +48 -0
- package/dist-pkg/render/recycling-tex.js +164 -0
- package/dist-pkg/render/scene-city.d.ts +77 -0
- package/dist-pkg/render/scene-city.js +181 -0
- package/dist-pkg/render/scene-parallax.d.ts +72 -0
- package/dist-pkg/render/scene-parallax.js +426 -0
- package/dist-pkg/render/scene-sky.d.ts +126 -0
- package/dist-pkg/render/scene-sky.js +295 -0
- package/dist-pkg/render/screen-pipeline.d.ts +134 -0
- package/dist-pkg/render/screen-pipeline.js +177 -0
- package/dist-pkg/render/set-cenario.d.ts +54 -0
- package/dist-pkg/render/set-cenario.js +103 -0
- package/dist-pkg/render/sprite-fx.d.ts +3 -0
- package/dist-pkg/render/sprite-fx.js +40 -0
- package/dist-pkg/render/textures.d.ts +75 -0
- package/dist-pkg/render/textures.js +240 -0
- package/dist-pkg/render/title-scene.d.ts +46 -0
- package/dist-pkg/render/title-scene.js +75 -0
- package/dist-pkg/render/viewports.d.ts +52 -0
- package/dist-pkg/render/viewports.js +185 -0
- package/dist-pkg/render/viz-axes.d.ts +98 -0
- package/dist-pkg/render/viz-axes.js +184 -0
- package/dist-pkg/render/viz-modes.d.ts +61 -0
- package/dist-pkg/render/viz-modes.js +74 -0
- package/dist-pkg/render/viz-setters.d.ts +130 -0
- package/dist-pkg/render/viz-setters.js +244 -0
- package/dist-pkg/render/weather.d.ts +96 -0
- package/dist-pkg/render/weather.js +171 -0
- package/dist-pkg/render/wheelchair-sprites.d.ts +24 -0
- package/dist-pkg/render/wheelchair-sprites.js +63 -0
- package/dist-pkg/render/world-tex.d.ts +18 -0
- package/dist-pkg/render/world-tex.js +87 -0
- package/dist-pkg/ui/activities-menu.d.ts +258 -0
- package/dist-pkg/ui/activities-menu.js +648 -0
- package/dist-pkg/ui/caa-sets.d.ts +39 -0
- package/dist-pkg/ui/caa-sets.js +65 -0
- package/dist-pkg/ui/changed-mark.d.ts +15 -0
- package/dist-pkg/ui/changed-mark.js +96 -0
- package/dist-pkg/ui/debug-panel.d.ts +67 -0
- package/dist-pkg/ui/debug-panel.js +173 -0
- package/dist-pkg/ui/dom.d.ts +24 -0
- package/dist-pkg/ui/dom.js +47 -0
- package/dist-pkg/ui/focus-trap.d.ts +71 -0
- package/dist-pkg/ui/focus-trap.js +99 -0
- package/dist-pkg/ui/fonts.d.ts +41 -0
- package/dist-pkg/ui/fonts.js +59 -0
- package/dist-pkg/ui/hud.d.ts +137 -0
- package/dist-pkg/ui/hud.js +229 -0
- package/dist-pkg/ui/item-announcement.d.ts +19 -0
- package/dist-pkg/ui/item-announcement.js +36 -0
- package/dist-pkg/ui/layout.d.ts +6 -0
- package/dist-pkg/ui/layout.js +54 -0
- package/dist-pkg/ui/loop-crash.d.ts +19 -0
- package/dist-pkg/ui/loop-crash.js +60 -0
- package/dist-pkg/ui/map-hub.d.ts +56 -0
- package/dist-pkg/ui/map-hub.js +138 -0
- package/dist-pkg/ui/menu-nav.d.ts +141 -0
- package/dist-pkg/ui/menu-nav.js +390 -0
- package/dist-pkg/ui/pause-icons.d.ts +327 -0
- package/dist-pkg/ui/pause-icons.js +620 -0
- package/dist-pkg/ui/reach-notice.d.ts +34 -0
- package/dist-pkg/ui/reach-notice.js +71 -0
- package/dist-pkg/ui/settings-audio.d.ts +139 -0
- package/dist-pkg/ui/settings-audio.js +544 -0
- package/dist-pkg/ui/settings-caa.d.ts +45 -0
- package/dist-pkg/ui/settings-caa.js +137 -0
- package/dist-pkg/ui/settings-controls.d.ts +100 -0
- package/dist-pkg/ui/settings-controls.js +152 -0
- package/dist-pkg/ui/settings-empathy.d.ts +62 -0
- package/dist-pkg/ui/settings-empathy.js +137 -0
- package/dist-pkg/ui/settings-motion.d.ts +95 -0
- package/dist-pkg/ui/settings-motion.js +282 -0
- package/dist-pkg/ui/settings-motor.d.ts +77 -0
- package/dist-pkg/ui/settings-motor.js +215 -0
- package/dist-pkg/ui/settings-panel.d.ts +76 -0
- package/dist-pkg/ui/settings-panel.js +187 -0
- package/dist-pkg/ui/settings-typo.d.ts +74 -0
- package/dist-pkg/ui/settings-typo.js +150 -0
- package/dist-pkg/ui/settings-visual.d.ts +111 -0
- package/dist-pkg/ui/settings-visual.js +252 -0
- package/dist-pkg/ui/shell.d.ts +239 -0
- package/dist-pkg/ui/shell.js +430 -0
- package/dist-pkg/ui/title.d.ts +36 -0
- package/dist-pkg/ui/title.js +60 -0
- package/dist-pkg/ui/vlibras.d.ts +11 -0
- package/dist-pkg/ui/vlibras.js +87 -0
- package/dist-pkg/ui/webcam.d.ts +7 -0
- package/dist-pkg/ui/webcam.js +88 -0
- package/docs/CREDITS.md +64 -0
- package/docs/LICENSES.md +132 -0
- package/package.json +115 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// core/loop.ts — driver do loop de jogo. Registra a função de frame num "ticker" (o app.ticker do PixiJS, ou
|
|
3
|
+
// qualquer objeto com .add(fn) e .deltaTime) e passa o dt CLAMPADO (evita saltos gigantes após a aba ficar em
|
|
4
|
+
// segundo plano). Módulo-folha. Mantém a cadência ATUAL (não muda o timing) — o fixed-timestep determinístico
|
|
5
|
+
// fica para depois, pois mudaria a física.
|
|
6
|
+
//
|
|
7
|
+
// ========================= O BOUNDARY DE ERRO (D16), E POR QUE ELE FALHA ALTO =========================
|
|
8
|
+
// Sem ele, um quadro que lança lança OUTRA VEZ no quadro seguinte, para sempre: tela congelada, console cheio, e
|
|
9
|
+
// nada na tela dizendo o que houve. A spec do `demos` pede isto por escala — "across 383 games, one bad game has
|
|
10
|
+
// to be distinguishable from a broken engine". A razão daqui é mais urgente: **criança cega não vê tela
|
|
11
|
+
// congelada.** Sem anúncio, o modo cego não distingue "travou" de "está pensando".
|
|
12
|
+
//
|
|
13
|
+
// ⚠️ E A TENTAÇÃO É O CONTRÁRIO DO CONSERTO. `try { frame() } catch { /* segue */ }` é pior que o defeito: vira
|
|
14
|
+
// jogo silenciosamente errado, rodando para sempre computando lixo. A regra aqui é a mesma que o ADR-0047 aplicou
|
|
15
|
+
// ao CRT — pega UMA vez, PARA, e ANUNCIA.
|
|
16
|
+
export function startLoop(ticker, frame, maxDt = 2, opcoes = {}) {
|
|
17
|
+
let parado = false;
|
|
18
|
+
const passo = () => {
|
|
19
|
+
if (parado)
|
|
20
|
+
return; // ticker sem `remove` não desregistra — a trava é o que faz o laço parar mesmo assim
|
|
21
|
+
try {
|
|
22
|
+
frame(Math.min(ticker.deltaTime, maxDt));
|
|
23
|
+
}
|
|
24
|
+
catch (erro) {
|
|
25
|
+
parado = true;
|
|
26
|
+
ticker.remove?.(passo); // some do ticker quando dá: callback que roda 60×/s para nada custa em hardware fraco
|
|
27
|
+
// O anúncio não pode ressuscitar o problema. Se o próprio aviso quebrar — sem leitor de tela, sem DOM —,
|
|
28
|
+
// uma exceção aqui voltaria a ser invisível dentro do ticker, que é exatamente o defeito que isto fecha.
|
|
29
|
+
try {
|
|
30
|
+
opcoes.aoFalhar?.(erro);
|
|
31
|
+
}
|
|
32
|
+
catch { /* noop: o aviso falhou; o laço já parou, que é o essencial */ }
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
ticker.add(passo);
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** Um campo do esquema: um nome e quantos BITS ele ocupa (⇒ valores de 0 a 2^bits − 1). */
|
|
3
|
+
export interface CampoSenha {
|
|
4
|
+
readonly nome: string;
|
|
5
|
+
readonly bits: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CodecSenha {
|
|
8
|
+
/** Quantos caracteres a senha tem, sem separadores. Constante: toda senha deste esquema tem este tamanho. */
|
|
9
|
+
readonly comprimento: number;
|
|
10
|
+
/** Os campos, na ordem em que foram declarados (cópia — quem lê não altera o esquema). */
|
|
11
|
+
readonly campos: readonly CampoSenha[];
|
|
12
|
+
/** Empacota os valores. Lança se algum campo faltar ou não couber — ver a nota em `codificar`. */
|
|
13
|
+
codificar(valores: Readonly<Record<string, number>>): string;
|
|
14
|
+
/** Desempacota. `null` = senha inválida (comprimento, símbolo, soma ou enchimento). */
|
|
15
|
+
decodificar(senha: string): Record<string, number> | null;
|
|
16
|
+
}
|
|
17
|
+
/** Crockford base32: sem I, L, O e U. Índice = valor de 5 bits. */
|
|
18
|
+
export declare const ALFABETO = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
19
|
+
/**
|
|
20
|
+
* Cria um codec para o esquema `campos`.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ LANÇA em esquema malformado, e isso é decisão: um esquema errado é erro de PROGRAMA, e um erro de
|
|
23
|
+
* programa que só aparece como senha estranha três meses depois custa mais caro que um boot que não sobe.
|
|
24
|
+
* É a mesma escolha de `boot/create-game` para a declaração malformada.
|
|
25
|
+
*/
|
|
26
|
+
export declare function criarCodec(campos: readonly CampoSenha[]): CodecSenha;
|
|
27
|
+
/**
|
|
28
|
+
* Insere um separador a cada `grupo` caracteres — `'A1B2C3D4'` vira `'A1B2-C3D4'`.
|
|
29
|
+
*
|
|
30
|
+
* É acessibilidade, não enfeite: uma sequência sem grupos é lida caractere a caractere pelo leitor de tela e
|
|
31
|
+
* copiada perdendo o lugar por quem lê da tela para o caderno. `decodificar` ignora o separador, então a
|
|
32
|
+
* senha agrupada e a senha corrida são a MESMA senha — quem digitar sem o traço não é punido por isso.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatar(senha: string, grupo?: number, sep?: string): string;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// core/password — O CODEC DE CÓDIGOS CURTOS copiados à mão. Módulo-FOLHA: zero dependências, zero I/O, zero DOM.
|
|
3
|
+
//
|
|
4
|
+
// ========================= PARA QUE ESTE CODEC EXISTE — E PARA QUE ELE NÃO EXISTE MAIS =========================
|
|
5
|
+
// Ele nasceu para outra coisa, e a honestidade do arquivo depende de dizer isso.
|
|
6
|
+
//
|
|
7
|
+
// A primeira versão era uma SENHA DE PROGRESSÃO: a criança anotava quatro letras no caderno e recuperava o
|
|
8
|
+
// nível de alfabetização numa máquina de escola restaurada de madrugada. O ADR-0034 registrou o desenho
|
|
9
|
+
// inteiro; o ADR-0037 o enterrou, e a razão veio do Dev, não de um defeito: **não existe salvar jogo, e não
|
|
10
|
+
// deve existir**. Os jogos têm cinco fases e terminam em quinze minutos. Uma senha para recuperar progresso
|
|
11
|
+
// de uma partida que acaba antes do recreio é máquina sem carga.
|
|
12
|
+
//
|
|
13
|
+
// O QUE SOBROU, e é o motivo de o arquivo continuar aqui: a única senha que o desenho tem é a que o PROFESSOR
|
|
14
|
+
// cria para a turma entrar na mesma sala. Enquanto não houver servidor, essa sala é o computador do professor
|
|
15
|
+
// na rede local. E um código de sala é o mesmo problema de engenharia que uma senha de progressão — poucos
|
|
16
|
+
// bits, copiados do quadro por uma criança de sete anos, onde um erro de cópia ACEITO é pior que um recusado.
|
|
17
|
+
// Por isso o codec permaneceu inteiro e o registro do lado do jogo (`game/progress`) foi apagado: o que morreu
|
|
18
|
+
// foi o significado dos campos, não a aritmética.
|
|
19
|
+
//
|
|
20
|
+
// ⚠️ NENHUM CHAMADOR HOJE. Este módulo está sem consumidor desde que `game/progress` saiu, e isso está dito
|
|
21
|
+
// aqui em vez de descoberto por alguém daqui a três meses. Ele fica porque a tela de sala vai precisar dele e
|
|
22
|
+
// porque apagá-lo custaria reescrever a verificação exaustiva que já está testada — não porque esteja em uso.
|
|
23
|
+
//
|
|
24
|
+
// ========================= O QUE ESTE MÓDULO SABE, E O QUE ELE NUNCA SABERÁ =========================
|
|
25
|
+
// A regra do ADR-0033 vale aqui inteira: "a entidade da engine pode declarar o que a ENGINE possui; não pode
|
|
26
|
+
// declarar o que o JOGO possui". Uma senha com os campos `nivel`, `pontos` e `fases` seria exatamente a
|
|
27
|
+
// violação que o ADR proíbe — seriam os campos DESTE jogo, congelados na engine, e o segundo consumidor
|
|
28
|
+
// (`quiz.html`) teria de fingir ter um "nivel" para caber.
|
|
29
|
+
//
|
|
30
|
+
// Então este módulo não conhece campo nenhum: ele recebe um ESQUEMA — uma lista de `{nome, bits}` que o jogo
|
|
31
|
+
// declara — e sabe empacotar inteiros pequenos em símbolos legíveis, com sobra de detecção de erro. O que a
|
|
32
|
+
// engine possui é a CODIFICAÇÃO; o que o jogo possui é o significado de cada campo.
|
|
33
|
+
//
|
|
34
|
+
// ========================= AS TRÊS DECISÕES QUE FAZEM O CÓDIGO SERVIR A UMA CRIANÇA =========================
|
|
35
|
+
//
|
|
36
|
+
// 1. O ALFABETO É DE 32 SÍMBOLOS, SEM I, L, O E U (o de Crockford). Os três primeiros saem por ambiguidade
|
|
37
|
+
// visual — I/1, L/1, O/0 são o erro clássico de quem copia de um caderno a lápis; o U sai porque sem ele
|
|
38
|
+
// o alfabeto não forma palavrão por acidente, e uma senha que sorteia uma ofensa na tela de uma sala de
|
|
39
|
+
// aula é um problema real, não hipotético. O que sobra são 32 símbolos = 5 bits exatos por caractere.
|
|
40
|
+
// A leniência de Crockford entra na LEITURA: quem digitar I, L ou O recebe 1, 1 e 0. Escrever nunca os
|
|
41
|
+
// produz; ler perdoa quem os escreveu. E o ADR-0027 diz que a entrada não é teclado, é GRADE DE LETRAS —
|
|
42
|
+
// 32 símbolos cabem numa grade 8x4 legível a 320x180, que é o que a grade precisa desenhar.
|
|
43
|
+
//
|
|
44
|
+
// 2. A VERIFICAÇÃO É EXATA PARA OS DOIS ERROS QUE A CRIANÇA COMETE, e não "provável". Um dígito de soma
|
|
45
|
+
// simples de 5 bits deixaria 1 código errado em 32 passar — e um código errado que PASSA é pior que um
|
|
46
|
+
// rejeitado: ele leva a criança para OUTRA SALA, com a atividade de outra turma, e ninguém na sala entende
|
|
47
|
+
// por quê. Aqui a soma é PONDERADA PELA POSIÇÃO, módulo 1021, em 2 símbolos:
|
|
48
|
+
// · trocar UM símbolo por outro, em qualquer posição → SEMPRE detectado (a diferença que ele causa na
|
|
49
|
+
// soma é (i+1)·d, e |(i+1)·d| < 1021, logo nunca cai em zero por acaso do módulo);
|
|
50
|
+
// · TROCAR DOIS símbolos DO CORPO de lugar → SEMPRE detectado ((i−j)·(vj−vi), idem).
|
|
51
|
+
// Não é probabilidade: é aritmética, e é por isso que `criarCodec` limita o corpo a 32 símbolos — acima
|
|
52
|
+
// disso o produto passaria de 1021 e a garantia cairia em silêncio. O que fica de fora da garantia é a
|
|
53
|
+
// troca de um símbolo do corpo com um dos dois da soma; para essa, e para uma senha forjada ao acaso, a
|
|
54
|
+
// chance de colar é ~1/1021.
|
|
55
|
+
// ⚠️ O QUE DÁ A GARANTIA É O TAMANHO DO MÓDULO, NÃO A PRIMALIDADE — e eu conferi em vez de supor: trocar
|
|
56
|
+
// 1021 por 1024 (potência de dois) NÃO faz nenhum dos 26 casos deste módulo falhar. É informação sobre o
|
|
57
|
+
// alcance do teste, e fica registrada aqui em vez de virar uma crença. 1021 continua sendo a escolha por
|
|
58
|
+
// ser o maior primo abaixo do teto de dois símbolos: para as duas classes provadas os dois módulos
|
|
59
|
+
// empatam, mas um módulo com fator 2^k é cego a erros MÚLTIPLOS cuja diferença total seja múltipla dele,
|
|
60
|
+
// e um primo não tem esse buraco de graça.
|
|
61
|
+
//
|
|
62
|
+
// 3. `decodificar` DEVOLVE `null`, e não uma exceção nem um objeto "meio válido". Senha errada é o caso
|
|
63
|
+
// NORMAL desta função — é o que acontece quando a criança erra uma letra —, e caso normal não se sinaliza
|
|
64
|
+
// com exceção. Quem chama sabe o comprimento esperado (`codec.comprimento`) e pode distinguir "faltam
|
|
65
|
+
// letras" de "senha errada" sem que este módulo invente vocabulário de interface para isso.
|
|
66
|
+
/** Crockford base32: sem I, L, O e U. Índice = valor de 5 bits. */
|
|
67
|
+
export const ALFABETO = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
68
|
+
/** O módulo da soma ponderada. Ver a decisão 2 no topo: é o TAMANHO dele que dá a garantia, e o limite de 32. */
|
|
69
|
+
const MODULO = 1021;
|
|
70
|
+
/** Símbolos do corpo, no máximo. Acima disso a garantia da soma cairia sem nenhum sintoma visível. */
|
|
71
|
+
const MAX_SIMBOLOS = 32;
|
|
72
|
+
const VALOR_DE = new Map();
|
|
73
|
+
for (let i = 0; i < ALFABETO.length; i++)
|
|
74
|
+
VALOR_DE.set(ALFABETO[i], i);
|
|
75
|
+
// A leniência de Crockford, só na LEITURA. Escrever jamais produz estes três.
|
|
76
|
+
VALOR_DE.set('I', 1);
|
|
77
|
+
VALOR_DE.set('L', 1);
|
|
78
|
+
VALOR_DE.set('O', 0);
|
|
79
|
+
/**
|
|
80
|
+
* A soma ponderada pela posição, módulo `MODULO`, como DOIS símbolos.
|
|
81
|
+
*
|
|
82
|
+
* O peso `i+1` (e não `i`) é o que faz o primeiro símbolo contar: com peso 0, trocar o primeiro caractere
|
|
83
|
+
* por qualquer outro passaria despercebido — e o primeiro caractere é justamente o que mais se digita errado.
|
|
84
|
+
*/
|
|
85
|
+
function soma(simbolos) {
|
|
86
|
+
let c = 0;
|
|
87
|
+
for (let i = 0; i < simbolos.length; i++)
|
|
88
|
+
c = (c + (i + 1) * simbolos[i]) % MODULO;
|
|
89
|
+
return [(c >> 5) & 31, c & 31];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Cria um codec para o esquema `campos`.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ LANÇA em esquema malformado, e isso é decisão: um esquema errado é erro de PROGRAMA, e um erro de
|
|
95
|
+
* programa que só aparece como senha estranha três meses depois custa mais caro que um boot que não sobe.
|
|
96
|
+
* É a mesma escolha de `boot/create-game` para a declaração malformada.
|
|
97
|
+
*/
|
|
98
|
+
// ⚠️ AS MENSAGENS DE ERRO ABAIXO ESTÃO EM INGLÊS, e os comentários deste arquivo não. Não é descuido: o gate
|
|
99
|
+
// `tests/engine-i18n.node.test.js` reprovou este módulo quando elas nasceram em português, e a gaveta que ele
|
|
100
|
+
// oferece — "mensagens de programador", onde já moram `core/contract`, `boot/create-game` e `render/sprites` —
|
|
101
|
+
// só me aceitaria subindo o teto global de 76 para 82 no mesmo commit que cria a dívida. Afrouxar o gate para
|
|
102
|
+
// caber nele é o defeito que o gate existe para impedir. Comentário não vai para tela nenhuma e segue em
|
|
103
|
+
// pt-BR, como no resto do repositório; STRING vai, mesmo que só até o console de quem programa.
|
|
104
|
+
export function criarCodec(campos) {
|
|
105
|
+
if (campos.length === 0) {
|
|
106
|
+
throw new Error('core/password: empty schema — a password with zero fields carries only its own checksum. Declare at least one field, or skip the codec.');
|
|
107
|
+
}
|
|
108
|
+
const vistos = new Set();
|
|
109
|
+
let bitsTotal = 0;
|
|
110
|
+
for (const c of campos) {
|
|
111
|
+
if (!c.nome)
|
|
112
|
+
throw new Error('core/password: field without a name — the name is the key it gets in the object `decodificar` returns.');
|
|
113
|
+
if (vistos.has(c.nome))
|
|
114
|
+
throw new Error(`core/password: field "${c.nome}" declared twice — the second one would shadow the first when reading.`);
|
|
115
|
+
vistos.add(c.nome);
|
|
116
|
+
if (!Number.isInteger(c.bits) || c.bits < 1 || c.bits > 30) {
|
|
117
|
+
throw new Error(`core/password: field "${c.nome}" asks for ${c.bits} bits — use an integer from 1 to 30 (a 0-bit field stores nothing; above 30 the bitwise arithmetic in JS stops being exact).`);
|
|
118
|
+
}
|
|
119
|
+
bitsTotal += c.bits;
|
|
120
|
+
}
|
|
121
|
+
const simbolosCorpo = Math.ceil(bitsTotal / 5);
|
|
122
|
+
if (simbolosCorpo > MAX_SIMBOLOS) {
|
|
123
|
+
throw new Error(`core/password: ${bitsTotal} bits would take ${simbolosCorpo} symbols, and above ${MAX_SIMBOLOS} the weighted checksum stops detecting EVERY transposition (see decision 2 at the top). Store less, or split it into two passwords.`);
|
|
124
|
+
}
|
|
125
|
+
const listaCampos = campos.map((c) => Object.freeze({ nome: c.nome, bits: c.bits }));
|
|
126
|
+
return {
|
|
127
|
+
comprimento: simbolosCorpo + 2,
|
|
128
|
+
campos: Object.freeze(listaCampos),
|
|
129
|
+
codificar(valores) {
|
|
130
|
+
// Bit a bit, do mais significativo ao menos, na ordem declarada. Escrito assim — e não com `<<` sobre
|
|
131
|
+
// um acumulador — porque um esquema de 40 bits estouraria os 32 bits dos operadores do JS, e o sintoma
|
|
132
|
+
// seria uma senha que decodifica errado só nos campos do fim.
|
|
133
|
+
const bits = [];
|
|
134
|
+
for (const c of listaCampos) {
|
|
135
|
+
const v = valores[c.nome];
|
|
136
|
+
const teto = 2 ** c.bits - 1;
|
|
137
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0 || v > teto) {
|
|
138
|
+
throw new Error(`core/password: field "${c.nome}" got ${String(v)} — expected an integer from 0 to ${teto}. Fix the value BEFORE encoding (clamping here silently would write a password that restores different progress).`);
|
|
139
|
+
}
|
|
140
|
+
for (let b = c.bits - 1; b >= 0; b--)
|
|
141
|
+
bits.push((v >> b) & 1);
|
|
142
|
+
}
|
|
143
|
+
while (bits.length % 5 !== 0)
|
|
144
|
+
bits.push(0); // enchimento: zeros à direita, conferidos na leitura
|
|
145
|
+
const simbolos = [];
|
|
146
|
+
for (let i = 0; i < bits.length; i += 5) {
|
|
147
|
+
simbolos.push((bits[i] << 4) | (bits[i + 1] << 3) | (bits[i + 2] << 2) | (bits[i + 3] << 1) | bits[i + 4]);
|
|
148
|
+
}
|
|
149
|
+
const [s1, s2] = soma(simbolos);
|
|
150
|
+
return [...simbolos, s1, s2].map((v) => ALFABETO[v]).join('');
|
|
151
|
+
},
|
|
152
|
+
decodificar(senha) {
|
|
153
|
+
if (typeof senha !== 'string')
|
|
154
|
+
return null;
|
|
155
|
+
const cru = [];
|
|
156
|
+
for (const ch of senha.toUpperCase()) {
|
|
157
|
+
if (ch === ' ' || ch === '-' || ch === '·')
|
|
158
|
+
continue; // separadores de leitura (ver `formatar`)
|
|
159
|
+
const v = VALOR_DE.get(ch);
|
|
160
|
+
if (v === undefined)
|
|
161
|
+
return null; // símbolo fora do alfabeto: não vale adivinhar o que ela quis dizer
|
|
162
|
+
cru.push(v);
|
|
163
|
+
}
|
|
164
|
+
if (cru.length !== simbolosCorpo + 2)
|
|
165
|
+
return null;
|
|
166
|
+
const simbolos = cru.slice(0, simbolosCorpo);
|
|
167
|
+
const [s1, s2] = soma(simbolos);
|
|
168
|
+
if (s1 !== cru[simbolosCorpo] || s2 !== cru[simbolosCorpo + 1])
|
|
169
|
+
return null;
|
|
170
|
+
const bits = [];
|
|
171
|
+
for (const v of simbolos)
|
|
172
|
+
for (let b = 4; b >= 0; b--)
|
|
173
|
+
bits.push((v >> b) & 1);
|
|
174
|
+
// O enchimento tem de ser zero: `codificar` só produz zeros ali. Uma senha com lixo no fim não saiu
|
|
175
|
+
// daqui, e aceitá-la seria aceitar uma senha que este módulo é incapaz de gerar.
|
|
176
|
+
for (let i = bitsTotal; i < bits.length; i++)
|
|
177
|
+
if (bits[i] !== 0)
|
|
178
|
+
return null;
|
|
179
|
+
const fora = {};
|
|
180
|
+
let p = 0;
|
|
181
|
+
for (const c of listaCampos) {
|
|
182
|
+
let v = 0;
|
|
183
|
+
for (let b = 0; b < c.bits; b++)
|
|
184
|
+
v = v * 2 + bits[p++];
|
|
185
|
+
fora[c.nome] = v;
|
|
186
|
+
}
|
|
187
|
+
return fora;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Insere um separador a cada `grupo` caracteres — `'A1B2C3D4'` vira `'A1B2-C3D4'`.
|
|
193
|
+
*
|
|
194
|
+
* É acessibilidade, não enfeite: uma sequência sem grupos é lida caractere a caractere pelo leitor de tela e
|
|
195
|
+
* copiada perdendo o lugar por quem lê da tela para o caderno. `decodificar` ignora o separador, então a
|
|
196
|
+
* senha agrupada e a senha corrida são a MESMA senha — quem digitar sem o traço não é punido por isso.
|
|
197
|
+
*/
|
|
198
|
+
export function formatar(senha, grupo = 4, sep = '-') {
|
|
199
|
+
if (grupo < 1)
|
|
200
|
+
return senha;
|
|
201
|
+
const partes = [];
|
|
202
|
+
for (let i = 0; i < senha.length; i += grupo)
|
|
203
|
+
partes.push(senha.slice(i, i + grupo));
|
|
204
|
+
return partes.join(sep);
|
|
205
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** Uma corrente de números pseudoaleatórios independente de qualquer outra. */
|
|
3
|
+
export interface Rng {
|
|
4
|
+
/** [0, 1). */
|
|
5
|
+
readonly rnd: () => number;
|
|
6
|
+
/** Inteiro em [lo, hi], ambos inclusive. */
|
|
7
|
+
readonly randInt: (lo: number, hi: number) => number;
|
|
8
|
+
/** Cópia baralhada (Fisher-Yates); não toca no original. */
|
|
9
|
+
readonly shuffle: <T>(arr: readonly T[]) => T[];
|
|
10
|
+
/** Reposiciona ESTA corrente. Não alcança nenhuma outra. */
|
|
11
|
+
readonly reseed: (s: number) => void;
|
|
12
|
+
}
|
|
13
|
+
/** A semente do jogo próprio da engine. Um consumidor externo escolhe a sua. */
|
|
14
|
+
export declare const SEMENTE_PADRAO = 20260601;
|
|
15
|
+
export declare const createRng: (semente?: number) => Rng;
|
|
16
|
+
export declare const reseed: (s: number) => void;
|
|
17
|
+
export declare const rnd: () => number;
|
|
18
|
+
export declare const randInt: (lo: number, hi: number) => number;
|
|
19
|
+
export declare const shuffle: <T>(arr: readonly T[]) => T[];
|
|
20
|
+
export declare const rngDecoracao: Rng;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// core/rng.ts — RNG semeado (LCG determinístico) p/ reprodutibilidade (coin placement / testes estáveis).
|
|
3
|
+
// Módulo-folha PURO, ZERO deps.
|
|
4
|
+
//
|
|
5
|
+
// ⚠️ DUAS COISAS MUDARAM AQUI NA MESMA MIGRAÇÃO, E ISSO É DE PROPÓSITO (issue #107).
|
|
6
|
+
//
|
|
7
|
+
// 1. `createRng(semente)` existe. A regra D13 do ADR-0038 — «um módulo que guarda estado exporta uma
|
|
8
|
+
// fábrica `createX()`» — nomeava este módulo desde sempre e a conversão nunca aconteceu. O preço foi
|
|
9
|
+
// medido pelo `game-15puzzle`, o segundo consumidor externo: com uma única corrente compartilhada por
|
|
10
|
+
// doze módulos (`game/*`, `render/fx`, `render/weather`, `render/draw`), «embaralhamento determinístico
|
|
11
|
+
// a partir da semente S» só é reproduzível numa página onde mais nada desenha — e a engine existe
|
|
12
|
+
// justamente para que o jogo NÃO esteja sozinho na página. Cada consumidor faz a sua corrente.
|
|
13
|
+
//
|
|
14
|
+
// 2. A aritmética do LCG estava a perder precisão. `_seed * 1103515245` com `_seed` perto de 2³¹ chega a
|
|
15
|
+
// ~2,37×10¹⁸, acima de 2⁵³: os bits BAIXOS — exatamente os que o `& 0x7fffffff` guarda — eram
|
|
16
|
+
// arredondados fora antes de a máscara correr. `Math.imul` faz a multiplicação em 32 bits, que é o que
|
|
17
|
+
// um LCG de 32 bits sempre quis dizer.
|
|
18
|
+
//
|
|
19
|
+
// ⚠️ POR QUE AS DUAS JUNTAS. Cada uma sozinha muda a sequência semeada. Fazer uma hoje e a outra depois
|
|
20
|
+
// mudaria as sequências DUAS vezes, e da segunda vez ninguém se lembraria do porquê — o defeito ficaria a
|
|
21
|
+
// parecer regressão. Uma migração, uma mudança de sequência, uma explicação.
|
|
22
|
+
//
|
|
23
|
+
// ⚠️ O QUE NÃO MUDA: continua determinístico. A aritmética antiga também era reprodutível (ponto flutuante
|
|
24
|
+
// é determinístico), e foi por isso que o defeito nunca apareceu: não era aleatoriedade errada, era um
|
|
25
|
+
// gerador PIOR do que o que foi escrito. Fixtures que dependiam dos valores concretos mudam de valor.
|
|
26
|
+
/** A semente do jogo próprio da engine. Um consumidor externo escolhe a sua. */
|
|
27
|
+
export const SEMENTE_PADRAO = 20260601;
|
|
28
|
+
export const createRng = (semente = SEMENTE_PADRAO) => {
|
|
29
|
+
let _seed = semente >>> 0;
|
|
30
|
+
// `Math.imul` e não `*`: ver o cabeçalho. O `+ 12345` cabe em segurança porque `imul` já devolveu
|
|
31
|
+
// um inteiro de 32 bits com sinal, e a máscara `& 0x7fffffff` desfaz o sinal.
|
|
32
|
+
const rnd = () => (_seed = (Math.imul(_seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff;
|
|
33
|
+
const randInt = (lo, hi) => lo + Math.floor(rnd() * (hi - lo + 1));
|
|
34
|
+
const shuffle = (arr) => {
|
|
35
|
+
const a = [...arr];
|
|
36
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
37
|
+
const j = (rnd() * (i + 1)) | 0;
|
|
38
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
39
|
+
}
|
|
40
|
+
return a;
|
|
41
|
+
};
|
|
42
|
+
const reseed = (s) => { _seed = s >>> 0; };
|
|
43
|
+
return { rnd, randInt, shuffle, reseed };
|
|
44
|
+
};
|
|
45
|
+
// ⚠️ A CORRENTE DO JOGO PRÓPRIO DA ENGINE, e SÓ dela. Existe porque `game/` ainda vive aqui dentro
|
|
46
|
+
// (issue #111: o cartucho que ainda não saiu, ADR-0036/ADR-0083). Quando `game/` sair, isto sai com ele.
|
|
47
|
+
// UM CONSUMIDOR EXTERNO NÃO DEVE IMPORTAR ESTES QUATRO — são estado partilhado, que é o defeito que a
|
|
48
|
+
// fábrica acima conserta. Faça `createRng(suaSemente)`.
|
|
49
|
+
const _padrao = createRng(SEMENTE_PADRAO);
|
|
50
|
+
export const reseed = _padrao.reseed;
|
|
51
|
+
export const rnd = _padrao.rnd;
|
|
52
|
+
export const randInt = _padrao.randInt;
|
|
53
|
+
export const shuffle = _padrao.shuffle;
|
|
54
|
+
// ⚠️ A CORRENTE DA DECORAÇÃO, separada da de cima porque a mistura era o defeito CONCRETO.
|
|
55
|
+
// `render/fx` tira um número por partícula, `render/weather` por gota, `render/draw` dois por tremor de
|
|
56
|
+
// câmara — dezenas por quadro. Saindo da mesma corrente das moedas, «semeie com S e o mapa sai igual»
|
|
57
|
+
// passava a depender de quantas partículas a tela desenhou antes, o que ninguém controla nem repara.
|
|
58
|
+
// Enfeite NÃO pode mover o sorteio do jogo. São dois assuntos, e agora são duas correntes.
|
|
59
|
+
// A semente é outra de propósito: se fosse a mesma, as duas correntes andariam em paralelo e o enfeite
|
|
60
|
+
// ficaria correlacionado com o mapa — determinístico, mas visivelmente repetitivo.
|
|
61
|
+
export const rngDecoracao = createRng(SEMENTE_PADRAO ^ 0x5eed);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** A fatia mínima de `Element` que este módulo lê. Estrutural para o teste de node não precisar de DOM real. */
|
|
3
|
+
export interface ElementoComRotulo {
|
|
4
|
+
getAttribute(nome: string): string | null;
|
|
5
|
+
textContent: string | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* O nome do controle: `aria-label` quando existe, o texto visível quando não.
|
|
9
|
+
*
|
|
10
|
+
* A ORDEM É A DECISÃO, e ela não é arbitrária: `aria-label` é o que a plataforma de acessibilidade JÁ vai
|
|
11
|
+
* anunciar. Narrar outra coisa não acrescenta informação — cria uma segunda versão do mesmo item, e quem
|
|
12
|
+
* escuta as duas não tem como saber qual é a verdadeira.
|
|
13
|
+
*
|
|
14
|
+
* O texto visível entra quando não há rótulo declarado, que é o caso da maioria dos botões: ali as duas
|
|
15
|
+
* fontes já coincidem por construção.
|
|
16
|
+
*/
|
|
17
|
+
export declare function rotuloAcessivel(el: ElementoComRotulo | null | undefined): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// core/rotulo-acessivel — COMO SE CHAMA UM CONTROLE, e uma resposta só para quem vê e para quem escuta.
|
|
3
|
+
//
|
|
4
|
+
// ========================= O DEFEITO QUE ISTO FECHA =========================
|
|
5
|
+
// MEDIDO no jogo construído, pousando o cursor no botão de número de jogadores do menu inicial:
|
|
6
|
+
//
|
|
7
|
+
// o jogo narrou: "◀ Number of players: 1 ▶, 1 of 4"
|
|
8
|
+
// o leitor de tela diz: "Number of players: 1. Click on the left for fewer, on the right for more."
|
|
9
|
+
//
|
|
10
|
+
// Duas frases diferentes para o MESMO item, no mesmo instante. Uma criança que usa leitor de tela E a
|
|
11
|
+
// narração do jogo ouve o item duas vezes, de dois jeitos — e a versão do jogo lê os glifos `◀` e `▶`, que é
|
|
12
|
+
// exatamente o ruído que o item 4 do ADR-0044 tirou da legenda da pausa.
|
|
13
|
+
//
|
|
14
|
+
// A REGRA JÁ EXISTIA, escrita uma vez: `legendaDoIcone` (ui/pause-icons) lê o `aria-label` do ícone justamente
|
|
15
|
+
// para que "passar o mouse ou focar diga a MESMA verdade que um leitor de tela anunciaria". Ela valia para os
|
|
16
|
+
// dez ícones e não para o resto dos menus. Aqui ela vira uma função, e as três chamadas passam a ser a mesma.
|
|
17
|
+
//
|
|
18
|
+
// ========================= POR QUE UM MÓDULO-FOLHA =========================
|
|
19
|
+
// Três consumidores em camadas diferentes (`ui/activities-menu`, `ui/menu-nav`, `ui/pause-icons`), e
|
|
20
|
+
// `ui/menu-nav` já importa `ui/pause-icons` — pôr a regra num deles fecharia ciclo ou obrigaria alguém a
|
|
21
|
+
// importar de quem não devia. Mesma lição de `core/anel`, e ela é recente: ciclo em ESM não estoura na hora,
|
|
22
|
+
// estoura no boot em TDZ, uma vez, em produção.
|
|
23
|
+
//
|
|
24
|
+
// Zero dependências, zero I/O, nenhum `document` global: recebe o elemento e devolve texto.
|
|
25
|
+
/** Espaço em branco de markup vira UM espaço; pontas somem. */
|
|
26
|
+
const enxuto = (s) => (s || '').replace(/\s+/g, ' ').trim();
|
|
27
|
+
/**
|
|
28
|
+
* O nome do controle: `aria-label` quando existe, o texto visível quando não.
|
|
29
|
+
*
|
|
30
|
+
* A ORDEM É A DECISÃO, e ela não é arbitrária: `aria-label` é o que a plataforma de acessibilidade JÁ vai
|
|
31
|
+
* anunciar. Narrar outra coisa não acrescenta informação — cria uma segunda versão do mesmo item, e quem
|
|
32
|
+
* escuta as duas não tem como saber qual é a verdadeira.
|
|
33
|
+
*
|
|
34
|
+
* O texto visível entra quando não há rótulo declarado, que é o caso da maioria dos botões: ali as duas
|
|
35
|
+
* fontes já coincidem por construção.
|
|
36
|
+
*/
|
|
37
|
+
export function rotuloAcessivel(el) {
|
|
38
|
+
if (!el)
|
|
39
|
+
return '';
|
|
40
|
+
return enxuto(el.getAttribute('aria-label')) || enxuto(el.textContent);
|
|
41
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import type { GateTile } from './state.js';
|
|
3
|
+
import type { Player } from './entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* OS EXTRAS DO NÍVEL — o que o mapa monta a cada rodada e ninguém persiste.
|
|
6
|
+
*
|
|
7
|
+
* `P` é o tipo do power-up, e ele vem de quem cria a instância. Ver a nota do genérico no cabeçalho.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExtrasDoNivel<P> {
|
|
10
|
+
/** Os power-ups espalhados pelo nível. `readonly`: quem lê não reordena. */
|
|
11
|
+
powerups: readonly P[];
|
|
12
|
+
/** Os tiles do portão, como chaves `"tx,ty"` — `core/collision` só pergunta `.has()`. */
|
|
13
|
+
gateTiles: ReadonlySet<string>;
|
|
14
|
+
/** A lista de tiles do portão, ou `null` quando o nível não tem portão. */
|
|
15
|
+
gate: readonly GateTile[] | null;
|
|
16
|
+
/** Portão aberto? FECHADO significa que os tiles acima são sólidos. */
|
|
17
|
+
gateOpen: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* O estado de RODADA. Hoje são os extras do nível mais o `wcSolid`; os outros campos chegam nos próximos
|
|
21
|
+
* passos da Fase B.
|
|
22
|
+
*
|
|
23
|
+
* O `wcSolid` fica FORA do `ExtrasDoNivel` porque ele não nasce com os outros quatro: a geometria de
|
|
24
|
+
* cadeirante é recalculada por conta própria quando o modo liga ou desliga, e tem setter só dela. Juntá-los
|
|
25
|
+
* num objeto só faria a assinatura mentir sobre quando cada um muda.
|
|
26
|
+
*/
|
|
27
|
+
export interface RunState<P> extends ExtrasDoNivel<P> {
|
|
28
|
+
/** Sólidos que só existem no modo cadeirante — rampas e plataformas, como chaves `"x,y"`. */
|
|
29
|
+
wcSolid: ReadonlySet<string>;
|
|
30
|
+
/**
|
|
31
|
+
* OS JOGADORES (1..4). Sem setter, e isso é declaração, não esquecimento: o array NUNCA é reatribuído —
|
|
32
|
+
* ele é mutado no lugar (`push`, `splice`, `length`, `players[i]`), e quem faz isso é `game/session`, que
|
|
33
|
+
* é o dono da entrada e da saída de jogador. Um setter aqui daria a impressão de que trocar a lista
|
|
34
|
+
* inteira é uma operação prevista, e ela não é: as referências que os módulos guardam sobreviveriam à
|
|
35
|
+
* troca apontando para a lista velha.
|
|
36
|
+
*
|
|
37
|
+
* `Player[]` é a visão da ENGINE. Cada jogo acrescenta campos (o `GamePlayer` daqui tem `quiz`), e os
|
|
38
|
+
* consumidores que precisam deles estreitam por conta própria — a dívida de conversões que isso gera é
|
|
39
|
+
* anterior a este arquivo e não muda com a mudança de endereço.
|
|
40
|
+
*/
|
|
41
|
+
players: Player[];
|
|
42
|
+
/**
|
|
43
|
+
* QUANTOS jogadores/telas (1..4). Não persistido — a partida seguinte começa com um de novo, que é o
|
|
44
|
+
* comportamento que uma sala de aula quer.
|
|
45
|
+
*/
|
|
46
|
+
numPlayers: number;
|
|
47
|
+
/** A rodada acabou (vitória). Trava a entrada e deixa o cenário simulando por trás do aviso. */
|
|
48
|
+
ended: boolean;
|
|
49
|
+
/** Semente da decoração do cenário. Sorteada a cada fase DE PROPÓSITO — é o que faz duas partidas da
|
|
50
|
+
* mesma fase não terem a mesma grama. */
|
|
51
|
+
decorSeed: number;
|
|
52
|
+
/** Fração das superfícies com grama: 1 = todas, 0.6 = 60% (a base das estações que virão). */
|
|
53
|
+
grassDensity: number;
|
|
54
|
+
/** QUAL jogador os painéis de acessibilidade visual estão editando. Não é preferência — é qual aba está
|
|
55
|
+
* aberta —, e por isso não se persiste: guardá-la faria a criança reabrir o jogo já editando o jogador 2. */
|
|
56
|
+
selVizPlayer: number;
|
|
57
|
+
/**
|
|
58
|
+
* QUEM abriu o menu de pausa, e portanto o ESCOPO de tudo o que se faz dentro dele.
|
|
59
|
+
*
|
|
60
|
+
* Importa mais do que parece: em telas separadas, este índice é o que faz o menu do jogador 2 editar as
|
|
61
|
+
* configurações DELE. Errar aqui não dá erro — dá a criança certa mexendo nos ajustes da criança errada,
|
|
62
|
+
* em silêncio.
|
|
63
|
+
*/
|
|
64
|
+
pauseActor: number;
|
|
65
|
+
/**
|
|
66
|
+
* Grava os quatro extras JUNTOS, que é como eles nascem.
|
|
67
|
+
*
|
|
68
|
+
* Um setter só, e não quatro, porque eles são um resultado só: `game/level-geometry.computeExtras()`
|
|
69
|
+
* devolve os quatro de uma vez, e gravá-los separadamente abriria uma janela em que o portão de um nível
|
|
70
|
+
* convive com os tiles de outro.
|
|
71
|
+
*/
|
|
72
|
+
setLevelExtras(x: ExtrasDoNivel<P>): void;
|
|
73
|
+
/** Abre ou fecha o portão. É o único dos cinco que muda DURANTE a rodada. */
|
|
74
|
+
setGateOpen(v: boolean): void;
|
|
75
|
+
/** Troca os sólidos de cadeirante (recalculados quando a geometria do nível muda). */
|
|
76
|
+
setWcSolid(s: ReadonlySet<string>): void;
|
|
77
|
+
setEnded(v: boolean): void;
|
|
78
|
+
setDecorSeed(s: number): void;
|
|
79
|
+
/**
|
|
80
|
+
* ⚠️ O CLAMP MORA AQUI, e é a razão de este setter existir.
|
|
81
|
+
*
|
|
82
|
+
* `grassDensity` é uma FRAÇÃO, e antes de `core/state` a proteção vivia no `window.__incl` — ou seja, só
|
|
83
|
+
* quem entrasse por ali era protegido. Qualquer outro caminho podia escrever 5 ou -1 e o cenário nascia
|
|
84
|
+
* errado sem nada reclamar. Um valor com faixa válida que depende de quem escreve é um valor sem faixa.
|
|
85
|
+
*/
|
|
86
|
+
setGrassDensity(v: number): void;
|
|
87
|
+
setSelVizPlayer(i: number): void;
|
|
88
|
+
setPauseActor(i: number): void;
|
|
89
|
+
/** Troca o número de jogadores e AVISA (ver `OpcoesDaRodada.aoTrocarJogadores`). */
|
|
90
|
+
setNumPlayers(n: number): void;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* O que a rodada precisa saber do mundo lá fora. Hoje é uma coisa só, e ela existe por um motivo concreto.
|
|
94
|
+
*
|
|
95
|
+
* `core/state.setNumPlayersValue` emitia `numPlayers` no barramento. Mover o campo para cá e simplesmente
|
|
96
|
+
* PARAR de emitir removeria em silêncio uma capacidade que a Fase C acabou de formalizar — e removê-la sem
|
|
97
|
+
* ninguém notar (o barramento ainda não tem assinante) é exatamente o tipo de perda que este repositório
|
|
98
|
+
* escreve ADR para não repetir.
|
|
99
|
+
*
|
|
100
|
+
* Então o aviso entra por INJEÇÃO, como o cabeçalho deste arquivo prometeu, e é OPCIONAL: uma rodada sem
|
|
101
|
+
* barramento funciona igual. Avisos são para painéis, e um painel ausente não é erro.
|
|
102
|
+
*/
|
|
103
|
+
export interface OpcoesDaRodada {
|
|
104
|
+
/** Chamado depois de `setNumPlayers`. Na raiz de composição é `(n) => emit('numPlayers', n)`. */
|
|
105
|
+
aoTrocarJogadores?: (n: number) => void;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Cria uma rodada. Sem I/O, sem estado de módulo, sem `emit` — quem quiser avisar alguém avisa por fora.
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ OS EVENTOS FICARAM DE FORA DE PROPÓSITO. `core/state` emitia `gateOpen` e `wcSolid`, e o barramento
|
|
111
|
+
* tinha ZERO assinantes (medido: 25 nomes de evento, 26 `emit()`, nenhum `on()`). Emitir para ninguém é
|
|
112
|
+
* cerimônia, e mantê-la aqui obrigaria a fábrica a depender do barramento — justo o que a Fase C vai
|
|
113
|
+
* refazer. Quando existir o primeiro assinante, ele entra por injeção, com o barramento já tipado.
|
|
114
|
+
*/
|
|
115
|
+
export declare function createRunState<P>(opcoes?: OpcoesDaRodada): RunState<P>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Cria uma rodada. Sem I/O, sem estado de módulo, sem `emit` — quem quiser avisar alguém avisa por fora.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ OS EVENTOS FICARAM DE FORA DE PROPÓSITO. `core/state` emitia `gateOpen` e `wcSolid`, e o barramento
|
|
6
|
+
* tinha ZERO assinantes (medido: 25 nomes de evento, 26 `emit()`, nenhum `on()`). Emitir para ninguém é
|
|
7
|
+
* cerimônia, e mantê-la aqui obrigaria a fábrica a depender do barramento — justo o que a Fase C vai
|
|
8
|
+
* refazer. Quando existir o primeiro assinante, ele entra por injeção, com o barramento já tipado.
|
|
9
|
+
*/
|
|
10
|
+
export function createRunState(opcoes = {}) {
|
|
11
|
+
const r = {
|
|
12
|
+
powerups: [],
|
|
13
|
+
gateTiles: new Set(),
|
|
14
|
+
gate: null,
|
|
15
|
+
gateOpen: true,
|
|
16
|
+
wcSolid: new Set(),
|
|
17
|
+
players: [],
|
|
18
|
+
numPlayers: 1,
|
|
19
|
+
setLevelExtras(x) {
|
|
20
|
+
r.powerups = x.powerups;
|
|
21
|
+
r.gateTiles = x.gateTiles;
|
|
22
|
+
r.gate = x.gate;
|
|
23
|
+
r.gateOpen = x.gateOpen;
|
|
24
|
+
},
|
|
25
|
+
setGateOpen(v) { r.gateOpen = v; },
|
|
26
|
+
setWcSolid(s) { r.wcSolid = s; },
|
|
27
|
+
ended: false,
|
|
28
|
+
decorSeed: 0,
|
|
29
|
+
grassDensity: 1,
|
|
30
|
+
selVizPlayer: 0,
|
|
31
|
+
pauseActor: 0,
|
|
32
|
+
setEnded(v) { r.ended = !!v; },
|
|
33
|
+
setDecorSeed(s) { r.decorSeed = s >>> 0; },
|
|
34
|
+
setGrassDensity(v) { r.grassDensity = Math.max(0, Math.min(1, +v || 0)); },
|
|
35
|
+
setSelVizPlayer(i) { r.selVizPlayer = i; },
|
|
36
|
+
setPauseActor(i) { r.pauseActor = i; },
|
|
37
|
+
setNumPlayers(n) { r.numPlayers = n; opcoes.aoTrocarJogadores?.(n); },
|
|
38
|
+
};
|
|
39
|
+
return r;
|
|
40
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Uma cena. Todos os ganchos são OPCIONAIS de propósito: uma tela de título que só desenha não deveria
|
|
4
|
+
* precisar declarar um `update` vazio, e um diálogo que só ouve não deveria precisar declarar um `draw`.
|
|
5
|
+
*/
|
|
6
|
+
export interface Scene {
|
|
7
|
+
/** Nome legível — para depuração e para os testes afirmarem a pilha por nome, não por identidade. */
|
|
8
|
+
readonly nome: string;
|
|
9
|
+
/** Chamado ao ENTRAR (push, ou ao reaparecer no topo por um pop). */
|
|
10
|
+
enter?(): void;
|
|
11
|
+
/** Chamado ao SAIR (pop, ou ao ser coberto por um push). */
|
|
12
|
+
exit?(): void;
|
|
13
|
+
/** Tempo. Só o topo recebe. */
|
|
14
|
+
update?(dt: number): void;
|
|
15
|
+
/** Desenho. Todas recebem, de baixo para cima. */
|
|
16
|
+
draw?(): void;
|
|
17
|
+
/** Entrada. Só o topo recebe. Devolva `true` se consumiu — `false`/nada deixa a tecla seguir. */
|
|
18
|
+
input?(intent: string): boolean | void;
|
|
19
|
+
}
|
|
20
|
+
export interface SceneStack {
|
|
21
|
+
/** Empilha `s` no topo. A anterior recebe `exit()` mas CONTINUA na pilha (e continua sendo desenhada). */
|
|
22
|
+
push(s: Scene): void;
|
|
23
|
+
/** Desempilha o topo e devolve-o (ou `null` se vazia). Quem reaparecer recebe `enter()`. */
|
|
24
|
+
pop(): Scene | null;
|
|
25
|
+
/** Troca o topo — `pop` seguido de `push`, numa chamada, porque é o que "ir para outra tela" quer dizer. */
|
|
26
|
+
replace(s: Scene): void;
|
|
27
|
+
/** A cena do topo, ou `null`. */
|
|
28
|
+
top(): Scene | null;
|
|
29
|
+
/** Os nomes, da base para o topo. Cópia: quem lê não muta a pilha por acidente. */
|
|
30
|
+
nomes(): string[];
|
|
31
|
+
update(dt: number): void;
|
|
32
|
+
draw(): void;
|
|
33
|
+
/** Devolve `true` se a cena do topo consumiu a intenção. */
|
|
34
|
+
input(intent: string): boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Cria uma pilha vazia.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ `enter`/`exit` são chamados em TRY/CATCH? NÃO — e é decisão, não esquecimento. Um erro dentro de
|
|
40
|
+
* `enter()` significa que a cena não montou; engolir isso deixaria a pilha num estado que ninguém declarou, e
|
|
41
|
+
* o sintoma apareceria três quadros depois, longe da causa. As chamadas de `update`/`draw`/`input` também
|
|
42
|
+
* propagam: a engine não é o lugar de decidir que o erro de um jogo não importa.
|
|
43
|
+
*/
|
|
44
|
+
export declare function criarPilha(): SceneStack;
|
|
45
|
+
/**
|
|
46
|
+
* O QUE A CASCA PRECISA SABER DA CENA — três booleanos, e nenhum nome de fase.
|
|
47
|
+
*
|
|
48
|
+
* Era `Phase = 'title' | 'playing' | 'paused'`, importado de `core/state`. O ADR-0030 registra ALARGAR essa
|
|
49
|
+
* união como NÃO-OPÇÃO, e a razão é curta: um segundo jogo continuaria amarrado ao NOSSO vocabulário — um
|
|
50
|
+
* jogo com mapa de fases ou tela de resultados teria de pedir uma constante nova à engine para existir.
|
|
51
|
+
*
|
|
52
|
+
* E o repositório já aprendeu isto uma vez, por evidência e não por gosto: o `consumer-quiz` precisou se
|
|
53
|
+
* declarar "pausado" para navegar os próprios menus, porque o `menu-nav` importava a fase. A correção de lá
|
|
54
|
+
* foi trocar `getPhase()` por `isNavigable()`, um BOOLEANO — e está registrada em `core/constants` como o
|
|
55
|
+
* erro a não repetir. Isto aqui é a mesma correção, aplicada à casca inteira.
|
|
56
|
+
*
|
|
57
|
+
* Quem NOMEIA as cenas é a raiz de composição, que é este jogo. Ela monta a pilha (`core/scenes`) e traduz o
|
|
58
|
+
* topo nestes três fatos. Uma quarta cena que a casca não conheça responde `false` nos três, e a projeção
|
|
59
|
+
* abaixo continua fazendo sentido: sem splash, sem menu de pausa, som mudo, foco no título.
|
|
60
|
+
*/
|
|
61
|
+
export interface FatosDaCena {
|
|
62
|
+
/** O topo é a tela de título (o splash cobre o mundo). */
|
|
63
|
+
telaDeTitulo: boolean;
|
|
64
|
+
/** O topo é o JOGO — o mundo recebe tempo, o som toca, o controle de toque pode aparecer. */
|
|
65
|
+
mundoRodando: boolean;
|
|
66
|
+
/** O topo é o menu de pausa. O jogo continua na pilha por baixo, e continua desenhado. */
|
|
67
|
+
menuDePausa: boolean;
|
|
68
|
+
}
|