Please help me install and set up "Merlot" from veered.org on my own computer or server. What it is: A real-time strategy game for 1 to 12 players. Project page: https://veered.org/merlot/ Source code download (zip): https://veered.org/merlot/merlot.zip How I'd like you to help: 1. First ask me what you need to know: my operating system and version, my desktop environment if it matters, whether I'm comfortable with the terminal, and any accounts or hardware this project needs. 2. Then take me through it one step at a time. For each command, say briefly what it does, and wait for me to paste back the result before moving on. 3. Before any step that changes system settings, uses sudo, or edits an existing configuration file, tell me, and have me make a backup first. 4. If something fails, help me work out why. If the instructions below don't fit my system, say so rather than guessing. 5. At the end, show me how to check that it works, and how to uninstall it. The project is shared as-is by its author, with no warranty. If we find a problem in the project itself, please help me write a short, clear note I can send to support@veered.org. Here is the project's documentation (README files from the download): ===== README.md ===== # Merlot: Real-Time Multiplayer Tank Game *Why Merlot? Named for the wine red of its steel winery.* **Play it now: [merlot.veered.org](https://merlot.veered.org)** · more games at [veered.org](https://veered.org) ![Merlot](docs/screenshot.jpg) Merlot is a free real-time browser strategy game for 1 to 12 players. Two teams of wine-bottle tanks fight across a steel winery. Nothing to install, no account needed: open the link, pick a seat, and any seat nobody takes is filled by a bot. ## Two houses, one cellar Every match is blue against orange. Each side has a base to defend, guard towers that shoot back, and waves of little minions that march out on their own. ![The two houses, blue and orange](docs/houses.jpg) ## Pick a hull, then keep it alive Before the fighting starts you draft a hull — the body your bottle drives. One is fast and fragile, one is a brawler, one keeps the others alive. They all handle differently, and the draft is half the game. ![The hulls you can draft](docs/hulls.jpg) Kills earn you abilities and, eventually, a prestige: a rank badge, a faster reload, and a boost to every minion your side sends out afterwards. Last Call is the closing window at the end of a match, when a single good push can still take it. ![Blue and orange hulls on the winery floor](docs/fleet.jpg) ## Please test before relying on it This is shared as-is, with no warranty. It works on my own computers, but your system, settings and software versions may differ, so please try it in a safe setting first. If something doesn't work, you can ask Claude (or another AI coding assistant) to look into it, and I'd appreciate hearing what you found and how you fixed it. You are also welcome to just let me know at support@veered.org, and I'll look into it. ## Put your own copy on the internet You do not have to be a programmer to do this, and it costs nothing. The whole game lives on Cloudflare, a company that will host a small site like this free, and the free allowance is far more than a game among friends will ever use. 1. **Get the game.** Download the source (the button at the top of this page) and unzip it. You now have a folder called `merlot`. 2. **Open a free Cloudflare account.** Go to [dash.cloudflare.com/sign-up](https://dash.cloudflare.com/sign-up), enter an email address and a password, and confirm the email they send you. No card is needed for the free plan. 3. **Install Node.js.** This is the program that does the uploading. Get it from [nodejs.org](https://nodejs.org) — take the version it offers you — and install it like any other program. 4. **Open a terminal** in the `merlot` folder. On Windows that is PowerShell (right-click the folder, "Open in Terminal"); on a Mac it is Terminal; on Linux you know already. 5. **Type these two lines**, pressing Enter after each and waiting for it to finish: ```bash npm install npx wrangler deploy ``` The first line collects the pieces the game needs. The second asks for your Cloudflare account: a browser window opens, you sign in and click Allow. When it finishes it prints a web address ending in `workers.dev` — that is your copy of the game. Send that link to anyone you want to play with. To try it on your own computer first, without publishing anything, type `npx wrangler dev` instead and open `http://localhost:8787` in your browser. If any of that goes wrong, paste the error into Claude (or another AI assistant) and ask what it means — that is genuinely the fastest way through it, and it is how this game was written in the first place. ## Information for nerds - **Server:** a Cloudflare Worker with Durable Objects (`src/`). Each match room is one Durable Object that holds the players' WebSockets and runs the simulation tick (`src/game.js`). The same simulation drives the bots, the replays and the tests. - **Client:** a single self-contained page (`public/index.html`) with its art, sound effects and music in `public/`. - **Art pipeline:** `art/` holds the Blender and Python scripts that rendered the sprites. The generated source images are not included; the finished sprites are in `public/art/`. - **Bot tuning:** `tune/` runs self-play matches to tune the bot tiers. - **Requirements:** Node.js 22 or later. - **Optional settings:** `cp .dev.vars.example .dev.vars` for local runs; in production the same names (operator stats key, email relay) are set with `npx wrangler secret put NAME`. - **Your own hostname:** uncomment the `[[routes]]` block in `wrangler.toml` and deploy again. - **Tests:** each file in `test/` is a standalone script that exits non-zero on failure: ```bash node test/-test.mjs ``` ## Credits and licenses - Code: MIT. See [LICENSE](LICENSE). - Original art in `public/art`: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Credit "Merlot by Veered". - Sound effects: CC0 packs from Kenney (kenney.nl). The announcer voices were synthesised. See [public/sfx/README.txt](public/sfx/README.txt). - Music: CC0 tracks from OpenGameArt. See [public/music/README.txt](public/music/README.txt). - Merlot was written with AI assistance (Claude). ===== tune/README.md ===== # tune/ — finding the expert doctrine by self-play The bots do not learn from watching anybody. There is no game data to learn from: the room records one row per finished match (who won, how long, how many bots) and nothing about how it was played, so there is no corpus of human play to imitate and never has been. What there *is* is a game that runs headless at about two hundred times real speed, which means the bots can be improved the other way — by playing millions of seconds of it against themselves and keeping what wins. Nothing in here runs in production. The output is sixteen numbers that get pasted into `DOCTRINE_EXPERT` in `src/game.js`; the Worker itself gains no model, no inference, and no new dependency. ## What is being searched `DOCTRINE_BASE` in `src/game.js` is every number a bot consults while deciding what to do — when to break off, when to come back, how close to stand to ground it is taking, how much a team-mate's objective pulls it, how often it reaches for a power. The search varies those and only those. It cannot touch health, reach or damage: an expert tier has to out-think you, not out-gun you. ## Running it ``` node tune/tune.mjs --gens 8 --pop 20 --seeds 3 --workers 10 # ~15 minutes node tune/validate.mjs --seeds 20 --workers 10 # ~10 minutes ``` `tune.mjs` writes `tune/expert.json`. **The score it prints is not a result** — it is measured on the same few seeds the population was selected against, which is the standard way to be fooled. `validate.mjs` is the verdict: it replays the candidate on seeds it has never been played on and prints a Wilson interval. Paste the doctrine in only on a PASS. ## Why the numbers can be trusted - **Seeded.** The sim draws on `Math.random` for objective picks, kamikaze runs and spawn jitter, so two runs of one doctrine differ by more than the doctrine does. `arena.mjs` swaps in a mulberry32 stream, so a match is a pure function of its seed and the two doctrines. - **Common random numbers.** Every candidate in a generation is played on the same seeds and the same line-ups. A candidate that scores better did something better; it did not draw easier matches. - **Both ends of the map.** Every pairing is played twice with the sides swapped. This is exact rather than approximate: two identical doctrines on one seed produce the identical match whichever end they start from, so the swap cancels the side bias completely and `normal vs normal` reads 50.0% with no spread at all. `validate.mjs` prints that row first — if it is not 50.0%, something is broken and the other rows mean nothing. - **Mirrored line-ups.** Both sides fly the same hulls, so a result is about the doctrine and not about which side drew the better tank. - **Held-out seeds.** The search draws seeds under 10000; validation starts at 500000. ## What the first run found Eight generations, population 20, three seeds a generation — about nine minutes on ten cores. The doctrine it converged on is in `expert.json` and pasted into `DOCTRINE_EXPERT`. Validated on seeds it had never played: | | 1v1 (96 games) | 2v2 (240 games) | 4v4 (144 games) | |---|---|---|---| | easy vs normal | 1.0% | 0.0% | — | | normal vs normal | 50.0% (exact) | 50.0% (exact) | 50.0% (exact) | | **expert vs normal** | **77.1%** [67.7-84.4] | **89.2%** [84.6-92.5] | **95.8%** | It was searched at 2v2 only, so the other two sizes are the check that it is a doctrine rather than a fit to one room size. It gets stronger as the side gets bigger, which is what `groupPull` at twice the shipped value should do: the more hulls there are to arrive together, the more a side that arrives together wins. It is one idea rather than sixteen numbers: pick a point, go there together, and never disengage. Breaking off at 2.7% instead of 15%, holding at three-quarters of weapon reach instead of the full length, twice the pull toward a team-mate's objective and twice as long committed to it. The interesting part is what that says about the shipped doctrine. `RETREAT_HP` and `BASE_MEND` were reasoned out together — driving home to mend beats giving the hull up above 60% health, so break off at 15% and rally at 60%. The arithmetic is right and the search still threw it away, which means the thing it leaves out dominates: a hull driving home is not on an objective, and this game is decided on ground held. Worth knowing before hand-tuning anything else. ## The honest caveat The fitness is *beats the normal bots*. That is not the same thing as *is hard for a person*, and no amount of self-play closes that gap on its own — a human plays the map differently from anything in the population. Treat the tuned tier as a strong opponent that has been measured, then play it a dozen times and say whether it earns the name. That is the one place a human's play genuinely matters here, and a dozen games is plenty for it. ## Files | | | |---|---| | `arena.mjs` | one headless match, or a swapped-sides duel; the seeded RNG | | `pool.mjs` | a worker per core, chewing through a queue of duels | | `worker.mjs` | the worker body; all the thinking is in `tune.mjs` | | `tune.mjs` | the cross-entropy search; writes `expert.json` | | `validate.mjs` | held-out verdict, with the null and the easy tier alongside | ## Garrisoning (2026-08-12) the designer's own play was the hypothesis: *hold territory and heal rather than retreat*. The mechanism is `recoveryPost()` in `src/game.js` — an argmin over ground we hold, costing the walk, the wait at that post's mending rate, and the ground given up, gated on the post being momentarily safe. Measuring it took four runs and three wrong answers, which is the point of having `ab.mjs`: | | result | what it actually compared | |---|---|---| | defaults, `easeHp` 0.45 | 11.1% | *early* disengage **added on top of** retreating | | `easeHp` 0.30 / 0.20 | 27.1% / 42.7% | same, trigger walked down | | tuned by search | 47.1% | the search's own best, on held-out seeds | | **same trigger, destination varied** | **61.1%** | the actual question | | **shipped config, 240 games** | **59.2%** [52.9-65.2] | garrison on vs off | The first three read as "the idea does not work" and all three were measuring the wrong thing: they varied *when* a hull disengages at the same time as *where* it recovers. Hold the trigger at the 15% it always was, vary only the destination, and holding ground beats driving home by a clear margin. Two lessons worth keeping. An A/B whose two arms differ in more than one thing answers a question nobody asked. And a search over a mechanism's own knobs (47.1%) can miss what a single controlled comparison finds (61.1%), because the search was handed a confound too. --- ## Three mechanisms from the human profile, 2026-08-13 — and one of them paid The replay recorder made a comparison possible for the first time: the designer's seat against the bots *in the same matches* (13 tapes, games he actually played). Damage per minute 324 vs 287 and kills per minute 0.77 vs 0.73 — the same output — against **deaths per minute 0.68 vs 1.25, damage per death 545 vs 270, time inside a tower ring 32% vs 25%**. He also casts *fewer* abilities than they do (4.6/min vs 5.0). So the edge is death economy and objective time, and "use powers more often" was never it. `scratchpad` recipe: `replayMatch` with an `onTick`, count cooldown 0→+ transitions for casts, `CAP_RADIUS` for objective time. Three mechanisms were built to encode that, all as knobs defaulting to **off**, so the shipped bots are unchanged — proved by replaying three production tapes to an exact match after the edits. `tune/frag-burst-ab.log` has the numbers. | knob | what it does | held-out result | verdict | |---|---|---|---| | `fragHold` | standoff scaled by the hull's own toughness | 54.5% [50.4-58.5] / 576 | marginal, and NOT per-chassis: stinger slate 51.2%, splodge 50.4% | | `burstFrac` / `burstFloor` | disengage on damage *rate*, not the health floor | 7.6% → 16.0% → 35.4% → 53.8% as it is detuned | **loses** | | `chaseCast` | a gap-closer waits for a gap its buff can close | 51.7% [45.4-57.9] | no effect on the result | `burstFrac` deserves its own line. It is the mechanism that most looks like the human number it was built from, and it loses in exactly the shape the garrison work lost in: **disengaging early costs ground, and ground decides the match.** That is now twice. Anything of the form "leave the fight sooner" should be assumed dead until a controlled comparison says otherwise. `chaseCast` is the third mechanism to fix visibly stupid play without moving the result (after `meleeChase`). The waste it removes is real and measurable — Cutter cast Overdrive at a median gap of 242px and a p90 of 503 against a buff that covers ~315px in its lifetime; the gate takes casts from 306 to 256 and p90 to 324 — and the win rate does not care. **What did pay was changing the fitness, not adding a knob.** The default search plays candidates against `normal`, and the standing caveat is that beating normal is not the same as being hard for a person. `--baseline expert` (now on both `tune.mjs` and `ab.mjs`) makes the shipped tier the opponent: node tune/tune.mjs --baseline expert --size 2 --gens 10 --out expert-vs-expert.json Ten generations found a set beating the shipped expert **65.3% [59.6-70.5] at 2v2 and 63.5% [57.8-68.9] at 4v4** on held-out seeds (in-search it read 83.3%, which is the usual flattery). It went to the floor on `combatHold`, `objNearBias` and `easeHp` — the same "get on the point and stay there" doctrine, harder — and it chose `burstFrac` 0 and `fragHold` 0.027, throwing away two of the three mechanisms above by itself. It kept `chaseCast` at 1.25. ### Two tooling bugs found on the way, both silent - **`ab.mjs --with ` never restricted the slate.** It computed `SLATE` and then looped `LINEUPS`. Every per-chassis number quoted from it before this date — including the Cutter `meleeChase` 49.3% — is a full-slate number. - **`tune/ability-probe.mjs`** (new: what every power is cast at, and how much of it lands on nothing) is only as good as its yardstick. Its first version measured "nothing in reach" against each hull's *own* range, which scores every Cutter gap-closer as waste by construction (26px reach), and its heal check forgot that an allies-aura heals the caster too. Two of its first three findings were artefacts of the metric, not the bots. ----- For Claude Code users ----- Download https://veered.org/merlot/merlot.zip, unzip it into a new folder, read every README.md in it, and help me install and set up "Merlot" (A real-time strategy game for 1 to 12 players) on this machine. Check the requirements against this system first and tell me what is missing. Ask me before running anything with sudo, installing packages, or changing existing configuration files, and back those files up first. Finish by showing me how to test it and how to uninstall it.