Authoritative multiplayer recipe-trading MVP with a Node TypeScript server and a GDScript-only Godot client.
DESCRIPTION.md is the canonical product/design document. PLAN.md tracks implementation phases and known remaining work.
Implemented:
- Server-owned tables, participants, ingredients, vouchers, recipes, platter, offers, dishes, transaction history, timers, bots, filtered snapshots, and WebSocket updates.
- Godot client for
Play Offline,Play Online, joining online tables, taking over prefilled bot seats, pausing/resuming, starting, watching automatic opening offerings, swapping with the platter, creating/responding to offers, preparing dishes, viewing dish totals/transaction history, ending the game, and eating. - Online host-controlled seats with filtered per-seat views and explicit acting-seat intents.
- Server-enforced round-robin turns. Each active cook keeps the turn until they pass or use
Redeem / Pass. - End-game stats distinguish player turns, fractional cycles, successful interactions, Common Basket swaps, direct exchanges, redemptions, settlement swaps, food-piece settlement swaps, and bites eaten.
- Offline pass-and-play rules runtime for one-device local seats and bots, using the same snapshot/intent UI path as online play.
- Deterministic tests for the core game rules.
Not implemented yet:
- Polished production UI/art pass.
- Node.js
>=24 - npm
- Godot
4.5.1available asgodot4
cd /home/wor/src/recipes
npm installOne-command local server:
/home/wor/src/recipes/start-server.shOr from the repo root:
./start-server.shDevelopment watch mode:
cd /home/wor/src/recipes
npm run devCompiled server:
cd /home/wor/src/recipes
npm run build
HOST=127.0.0.1 PORT=3000 node server/dist/index.jsPublic remote server, usually behind nginx and HTTPS:
cd /opt/recipes
npm ci
HOST=127.0.0.1 PORT=3000 ./start-server.shFor production, run the Node process on localhost through a reverse proxy instead of exposing port 3000 directly. Example files are provided at:
deploy/systemd/recipes-server.service
deploy/nginx/recipes-server.example.conf
The production server hostname currently expected by the client server list is:
https://recipes-server.grassecon.org
Point that DNS name to the remote server once its IP is known, or update client/data/servers.json before exporting a release build if the final hostname changes.
Health check:
curl -s http://127.0.0.1:3000/healthCreate a table over HTTP:
curl -s -X POST http://127.0.0.1:3000/tables \
-H 'Content-Type: application/json' \
-d '{"hostName":"","seed":"demo"}'Gameplay intents use WebSocket:
ws://127.0.0.1:3000/tables/:code/socket?seatToken=...
Start the server first, then run:
cd /home/wor/src/recipes
godot4 --path clientManual smoke path:
- Leave
Name (auto)blank or enter a custom name. - Click
Play Offlinefor pass-and-play orPlay Onlineto create a hosted table. - The table starts with 8 seats: you plus 7 bots.
- Use the seat grid to edit seat names and switch available bot seats between
PlayerandBotbefore start. - Online tables are public by default while joinable. The host can toggle
Public Table/Private Tablein the lobby; public joinable tables appear in the server browser below the create/join buttons. - Click
Start Cooking. - Watch the automatic opening offerings fill the Common Basket.
- Test host pause/resume, manual player-to-bot conversion, platter swaps, offers,
Redeem / Passwith automatic dish preparation, dish totals, transaction history,End Game, and dish bites.
To test multiple humans locally on one desktop, run each Godot client with a different local profile. Each profile gets its own saved online seat, so one client can reconnect as the host while the others join as separate players:
cd /home/wor/src/recipes
godot4 --path client -- --profile host
godot4 --path client -- --profile p2
godot4 --path client -- --profile p3
godot4 --path client -- --profile p4Create the table in the host window, then enter the same invite code in the p2, p3, and p4 windows, or choose the public table from the online server browser if the host leaves it public. If you run multiple clients without separate profiles, they intentionally share the same saved user:// session and may show Reconnect Seat instead of Join Table.
cd /home/wor/src/recipes
npm run typecheck
npm run test:run
npm run test:offline
npm run test:visual
godot4 --headless --log-file /tmp/recipes-godot-headless.log --path client --quit-after 5Full local regression pass:
npm run test:allVitest watch mode:
npm testRun an 8-seat game through the real HTTP/WebSocket API:
cd /home/wor/src/recipes
npm run simulate:game -- --players=8 --dish-goal=3 --profile=local --turn-mode=round_robin
npm run simulate:game -- --players=8 --dish-goal=3 --profile=disconnect
npm run simulate:game -- --players=8 --dish-goal=3 --profile=jitter
npm run simulate:game -- --players=8 --dish-goal=3 --profile=bad
npm run simulate:game -- --games=3 --player-min=8 --player-max=8 --concurrency=2 --dish-goal=3 --profile=local --suite-max-duration-ms=300000Profiles:
local: no artificial network delay.jitter: 100-800 ms delay before intents.disconnect: periodic socket close/reconnect.bad: jitter, reconnects, and intentional stale/invalid actions.
Reports are written to:
tmp/simulations/
Single-game reports include full per-client frame arrays for debugging. Multi-game suite reports keep compact per-game rows and aggregate frame/byte metrics so 100+ table runs stay readable.
Round-robin is the only supported turn mode. The simulator accepts --turn-mode=round_robin for compatibility with older scripts.
Use --suite-max-duration-ms to bound the entire multi-game run; per-game --max-duration-ms still applies inside the suite.
The recipe catalog generator creates one committed 8-player recipe set using the generalized ingredients Cheese, Flour, Herbs, Vegetables, Rice, Beans, Spices, and Eggs. It creates three short, unique, real-dish-inspired recipes per ingredient: one initial recipe and two followups, for 24 recipes total.
The live server uses the same committed ingredient set when it assigns ingredients during a game. Runtime tables do not randomly choose ingredients. In docs/recipes-catalog.ods, the Player Count Ingredient Sets sheet shows the committed set, and the recipe, requirement, and validation sheets show the playable catalog.
Rules enforced by tests:
- each recipe includes the owner's ingredient,
- total required quantity is exactly 6,
- distinct ingredient count is 4, 5, or 6,
- quantity shape is
2+2+1+1,2+1+1+1+1, or1+1+1+1+1+1, - every required ingredient belongs to the active ingredient set,
- recipe names are unique within the 8-player set,
- ingredient/quantity requirement signatures are unique within the 8-player set,
- every recipe uses its committed real-dish-inspired name directly,
- every ingredient has image metadata for cards, recipe slots, and inventory.
Generate the spreadsheet:
cd /home/wor/src/recipes
npm run generate:recipesOutput:
docs/recipes-catalog.ods
Export presets are configured under client/export_presets.cfg.
Web export:
cd /home/wor/src/recipes
npm run export:webBuild and serve locally in one command:
cd /home/wor/src/recipes
./start-web.shThe Web client export writes to client/web/, copies CNAME, and is intended to be served at:
https://recipes.grassecon.org
Local Web-client smoke server:
cd /home/wor/src/recipes
npm run serve:web -- --host 127.0.0.1 --port 8081The local smoke server sends no-cache headers. If an older browser session still
shows stale UI, clear the old Godot service worker once in DevTools:
Application -> Service Workers -> Unregister, then
Application -> Storage -> Clear site data.
Remote static Web hosting:
cd /opt/recipes
npm ci
npm run export:web
sudo rsync -a --delete client/web/ /var/www/recipes/Serve /var/www/recipes with HTTPS and the Godot Web headers shown in:
deploy/nginx/recipes-web.example.conf
The root CNAME and generated client/web/CNAME both use recipes.grassecon.org. The Web build must be served over HTTPS for normal browser security rules; localhost testing can use scripts/serve-web.py.
The production deployment has two independent services:
- Static Godot Web client:
https://recipes.grassecon.org - Authoritative game server:
https://recipes-server.grassecon.org
Before deployment, decide whether those hostnames are final. If the server hostname changes, update client/data/servers.json before running npm run export:web; the exported Web client embeds that server list in client/web/index.pck.
Remote machine prerequisites:
sudo apt update
sudo apt install -y git nginx certbot python3-certbot-nginxInstall Node.js >=24 and npm on the remote host. Use your preferred Node package source or version manager, then verify:
node --version
npm --versionInstall Godot 4.5.1 as godot4 only if the remote host will build the Web client. If CI or a developer machine exports client/web/, the remote host only needs nginx plus the built files.
Clone and build:
sudo mkdir -p /opt/recipes
sudo chown "$USER":"$USER" /opt/recipes
git clone https://github.com/grassrootseconomics/recipes.git /opt/recipes
cd /opt/recipes
npm ci
npm run buildCreate a system user for the server process:
sudo useradd --system --home /opt/recipes --shell /usr/sbin/nologin recipes || true
sudo chown -R recipes:recipes /opt/recipesInstall the server service:
sudo cp deploy/systemd/recipes-server.service /etc/systemd/system/recipes-server.service
sudo systemctl daemon-reload
sudo systemctl enable --now recipes-server
sudo systemctl status recipes-serverThe service runs Node on 127.0.0.1:3000. Do not expose port 3000 publicly in production; nginx should terminate HTTPS and proxy HTTP/WebSocket traffic to localhost.
Configure DNS:
recipes.grassecon.org A/AAAA -> remote server IP
recipes-server.grassecon.org A/AAAA -> remote server IP
Install nginx configs and certificates:
sudo cp deploy/nginx/recipes-server.example.conf /etc/nginx/sites-available/recipes-server
sudo cp deploy/nginx/recipes-web.example.conf /etc/nginx/sites-available/recipes-web
sudo ln -sf /etc/nginx/sites-available/recipes-server /etc/nginx/sites-enabled/recipes-server
sudo ln -sf /etc/nginx/sites-available/recipes-web /etc/nginx/sites-enabled/recipes-web
sudo nginx -t
sudo certbot --nginx -d recipes-server.grassecon.org
sudo certbot --nginx -d recipes.grassecon.org
sudo systemctl reload nginxThe web nginx config must keep these headers for Godot Web:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Build and publish the Web client:
cd /opt/recipes
sudo chown -R "$USER":"$USER" /opt/recipes
npm run export:web
sudo mkdir -p /var/www/recipes
sudo rsync -a --delete client/web/ /var/www/recipes/
sudo chown -R www-data:www-data /var/www/recipes
sudo systemctl reload nginxIf client/data/servers.json, Godot scripts, art, or recipes change, rebuild the Web export and copy client/web/ again. The current export does not intentionally use a PWA service worker; if a tester previously loaded an older build, they may need to unregister the old service worker and clear site data once.
Remote smoke checks:
curl -s https://recipes-server.grassecon.org/health
curl -I https://recipes.grassecon.org/
sudo journalctl -u recipes-server -n 100 --no-pagerManual online smoke:
- Open
https://recipes.grassecon.org. - Select
Grassroots Recipes Server. - Create a public table.
- Open another browser/device and verify the public table appears.
- Join, rename the joined seat, start cooking, and verify both clients receive live updates.
Common deployment adjustments:
- Change production domains in
CNAME,client/web/CNAME,deploy/nginx/*.conf, andclient/data/servers.jsonifrecipes.grassecon.org/recipes-server.grassecon.orgare not final. - Change
WorkingDirectory,ExecStart,User, andGroupindeploy/systemd/recipes-server.serviceif the repo is not deployed at/opt/recipes. - Change
/var/www/recipesindeploy/nginx/recipes-web.example.confif static files are served from a different directory. - Keep WebSocket upgrade headers in
deploy/nginx/recipes-server.example.conf; online gameplay depends on them. - Open firewall ports
80and443; keep3000private to localhost.
Android test APK:
cd /home/wor/src/recipes
mkdir -p client/build/android
godot4 --headless --path client --export-debug Android-TestAPK build/android/recipes-test.apkGodot export templates must be installed for export commands to work.
- Keep the server authoritative. The client sends intents only.
- Keep the Godot client GDScript-only.
- Do not add accounts, wallets, blockchain, borrowing, free chat, or a global leaderboard unless
DESCRIPTION.mdchanges first. - Commit Godot source resource metadata such as
.importand.uidfiles; only generated build/editor output is ignored. - Generated folders such as
node_modules/,server/dist/,client/.godot/,client/build/, andclient/web/are ignored.
If this directory does not have a valid Git repository yet:
cd /home/wor/src/recipes
rmdir .git
git init
git add .
git commit -m "Initial Recipes MVP"
git branch -M main
git remote add origin <repo-url>
git push -u origin mainIf .git is already a valid repository:
cd /home/wor/src/recipes
git status --short
git add .
git commit -m "Initial Recipes MVP"
git remote add origin <repo-url>
git push -u origin main