Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/rules/pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
paths:
- "**/*.html"
- "sitemap.xml"
- "tools/deploy.sh"
- "scripts/deploy.sh"
---

# A new page, or a new top-level file or folder of the site

- Copy the `<head>` of an existing page, with its Content-Security-Policy line.
- Add the URL to `sitemap.xml` (`tools/check.py` fails without it).
- Add a new top-level file or folder to the `SITE` list in `tools/deploy.sh`, or it is not published.
- Add the URL to `sitemap.xml` (`scripts/check.py` fails without it).
- Add a new top-level file or folder to the `SITE` list in `scripts/deploy.sh`, or it is not published.
- Update "Contents" in the title block of `index.html` and the sheet numbers of its sections.
2 changes: 1 addition & 1 deletion .claude/skills/adding-a-thought/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Then:

Follow CLAUDE.md: serve the site (`python3 -m http.server 8000`), look at 1440, 390 and 320 px in both themes, no sideways scroll, no console errors. Click every switch and pick, and look at the narrow drawings. Commit on the session's branch and push.

Tim reviews a thought as a Claude artifact before it is published: run `python3 tools/preview-artifact.py thoughts/<slug> <scratchpad>/preview-<slug>` and publish its `index.html` with the Artifact tool, with every other printed file in `files`. After a change, build again and publish the same `file_path`, so the link stays the same. Comments on the artifact reach the session: answer them and change the page in the repository, never only in the bundle. `tools/ship.sh <pr>` merges and checks the live site when Tim asks for it.
Tim reviews a thought as a Claude artifact before it is published: run `python3 scripts/preview-artifact.py thoughts/<slug> <scratchpad>/preview-<slug>` and publish its `index.html` with the Artifact tool, with every other printed file in `files`. After a change, build again and publish the same `file_path`, so the link stays the same. Comments on the artifact reach the session: answer them and change the page in the repository, never only in the bundle. `scripts/ship.sh <pr>` merges and checks the live site when Tim asks for it.

## Revising a thought

Expand Down
8 changes: 4 additions & 4 deletions .claude/skills/handling-review-notes/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
---
name: handling-review-notes
description: Use when Tim mentions notes or feedback on the review site (the tab »Note«), when the session start says there are open review notes, or when you change tools/review/ or docker-compose.dev.yml.
description: Use when Tim mentions notes or feedback on the review site (the tab »Note«), when the session start says there are open review notes, or when you change scripts/review/ or docker-compose.dev.yml.
---

# Handling review notes

Tim writes notes on the review site: the tab »Note« at the right edge. The review site is this clone, served by `tools/review/devserver.py` in the container `t7d-review` (`docker-compose.dev.yml`, host name in `.env` as `REVIEW_HOST`). It shows the checked-out branch at once and loads the page again when a file changes. A note is a JSON file in `.review/notes/`.
Tim writes notes on the review site: the tab »Note« at the right edge. The review site is this clone, served by `scripts/review/devserver.py` in the container `t7d-review` (`docker-compose.dev.yml`, host name in `.env` as `REVIEW_HOST`). It shows the checked-out branch at once and loads the page again when a file changes. A note is a JSON file in `.review/notes/`.

1. The SessionStart hook (`.claude/settings.json`) prints the number of open notes in `.review/notes/`. Tell Tim how many are open, and list them with `ls .review/notes/*.json`.
2. Read the note: `note`, `path`, `viewport`, `theme`, `scroll`, `target` (`selector`, `text`, `rect`), `selection`, `branch`, `commit`.
3. Run `uv run --no-project --with playwright python3 tools/review/show.py <id>` (more ids in one call). Look at `.review/notes/<id>.png`. The red frame is the part that Tim pointed at.
3. Run `uv run --no-project --with playwright python3 scripts/review/show.py <id>` (more ids in one call). Look at `.review/notes/<id>.png`. The red frame is the part that Tim pointed at.
4. Do the work with the rules of this file: a branch, `DESIGN.md`, the look at three widths.
5. After the fix, run `show.py <id> --suffix after` and compare the two pictures.
6. Add an `"answer"` field to the note: what changed, and the commit. Move the note to `.review/notes/done/`. Tim sees the answer under »My notes« in the tab.

- Keep notes and their pictures out of git. `.review/` and `.env` are ignored.
- A change to `devserver.py` needs `docker restart t7d-review`. A change to `review.js` or `review.css` needs only a page load.
- The server adds only `connect-src 'self'` to the CSP of a page. Keep it that way, so that the review site keeps the rules of the real site.
- Tests: `python3 -m unittest tools/review/test_devserver.py`.
- Tests: `python3 -m unittest scripts/review/test_devserver.py`.
14 changes: 7 additions & 7 deletions .claude/skills/updating-game-timeline/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ The product page of a game has a section "How it changed": one entry per merged

| Game | Stages | Clips | Page |
| --- | --- | --- | --- |
| Kleinbahn | `tools/reels/timeline-kleinbahn.mjs`, filmed by `timeline.mjs` | `assets/projects/timeline/kleinbahn-<id>.mp4` and `.jpg` | `projects/kleinbahn/index.html`, section `#changes` |
| Wortzauber | `tools/reels/timeline-wortzauber.mjs`, filmed by `timeline-wz.mjs` | `assets/projects/timeline/wortzauber-<id>.mp4` and `.jpg` | `projects/wortzauber/index.html`, section `#changes` |
| Kleinbahn | `scripts/reels/timeline-kleinbahn.mjs`, filmed by `timeline.mjs` | `assets/projects/timeline/kleinbahn-<id>.mp4` and `.jpg` | `projects/kleinbahn/index.html`, section `#changes` |
| Wortzauber | `scripts/reels/timeline-wortzauber.mjs`, filmed by `timeline-wz.mjs` | `assets/projects/timeline/wortzauber-<id>.mp4` and `.jpg` | `projects/wortzauber/index.html`, section `#changes` |

## Rules
- **One clip per PR, the coolest change.** Pick the change a player sees and enjoys most: something that moves on the island beats a panel, a panel beats a fix. List the rest of the PR in one short line each.
Expand All @@ -27,22 +27,22 @@ Do not read the code of every PR. Work from the git log.
3. Read each new PR's commit messages only: `git -C ../tiny-train log --format='%B' <merge>^1..<merge>^2`. They name every change. Decide: entry or "Not shown", and which change to film.
4. Find the names a shot needs with one `git grep` at that ref (`git -C ../tiny-train grep -n 'function aStartFlight' <ref> -- src/game`), not by reading files.
5. Add one stage per new entry to the stages file (fields: see the comment at its top). Copy the closest existing stage: an event (`setup` + `until` + `cam: null` to follow), a camera move (`cam`), a UI moment (`ui: true` and an `each` that presses a button), shared play (`net: 'host'` or `'guest'`).
6. Test only the new stages, three frames each: `node tools/reels/timeline.mjs ../tiny-train --test --only pr14`. Join the frames into one sheet and read it (`ffmpeg -i <work>/tl/kb-test/pr14/%04d.jpg -vf tile=3x1 sheet.jpg`).
7. Film them: `node tools/reels/timeline.mjs ../tiny-train --only pr14` (about 1 to 3 s a frame; a stage of 96 frames takes 2 to 5 min).
8. Cut: `python3 tools/reels/build.py timeline` writes the clips and posters of every filmed stage into `assets/projects/timeline/` (640x360, about 100 to 400 KB each).
6. Test only the new stages, three frames each: `node scripts/reels/timeline.mjs ../tiny-train --test --only pr14`. Join the frames into one sheet and read it (`ffmpeg -i <work>/tl/kb-test/pr14/%04d.jpg -vf tile=3x1 sheet.jpg`).
7. Film them: `node scripts/reels/timeline.mjs ../tiny-train --only pr14` (about 1 to 3 s a frame; a stage of 96 frames takes 2 to 5 min).
8. Cut: `python3 scripts/reels/build.py timeline` writes the clips and posters of every filmed stage into `assets/projects/timeline/` (640x360, about 100 to 400 KB each).
9. Add the entry to `projects/kleinbahn/index.html`: copy the first `<li>` of `.tl` and put the new one above it (newest first). Date, PR number, title, the sentence on the clip, the list of the other changes.
10. Serve the site and check `/projects/kleinbahn/#changes` at 1440, 390 and 320 px, light and dark. Commit the stages file, the clips and the page.

## The timeline script
- `tools/reels/timeline.mjs` checks out each stage's `ref` as a git worktree in `<work>/tl/wt/`, links `node_modules` from the game repo, opens that version through its own `tools/page.mjs` (virtual clock, seeded random), films the frames and removes the worktree. `<work>` is `$REELS_WORK` or `.reels/`.
- `scripts/reels/timeline.mjs` checks out each stage's `ref` as a git worktree in `<work>/tl/wt/`, links `node_modules` from the game repo, opens that version through its own `tools/page.mjs` (virtual clock, seeded random), films the frames and removes the worktree. `<work>` is `$REELS_WORK` or `.reels/`.
- One frame is one `__KB.step(1, 1/24)`. A canvas stage reads the WebGL canvas; a `ui` stage takes a page screenshot, so the controls and toasts show.
- `net: 'host'`: the game hosts and `window.__peer` is a guest (`__peer.world` its island, `__peer.sync` its engine). Change `__peer.world.layers` and the host gets it next frame; `__peer.sync.presence({...})` moves its pin.
- `net: 'guest'`: the game joins `__peer`, a host whose island is the game's own island without trees. `__peer.sync.event(k, id, e, d)` starts a host event (eruption, launch, fire); `__peer.sync.leave()` closes the room.
- A name a stage uses must exist at that ref. If a stage prints `FAILED` or `ERR`, `git grep` the name at the ref.
- Node 22 runs the script. Run nothing heavy next to it, and never next to a Wortzauber recording.

## Wortzauber
- The game runs in real time, so each stage is a screen recording: `node tools/reels/timeline-wz.mjs ../wortzauber [--dry] [--only pr65]`. `--dry` saves one still per stage into `<work>/tl/wz-dry/`; read them as one sheet before you record. All stages take about 5 minutes.
- The game runs in real time, so each stage is a screen recording: `node scripts/reels/timeline-wz.mjs ../wortzauber [--dry] [--only pr65]`. `--dry` saves one still per stage into `<work>/tl/wz-dry/`; read them as one sheet before you record. All stages take about 5 minutes.
- Each worktree is served with `python3 -m http.server` on its own port. That is fine here: every stage opens a fresh browser, so nothing is cached. (For normal work on the game, use its `devserver.py`.)
- The cheapest stage uses a layout-check scene of the game at that ref (`scene: 'castle'`): a feature PR often added its own scene, see `.claude/skills/checking-device-layouts/scenes/` in the game and `git log --diff-filter=A` on it. Else a short `setup` with `WZ.jump(...)` (from PR #31 on), `WZ.useWord(...)`, `Wall`, `Castle`, `Together.testLink()`; an `act` films the action itself.
- The clips are silent. A PR that only adds sound goes into the "Not shown" line, and so does a change that opens only deep in a level.
Expand Down
20 changes: 10 additions & 10 deletions .claude/skills/updating-product-reels/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: Use when Kleinbahn (tiny-train) or Wortzauber changed and the reel,
# Updating the product reels and pictures

## Overview
Each game has a product page under `projects/<game>/` with a reel, a poster, four stills and a phone frame that loads the live demo. Everything visual comes from the real game, filmed headless by the scripts in `tools/reels/`. Nothing is drawn or generated.
Each game has a product page under `projects/<game>/` with a reel, a poster, four stills and a phone frame that loads the live demo. Everything visual comes from the real game, filmed headless by the scripts in `scripts/reels/`. Nothing is drawn or generated.

| Game | Repo | Product page | Live demo in the phone frame |
| --- | --- | --- | --- |
Expand All @@ -18,7 +18,7 @@ Both game repos are private. Clone them next to this repo first (in a cloud sess
## What needs a new capture
- The live demo in the phone frame updates by itself when the game is published to beacon.host. Only its poster picture is from here.
- A visual change of the game (art, UI, a scene in a shot): film that game again, then build and publish.
- Only a caption or a title: change the text in `tools/reels/overlays.mjs`, then run overlays, build and publish. No filming.
- Only a caption or a title: change the text in `scripts/reels/overlays.mjs`, then run overlays, build and publish. No filming.
- Only the page text: edit `projects/<game>/index.html`. The text must stay true to the game: read its `README.md` before you claim a feature.
- Wording, on the pages and in the captions: plain and down to earth. A caption names what the shot shows ("Trains", "Shuttle launch"). No slogans and no superlatives.

Expand All @@ -27,13 +27,13 @@ All of them write to a work folder: `$REELS_WORK`, or `.reels/` in this repo (gi

| Step | Command | Time |
| --- | --- | --- |
| Kleinbahn preview | `node tools/reels/kleinbahn-shots.mjs ../tiny-train --test` | 1 min |
| Kleinbahn frames | `node tools/reels/kleinbahn-shots.mjs ../tiny-train [--only b-train,c-town]` | 25 to 40 min for all nine shots |
| Wortzauber dry run | `node tools/reels/wortzauber-takes.mjs ../wortzauber` | 3 min |
| Wortzauber recording | `node tools/reels/wortzauber-takes.mjs ../wortzauber --rec [--only meadow]` | 3 min |
| Titles and captions | `node tools/reels/overlays.mjs ../wortzauber` | seconds |
| Cut a reel | `python3 tools/reels/build.py kb` and `python3 tools/reels/build.py wz ../wortzauber` | 1 to 2 min each |
| Put it on the site | `python3 tools/reels/build.py publish` | 1 min |
| Kleinbahn preview | `node scripts/reels/kleinbahn-shots.mjs ../tiny-train --test` | 1 min |
| Kleinbahn frames | `node scripts/reels/kleinbahn-shots.mjs ../tiny-train [--only b-train,c-town]` | 25 to 40 min for all nine shots |
| Wortzauber dry run | `node scripts/reels/wortzauber-takes.mjs ../wortzauber` | 3 min |
| Wortzauber recording | `node scripts/reels/wortzauber-takes.mjs ../wortzauber --rec [--only meadow]` | 3 min |
| Titles and captions | `node scripts/reels/overlays.mjs ../wortzauber` | seconds |
| Cut a reel | `python3 scripts/reels/build.py kb` and `python3 scripts/reels/build.py wz ../wortzauber` | 1 to 2 min each |
| Put it on the site | `python3 scripts/reels/build.py publish` | 1 min |

`publish` writes `assets/projects/<game>-reel.mp4` (web size), `-poster.jpg`, `-1.jpg` to `-4.jpg`, `-loop.webp` (the gameplay loop of the game box on the front page) and `wortzauber-phone.jpg`. `kleinbahn-phone.jpg` is a separate step (below).

Expand All @@ -60,7 +60,7 @@ Before a run: start the dev server in `../wortzauber` with `python3 devserver.py
- Rules of the game repo that hold here too: do not generate voice, music or images, and do not read the ElevenLabs key. The reels are silent: the site plays every video as a muted loop.

## Build and check
1. `overlays.mjs` renders every title, caption and end card as a transparent 1280x720 PNG, in the fonts of each game (Fredoka and Nunito from `tools/reels/fonts/`, Andika from the Wortzauber repo).
1. `overlays.mjs` renders every title, caption and end card as a transparent 1280x720 PNG, in the fonts of each game (Fredoka and Nunito from `scripts/reels/fonts/`, Andika from the Wortzauber repo).
2. `build.py kb` and `build.py wz` cut the reels. The timeline of a reel is the `parts` list in `kb()` and `wz()`: one `seg` or `clip` per cut, with its overlays and their fade times.
3. Look at the result before you publish. Make a contact sheet and read it:
`ffmpeg -i <work>/kleinbahn-reel.mp4 -vf "fps=16/39,scale=320:180,tile=4x4" -frames:v 1 sheet.jpg` (16 pictures over a 39 s reel).
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: CSP rules and sitemap
run: python3 tools/check.py
run: python3 scripts/check.py
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Tests of the tool page generator
run: |
pip install markdown==3.11.3
python3 -m unittest scripts/test_gen_tools.py
10 changes: 8 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Check the pages
run: python3 tools/check.py
run: python3 scripts/check.py
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install the Markdown renderer for the tool pages
run: pip install markdown==3.11.3
- name: Publish the site files to gh-pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: github-actions[bot]
GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
run: tools/deploy.sh
run: scripts/deploy.sh
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
.reels/
__pycache__/
# made by tools/gen-feeds.py at deploy time
# made by scripts/gen-feeds.py at deploy time
/llms.txt
/thoughts/feed.xml
/blog/feed.xml
.review/
.env
.preview/

# local drafts: never committed, never published (the repo is public)
/drafts/
Loading
Loading