Skip to content

Commit bece2a1

Browse files
Add replay mode
1 parent 1f86089 commit bece2a1

15 files changed

Lines changed: 396 additions & 21 deletions

.github/workflows/deploy.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ jobs:
6363
python tests/french_core_smoke.py
6464
node tests/core_smoke.js
6565
node tests/generated_browser_module_smoke.js
66+
node tests/generated_parity_smoke.mjs
67+
node tests/browser_generated_flow_smoke.mjs
6668
node tests/packed_runtime_smoke.mjs
6769
node tests/static_site_smoke.js
70+
node tests/rendering_smoke.js
6871
6972
- name: Verifier la structure GitHub Pages
7073
run: |

.github/workflows/monitor-multilingual.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,15 @@ jobs:
7878
node --check public/app.mjs
7979
node --check public/generated/automate_packed_runtime.mjs
8080
node --check public/generated/automate_packed/host_shim.mjs
81+
node --check public/generated/automate_universel/browser_module.mjs
8182
8283
- name: Valider le moteur et le WASM
8384
run: |
8485
python tests/french_core_smoke.py
8586
node tests/core_smoke.js
87+
node tests/generated_browser_module_smoke.js
88+
node tests/generated_parity_smoke.mjs
89+
node tests/browser_generated_flow_smoke.mjs
8690
node tests/packed_runtime_smoke.mjs
8791
node tests/static_site_smoke.js
8892

docs/architecture.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,21 @@ The intended stable boundary is the configuration object. The UI should not know
1313

1414
The browser layer is intentionally narrow: one module reads and writes form controls, delegates configuration assembly, evolution, canonical rule-table generation, genetic evaluation, and perturbation mutations to the core bridge, renders the resulting universe, and wires a static preset gallery. Presentation-only concerns such as tab systems, mirrored form state, and client-side gallery choreography are not part of the long-term architecture.
1515

16+
`scripts/build_wasm_bundle.py` now generates a browser-native rich module from the canonical French source:
17+
18+
```text
19+
public/generated/automate_universel/browser_module.mjs
20+
```
21+
22+
`public/app.mjs` loads that module into `window.AutomaginariumUniversVivant` before the UI starts. `public/automate-core.js` is therefore a generated-first compatibility bridge: it forwards rich JSON behavior to the generated `.multi` module when present, keeps JavaScript fallbacks for local/dev compatibility, and still calls the packed WASM numeric ABI for narrow scalar helpers.
23+
24+
The Exporter panel is also the first user-facing payoff for the living-universe manifest: it shows the generated semantic-core JSON/source and offers a Replay mini-canvas driven by the current universe timeline.
25+
1626
Advanced tools such as the genetic workbench and perturbation toolkit are now lazy browser features. They are loaded only when their panels are opened, so the baseline application path stays focused on the core Multilingual configuration and rendering loop.
1727

1828
Canonical JSON-array rule keys use forms such as `"[0,1,0]"`, rather than concatenated keys such as `"010"`. The browser adapter still accepts older Cellcosmos-style keys as a compatibility fallback.
1929

20-
The packed Multilingual/WASM bridge keeps the full French core as the canonical configuration and evolution model, while `src/automate_packed_wasm.multi` exposes a narrow numeric ABI for browser calls. The browser prefers `window.AutomaginariumPacked` when available and falls back to JavaScript only where rich JSON orchestration is still needed.
30+
The packed Multilingual/WASM bridge keeps the full French core as the canonical configuration and evolution model, while `src/automate_packed_wasm.multi` exposes a narrow numeric ABI for browser calls. The browser prefers `window.AutomaginariumPacked` for numeric primitives and `window.AutomaginariumUniversVivant` for rich configuration/universe behavior before falling back to JavaScript compatibility code.
2131

2232
Rule-table generation follows the same pattern: canonical helpers live in `src/automate_universel.multi`, narrow deterministic numeric helpers live in `src/automate_packed_wasm.multi`, and `public/automate-core.js` exposes the browser bridge so `public/app.mjs` does not duplicate the rule logic, form-to-config assembly, or configuration summaries.
2333

docs/packed-runtime.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Generated artifacts are written to:
1919
```text
2020
public/generated/automate_packed/
2121
public/generated/automate_universel.py
22+
public/generated/automate_universel/browser_module.mjs
2223
```
2324

2425
These generated files are intentionally ignored by Git and regenerated by GitHub Actions before deployment. The committed runtime loader is:
@@ -27,14 +28,14 @@ These generated files are intentionally ignored by Git and regenerated by GitHub
2728
public/generated/automate_packed_runtime.mjs
2829
```
2930

30-
The browser loads `public/generated/automate_packed_runtime.mjs`, which instantiates `module.wasm` and installs `window.AutomaginariumPacked`. `public/automate-core.js` then calls generated Multilingual/WASM exports for narrow numeric primitives such as Wolfram and totalistic outputs, while retaining JavaScript fallback behavior for rich JSON table orchestration.
31+
The browser loads `public/generated/automate_packed_runtime.mjs`, which instantiates `module.wasm` and installs `window.AutomaginariumPacked`. It also loads `public/generated/automate_universel/browser_module.mjs` into `window.AutomaginariumUniversVivant`. `public/automate-core.js` calls the rich generated module first for JSON-compatible behavior such as configuration summaries, universe generation, perturbations, genetic evaluation, and semantic-core export, then falls back to JavaScript compatibility code when the generated module is unavailable.
3132

3233
Regenerate artifacts with:
3334

3435
```powershell
3536
.\scripts\build-packed-runtime.ps1
3637
```
3738

38-
Current limitation: the full JSON/dictionary/list-rich core compiles to generated Python but does not yet produce a browser WASM bundle with the current Multilingual WAT backend. The narrow ABI module is the deliberate bridge until richer data lowering is available.
39+
Current limitation: the rich core is generated as browser-native JavaScript rather than a browser WASM bundle. The narrow ABI module remains the WASM bridge for scalar numeric helpers until richer WAT data lowering is available.
3940

40-
The generated rich core is exercised by `tests/french_core_smoke.py`, so CI verifies behavior that comes directly from `src/automate_universel.multi`.
41+
The generated rich core is exercised by `tests/french_core_smoke.py`, `tests/generated_browser_module_smoke.js`, `tests/generated_parity_smoke.mjs`, and `tests/browser_generated_flow_smoke.mjs`, so CI can verify behavior that comes directly from `src/automate_universel.multi` and that the served app points at the generated browser module.

docs/release-checklist.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@ Before publishing a release:
88
.\scripts\build-packed-runtime.ps1
99
```
1010

11+
`requirements-build.txt` currently installs `multilingualprogramming[wasm]`
12+
from the multilingual `main` branch because the published PyPI 0.8.0 wheel
13+
does not include `build-browser-module`. After the next multilingual release,
14+
repin this to the released version and rerun this checklist.
15+
1116
2. Run all local tests.
1217

1318
```powershell
1419
python tests\french_core_smoke.py
1520
node tests\core_smoke.js
1621
node tests\generated_browser_module_smoke.js
22+
node tests\generated_parity_smoke.mjs
23+
node tests\browser_generated_flow_smoke.mjs
1724
node tests\packed_runtime_smoke.mjs
1825
node tests\static_site_smoke.js
1926
```
@@ -32,8 +39,10 @@ Before publishing a release:
3239
- all presets load
3340
- Wolfram/random/symmetric/totalistic generation works
3441
- JSON import/export works
42+
- Exporter panel semantic-core JSON/source works with the generated module loaded
43+
- Replay controls in the Exporter panel step and play the current universe
3544
- PNG export works
36-
- browser console has no missing `module.wasm` or preset JSON errors
45+
- browser console has no failed dynamic import, missing `module.wasm`, or preset JSON errors
3746

3847
6. Push to GitHub and verify Actions.
3948

@@ -50,4 +59,4 @@ Before publishing a release:
5059
git status --short --ignored
5160
```
5261

53-
`public/generated/automate_packed/` and `public/generated/automate_universel.py` should remain ignored locally.
62+
`public/generated/automate_packed/`, `public/generated/automate_universel.py`, and `public/generated/automate_universel/` should remain ignored locally.

public/app.mjs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const PRESETS = [
1818
{ id: "symboles-jardin", label: "Symboles jardin" },
1919
];
2020

21-
const state = { config: null, universe: null, lastValidConfig: null };
21+
const state = { config: null, universe: null, manifest: null, lastValidConfig: null };
2222
const presetCache = new Map();
2323
const canvas = document.querySelector("#universe");
2424
const ctx = canvas.getContext("2d");
@@ -32,6 +32,12 @@ const universeLiveSummary = document.querySelector("#universe-live-summary");
3232
const transitionLiveSummary = document.querySelector("#transition-live-summary");
3333
const processLiveSummary = document.querySelector("#process-live-summary");
3434
const processJsonView = document.querySelector("#process-json");
35+
const replayCanvas = document.querySelector("#replay-canvas");
36+
const replayCtx = replayCanvas?.getContext("2d");
37+
const replayStatus = document.querySelector("#replay-status");
38+
const replayPlayButton = document.querySelector("#replay-play");
39+
const replayStepButton = document.querySelector("#replay-step");
40+
const replayResetButton = document.querySelector("#replay-reset");
3541
const syncIndicatorDot = document.querySelector("#sync-indicator-dot");
3642
const syncIndicatorText = document.querySelector("#sync-indicator-text");
3743
const heroSyncDot = document.querySelector("#hero-sync-dot");
@@ -79,6 +85,8 @@ const liveControlEntries = [
7985
[controls.ruleNumber, "input"],
8086
];
8187
let liveApplyTimer = null;
88+
let replayTimer = null;
89+
let replayGeneration = 0;
8290
const featureModules = {
8391
genetic: null,
8492
perturb: null,
@@ -531,9 +539,64 @@ function render() {
531539
}
532540
}
533541

542+
function manifestInitialRow(manifest) {
543+
const loci = manifest?.state?.loci;
544+
if (!Array.isArray(loci)) return null;
545+
return loci
546+
.slice()
547+
.sort((left, right) => Number(left.locus?.[0] || 0) - Number(right.locus?.[0] || 0))
548+
.map((locus) => locus.state?.symbol);
549+
}
550+
551+
function drawUniverseRow(targetCtx, targetCanvas, universe, rowIndex, manifest = null) {
552+
if (!targetCtx || !targetCanvas || !universe) return;
553+
const { configuration, lignes, sorties } = universe;
554+
const row = rowIndex === 0 ? (manifestInitialRow(manifest) || lignes?.[rowIndex] || []) : (lignes?.[rowIndex] || []);
555+
const rowSorties = sorties?.[rowIndex] || [];
556+
const width = Math.max(1, row.length);
557+
const cellWidth = targetCanvas.width / width;
558+
targetCtx.fillStyle = canvasBackgroundFor(configuration);
559+
targetCtx.fillRect(0, 0, targetCanvas.width, targetCanvas.height);
560+
row.forEach((value, x) => {
561+
const channels = rowSorties[x];
562+
const visualValue = channels && channels.length > 1 ? channels[1] : value;
563+
const bgValue = configuration.alphabet_sortie?.[0] ?? configuration.alphabet_entree[0];
564+
if (!configuration.rendu.afficher_zero && String(visualValue) === String(bgValue)) return;
565+
targetCtx.fillStyle = colorFor(value, configuration, channels, x, rowIndex, width, lignes.length);
566+
targetCtx.fillRect(Math.floor(x * cellWidth), 0, Math.ceil(cellWidth), targetCanvas.height);
567+
});
568+
}
569+
570+
function updateReplayStatus() {
571+
if (!replayStatus || !state.universe?.lignes) return;
572+
const steps = state.manifest?.schedule?.steps ?? Math.max(0, state.universe.lignes.length - 1);
573+
replayStatus.textContent = `Generation ${Math.min(replayGeneration + 1, state.universe.lignes.length)} / ${steps + 1}`;
574+
}
575+
576+
function renderReplay() {
577+
if (!replayCanvas || !state.universe?.lignes?.length) return;
578+
replayGeneration = Math.max(0, Math.min(replayGeneration, state.universe.lignes.length - 1));
579+
drawUniverseRow(replayCtx, replayCanvas, state.universe, replayGeneration, state.manifest);
580+
updateReplayStatus();
581+
}
582+
583+
function stopReplay() {
584+
if (replayTimer) window.clearInterval(replayTimer);
585+
replayTimer = null;
586+
if (replayPlayButton) replayPlayButton.textContent = "Lire";
587+
}
588+
589+
function stepReplay() {
590+
if (!state.universe?.lignes?.length) return;
591+
replayGeneration = (replayGeneration + 1) % state.universe.lignes.length;
592+
renderReplay();
593+
}
594+
534595
function describe(config) {
535596
const description = window.AutomaginariumCore.describeConfiguration(config);
536597
const semanticSummary = window.AutomaginariumCore.resumerUniversVivant(config);
598+
const manifest = window.AutomaginariumCore.construireUniversVivant(config);
599+
state.manifest = manifest;
537600
title.textContent = description.title;
538601
meta.textContent = description.metaText;
539602
tableView.innerHTML = description.ruleTableHtml;
@@ -549,7 +612,7 @@ function describe(config) {
549612
processLiveSummary.innerHTML = `<span class="live-summary-label">Comportement</span><strong>${semanticSummary.rule} | ${semanticSummary.topology} | ${semanticSummary.schedule}</strong>`;
550613
}
551614
if (processJsonView) {
552-
processJsonView.value = JSON.stringify(window.AutomaginariumCore.construireUniversVivant(config), null, 2);
615+
processJsonView.value = JSON.stringify(manifest, null, 2);
553616
}
554617
}
555618

@@ -627,6 +690,9 @@ function applyConfig(config, { source = "Configuration" } = {}) {
627690
}
628691
updateHudRule();
629692
render();
693+
replayGeneration = 0;
694+
stopReplay();
695+
renderReplay();
630696
setSyncState("ok", `${source} chargee`);
631697
return true;
632698
} catch (error) {
@@ -737,6 +803,23 @@ function applyRandomRule() {
737803

738804
document.querySelector("#random-rule").addEventListener("click", applyRandomRule);
739805
document.querySelector("#random-rule-canvas").addEventListener("click", applyRandomRule);
806+
replayPlayButton?.addEventListener("click", () => {
807+
if (replayTimer) {
808+
stopReplay();
809+
return;
810+
}
811+
replayPlayButton.textContent = "Pause";
812+
replayTimer = window.setInterval(stepReplay, 180);
813+
});
814+
replayStepButton?.addEventListener("click", () => {
815+
stopReplay();
816+
stepReplay();
817+
});
818+
replayResetButton?.addEventListener("click", () => {
819+
stopReplay();
820+
replayGeneration = 0;
821+
renderReplay();
822+
});
740823
document.querySelector("#apply-json").addEventListener("click", () => {
741824
try {
742825
applyConfig(JSON.parse(controls.json.value), { source: "JSON" });

public/automate-core.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/*
22
* Automaginarium browser bridge.
33
*
4-
* This adapter forwards calls to the compiled French Multilingual core.
5-
* All domain logic (cellular automata, rules, generation) is canonical in
6-
* src/automate_universel.multi. These functions are temporary fallbacks/mirrors
7-
* until the Multilingual→WASM compilation pipeline is live.
4+
* This adapter forwards calls to the generated French Multilingual browser
5+
* module when it is available. All domain logic (cellular automata, rules,
6+
* generation) is canonical in src/automate_universel.multi. The JavaScript
7+
* implementations below are compatibility fallbacks for local/dev paths where
8+
* generated artifacts have not been built yet.
89
*
910
* JavaScript serves only: canvas rendering, DOM, interface events.
1011
* All cellular automata logic belongs in src/.
@@ -36,7 +37,7 @@ function callPacked(name, args, fallback) {
3637

3738
// ============================================================================
3839
// TEMPORARY: These functions are duplicated from src/automate_universel.multi.
39-
// They are fallbacks until the ML is compiled to WASM/JS.
40+
// Generated browser JS is preferred; these stay as compatibility fallbacks.
4041
// DO NOT ADD NEW LOGIC HERE. Update src/automate_universel.multi instead.
4142
// ============================================================================
4243

@@ -300,12 +301,18 @@ function randomOutput(configuration, seed, neighborhoodIndex) {
300301
}
301302

302303
function tableAleatoire(configurationBrute, seed = Date.now() >>> 0) {
304+
if (window.AutomaginariumUniversVivant?.table_aleatoire) {
305+
return window.AutomaginariumUniversVivant.table_aleatoire(configurationBrute, seed);
306+
}
303307
const configuration = normaliserConfiguration(configurationBrute);
304308
const keys = toutesClesVoisinage(configuration.alphabet_entree, configuration.taille_voisinage);
305309
return Object.fromEntries(keys.map((key, index) => [key, randomOutput(configuration, seed, index)]));
306310
}
307311

308312
function tableSymetrique(configurationBrute, seed = Date.now() >>> 0) {
313+
if (window.AutomaginariumUniversVivant?.table_symetrique) {
314+
return window.AutomaginariumUniversVivant.table_symetrique(configurationBrute, seed);
315+
}
309316
const configuration = normaliserConfiguration(configurationBrute);
310317
const table = {};
311318
const keys = toutesClesVoisinage(configuration.alphabet_entree, configuration.taille_voisinage);
@@ -650,11 +657,20 @@ function metriquesUnivers(univers) {
650657
const lignes = univers?.lignes || [];
651658
const configuration = univers?.configuration || {};
652659
const valeurVide = configuration.alphabet_entree ? configuration.alphabet_entree[0] : 0;
653-
const totalCells = lignes.reduce((sum, ligne) => sum + ligne.length, 0);
654-
const liveCells = lignes.flat().filter((value) => celluleVivante(value, valeurVide)).length;
655-
const densite = totalCells > 0 ? liveCells / totalCells : 0;
656-
const entropie = densite > 0 && densite < 1 ? -(densite * Math.log2(densite) + (1 - densite) * Math.log2(1 - densite)) : 0;
660+
const toutesValeurs = lignes.flat().map(String);
661+
const totalCells = Math.max(1, toutesValeurs.length);
662+
const comptes = new Map();
663+
toutesValeurs.forEach((value) => {
664+
comptes.set(value, (comptes.get(value) || 0) + 1);
665+
});
666+
const entropie = [...comptes.values()].reduce((sum, count) => {
667+
const probability = count / totalCells;
668+
return probability > 0 ? sum - (probability * Math.log2(probability)) : sum;
669+
}, 0);
657670
const densitesParGeneration = lignes.map((ligne) => calculerDensiteLigne(ligne, valeurVide));
671+
const densite = densitesParGeneration.length > 0
672+
? densitesParGeneration.reduce((sum, value) => sum + value, 0) / densitesParGeneration.length
673+
: 0;
658674
const compacites = lignes.map((ligne) => calculerCompaciteLigne(ligne, valeurVide));
659675
const symetries = lignes.map(calculerSymetrieLigne);
660676
const compacite = compacites.length > 0 ? compacites.reduce((sum, value) => sum + value, 0) / compacites.length : 0;
@@ -701,7 +717,7 @@ function evaluerFitness(univers, poidsObj = {}) {
701717
"fitness_ponderee_scalaire",
702718
[fSym, fDen, fSta, oscillation, fCmp, fCro, ...poids],
703719
(fSym * poids[0] + fDen * poids[1] + fSta * poids[2] + oscillation * poids[3] + fCmp * poids[4] + fCro * poids[5]) /
704-
poids.reduce((a, b) => a + b, 1),
720+
Math.max(1, poids.reduce((a, b) => a + b, 0)),
705721
);
706722
const configuration = univers?.configuration || {};
707723
const scoresParCanal = Array.from(

public/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,21 @@ <h2>Exporter le comportement</h2>
278278
<span class="live-summary-label">Comportement</span>
279279
<strong>Univers exportable en attente</strong>
280280
</div>
281+
<div class="replay-panel" aria-label="Rejouer cet univers">
282+
<div class="panel-heading inline-heading compact-heading">
283+
<div>
284+
<p class="eyebrow">Replay</p>
285+
<h3>Rejouer cet univers</h3>
286+
</div>
287+
<p id="replay-status" class="meta">Generation 1 / 1</p>
288+
</div>
289+
<canvas id="replay-canvas" width="300" height="90" aria-label="Replay de l'univers courant"></canvas>
290+
<div class="button-row replay-controls">
291+
<button id="replay-play" type="button">Lire</button>
292+
<button id="replay-step" type="button">Pas</button>
293+
<button id="replay-reset" type="button">Revenir</button>
294+
</div>
295+
</div>
281296
<textarea id="process-json" spellcheck="false" readonly aria-label="Données exportables de l'univers vivant"></textarea>
282297
</div>
283298
</div>

0 commit comments

Comments
 (0)