diff --git a/Dockerfile b/Dockerfile
index 8249428..e328e2b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,8 @@
# stage COPYs the marker so BuildKit cannot skip this stage as unused.
FROM node:22-alpine AS test
WORKDIR /app
-COPY *.js /app/
+COPY test.js /app/
+COPY src /app/src
RUN node test.js && touch /tests-passed
FROM python:3.12-alpine
@@ -14,7 +15,8 @@ FROM python:3.12-alpine
# ponytail: switch to hashed filenames if traffic ever matters.
WORKDIR /app
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
# 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
diff --git a/PROJECT.md b/PROJECT.md
index e863dd5..2ad5fe2 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -29,7 +29,8 @@ the primary target.
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.
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
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
@@ -51,7 +52,7 @@ the primary target.
- **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.
-## 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 +
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
@@ -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
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
- `main.js?v=N`, and `fetch('main.js', {cache:'reload'})` does not bust that URL — so main.js alone
- stays stale through a reload while every other module is fresh. Read the src off the page:
+ `src/main.js?v=N`, and `fetch('src/main.js', {cache:'reload'})` does not bust that URL — so main.js
+ 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
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
diff --git a/index.html b/index.html
index 6536780..ec8dabb 100644
--- a/index.html
+++ b/index.html
@@ -398,6 +398,6 @@
-
+