The code moves into src/: index, server and the test harness keep the root

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-11 13:45:23 +02:00
1 parent c122745390
commit 1f636530b9
16 files changed
+16 -13

No files matched your search

+4 -2
View File
@@ -3,7 +3,8 @@
# stage COPYs the marker so BuildKit cannot skip this stage as unused. # stage COPYs the marker so BuildKit cannot skip this stage as unused.
FROM node:22-alpine AS test FROM node:22-alpine AS test
WORKDIR /app WORKDIR /app
COPY *.js /app/ COPY test.js /app/
COPY src /app/src
RUN node test.js && touch /tests-passed RUN node test.js && touch /tests-passed
FROM python:3.12-alpine FROM python:3.12-alpine
@@ -14,7 +15,8 @@ FROM python:3.12-alpine
# ponytail: switch to hashed filenames if traffic ever matters. # ponytail: switch to hashed filenames if traffic ever matters.
WORKDIR /app WORKDIR /app
COPY --from=test /tests-passed /tmp/tests-passed COPY --from=test /tests-passed /tmp/tests-passed
COPY index.html manifest.webmanifest *.js server.py /app/ COPY index.html manifest.webmanifest server.py /app/
COPY src /app/src
COPY assets /app/assets COPY assets /app/assets
# THE SCORES LIVE HERE AND THIS MUST BE A PERSISTENT VOLUME. Without one the json sits in the # THE SCORES LIVE HERE AND THIS MUST BE A PERSISTENT VOLUME. Without one the json sits in the
# container's writable layer and every deploy silently resets the leaderboard to empty — which # container's writable layer and every deploy silently resets the leaderboard to empty — which
+5 -4
View File
@@ -29,7 +29,8 @@ the primary target.
the Dockerfile. Oliver tests on his phone at splatter-tennis.wuwi.one ~30s after the Dockerfile. Oliver tests on his phone at splatter-tennis.wuwi.one ~30s after
a push. **Bump `main.js?v=N` in index.html every time** or phones keep the old build. a push. **Bump `main.js?v=N` in index.html every time** or phones keep the old build.
The Dockerfile must also COPY any new top-level file (it currently ships `index.html`, The Dockerfile must also COPY any new top-level file (it currently ships `index.html`,
`manifest.webmanifest`, `*.js`, `server.py`, `assets/`). `manifest.webmanifest`, `server.py`, `src/`, `assets/` — `test.js` only feeds the test stage and
is deliberately not in the served image).
- **The image is `python:3.12-alpine` running `server.py`, no longer `nginx:alpine`.** The world - **The image is `python:3.12-alpine` running `server.py`, no longer `nginx:alpine`.** The world
board needs one process that can WRITE, and nginx is not one; everything the nginx conf did here board needs one process that can WRITE, and nginx is not one; everything the nginx conf did here
(no-cache so a deploy lands on a plain reload, and `application/manifest+json`, without which (no-cache so a deploy lands on a plain reload, and `application/manifest+json`, without which
@@ -51,7 +52,7 @@ the primary target.
- **Oliver edits files himself** (tuning values, sometimes sprites). ALWAYS Read current file state - **Oliver edits files himself** (tuning values, sometimes sprites). ALWAYS Read current file state
before editing; never assume your last version is what is on disk. before editing; never assume your last version is what is on disk.
## Module map (all in repo root) ## Module map (game code in `src/`; `index.html`, `server.py` and `test.js` at the root)
`config.js` TUNE (live T panel) + structural constants + `UI` palette · `state.js` the `G` object + `config.js` TUNE (live T panel) + structural constants + `UI` palette · `state.js` the `G` object +
P/O/ball/score + `setupPoint`/`sendBoy` · `gameplay.js` ballistics/swings/serve/rules/AI/`update()` + P/O/ball/score + `setupPoint`/`sendBoy` · `gameplay.js` ballistics/swings/serve/rules/AI/`update()` +
transition + `splatPoint` + ball-boy routines · `beats.js` **the beat machinery: every mode transition + `splatPoint` + ball-boy routines · `beats.js` **the beat machinery: every mode
@@ -827,8 +828,8 @@ Clear the intro first: `document.getElementById('intro')?.remove()`.
own geometry could possibly reach. To check WHERE something lands, recompute the projection own geometry could possibly reach. To check WHERE something lands, recompute the projection
directly (`T.proj(...)`) and compare numbers; keep pixel-diffing for "did anything at all change". directly (`T.proj(...)`) and compare numbers; keep pixel-diffing for "did anything at all change".
- **The module-refresh list MUST include the VERSIONED entry point.** `index.html` loads - **The module-refresh list MUST include the VERSIONED entry point.** `index.html` loads
`main.js?v=N`, and `fetch('main.js', {cache:'reload'})` does not bust that URL — so main.js alone `src/main.js?v=N`, and `fetch('src/main.js', {cache:'reload'})` does not bust that URL — so main.js
stays stale through a reload while every other module is fresh. Read the src off the page: alone stays stale through a reload while every other module is fresh. Read the src off the page:
`document.querySelector('script[type=module]').getAttribute('src')` and fetch that too. Symptom: a `document.querySelector('script[type=module]').getAttribute('src')` and fetch that too. Symptom: a
change in main.js appears to do nothing at all, repeatably. change in main.js appears to do nothing at all, repeatably.
- **You MUST set `T.TUNE.autoAssist = 1.15` first** — the Browser pane reports `pointer: fine`, so - **You MUST set `T.TUNE.autoAssist = 1.15` first** — the Browser pane reports `pointer: fine`, so
+1 -1
View File
@@ -398,6 +398,6 @@
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="main.js?v=148"></script> <script type="module" src="src/main.js?v=149"></script>
</body> </body>
</html> </html>
View File
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
+1 -1
View File
@@ -278,7 +278,7 @@ intro.addEventListener('pointerdown', () => menuMusic());
// which build am I looking at? Push = deploy and the browser cannot see git, so the served file's // which build am I looking at? Push = deploy and the browser cannot see git, so the served file's
// own timestamp IS the version marker — it changes on every deploy, with nothing to remember to bump. // own timestamp IS the version marker — it changes on every deploy, with nothing to remember to bump.
// Match it against `git log -1 --format=%cd` when a phone looks suspiciously out of date. // Match it against `git log -1 --format=%cd` when a phone looks suspiciously out of date.
fetch('main.js', { method: 'HEAD' }) fetch('src/main.js', { method: 'HEAD' }) // page-relative: index.html sits at the root, the code in src/
.then(r => { const t = r.headers.get('last-modified'); if (!t) return; .then(r => { const t = r.headers.get('last-modified'); if (!t) return;
document.getElementById('build').textContent = 'BUILD ' + new Date(t).toLocaleString('en-GB').replace(',', ''); }) document.getElementById('build').textContent = 'BUILD ' + new Date(t).toLocaleString('en-GB').replace(',', ''); })
.catch(() => {}); // no header, no line — never a broken start screen .catch(() => {}); // no header, no line — never a broken start screen
View File
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
+5 -5
View File
@@ -60,11 +60,11 @@ globalThis.ResizeObserver = class { observe() {} disconnect() {} };
globalThis.getComputedStyle = () => ({ paddingTop: '0' }); globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ---------------------------------------------- // ---- game modules (real code) ----------------------------------------------
const { G, P, score, ball, setupPoint } = await import('./state.js'); const { G, P, score, ball, setupPoint } = await import('./src/state.js');
const { update, forceZombies, splatPoint, restartRun } = await import('./gameplay.js'); const { update, forceZombies, splatPoint, restartRun } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight } = await import('./beats.js'); const { startDuel, resolveInterlude, startBossFight, endBossFight } = await import('./src/beats.js');
const { zombies, spawnZombie } = await import('./zombies.js'); const { zombies, spawnZombie } = await import('./src/zombies.js');
const { TUNE } = await import('./config.js'); const { TUNE } = await import('./src/config.js');
const DT = 1 / 60; const DT = 1 / 60;
function freshSplatter() { function freshSplatter() {