Windward Mark

Q8FMADE BY qwen3.8-flash-next

Two-up windward racing on one simulated quantity: the apparent wind. The red wedge off the bow, the blue masthead arrow, the sheet gauge and the force that actually moves the boat all come out of the same function of the same number, so the wedge is not a painted rule but the direction your keelboat genuinely cannot make way in. Round three marks in order and cross the line, and the geometry forces both manoeuvres: each leg can be sailed on one tack only, so you have to tack through the wedge to beat to the weather mark and gybe around the other side to get home — against a clock that an optimal run already uses a third to a half of.

▸ CARTRIDGE SLOT · WINDWARD MARK
↗

* THE GAME RUNS IN A SANDBOXED FRAME — IT CAN’T SEE THIS SITE OR YOUR OTHER TABS. BEST SCORES STAY IN THIS BROWSER.

Cartridge promptTHE EXACT PROMPT THAT BUILT THIS GAME
Build a tiny browser game called "Windward Mark".

Game spec
---------
A single-file sailing cartridge on a fixed 800x450 canvas, playable with keyboard, mouse or touch, whose entire design is one quantity: the apparent wind, the air's velocity relative to the boat. THE CENTRAL CONTRACT is that the painted no-go wedge, the masthead apparent-wind arrow, the trim gauge's green band and the force that moves the boat are all computed from one function of one value. A flat-plate sail gives the forward force as the closed form (cos(awa - 2*sig) - cos(awa))/2, zero when luffing, negative when backwinded, maximal at half the apparent angle; a stall model floors it at BLUFF because a stalled sail is a parachute, not an empty bag. The no-go zone is DERIVED, never declared: steadySpeed() iterates the boat's own equations to convergence (speed -> apparent wind -> drive -> drag -> speed, including leeway, using the same apparentWith() and leewayDrift() the integrator calls) and noGoHalf() is the narrowest heading off the true wind that still makes VMIN. That derivation took four wrong attempts to get right and each failure is recorded in the file: a force compared to a magic number (wedge opened to 12 degrees), a force balance against a caller-supplied apparent speed that asked whether a heading could produce force but not whether the boat could HOLD that speed (21 degrees - pointing 25 degrees closer to the wind than any keelboat, which turns a sailing game into pointing at the finish), and a truncated fixed iteration count that left slow headings unsolved so the HUD under-promised speed and the wedge sat 10 degrees too wide. Constants are set by a polar sweep against real sailing targets, not by eye: a no-go half-angle of 42-54 degrees depending on breeze, best velocity-made-good at 52-56 degrees off the wind, and a top speed of 144-150 px/s on a broad reach around 100 degrees. scripts/scratch/wm-wedge.mjs proves the picture and the physics are one thing: it paints the scene with and without the wedge, diffs the frames to measure the painted half-width against the derived boundary, checks the sector is anchored to the WIND rather than the bow, and mutation-tests itself by halving sail force and demanding both move together in the same direction. The course is three marks rounded IN ORDER then the finish, and the ordering plus the leg bearings are what make the manoeuvres mandatory rather than stylistic: each leg is sailable on one tack only, so wm-course.mjs asserts every finishing run contains at least one tack AND one gybe (mean 2.4 tacks) - the earlier single-mark course let a bot finish without ever changing tack, which removed the whole point of the game. Tacking is also punished honestly: the boat has to be steered through dead downwind or dead upwind and loses speed doing it. Difficulty (true wind, gustiness, veer, crosswise current, mark radius, and whether the HUD shows numbers) is measured rather than asserted: time limits are DERIVED from the polar as a multiple of the optimal race time, so an optimal run uses 31-53% of the clock and the timer is a real constraint rather than the decoration it was when the limits were invented (a perfect player finished in 12s against a 95s limit and all three difficulties measured 100% winnable); a tiered player model then reports a graded curve - a skilled player wins everywhere, an ordinary 260ms reaction wins easy/medium and loses hard, and 430ms and sloppy play fall away on the rungs above them. Movement is owned by the simulation: keyboard and pointer both only set the same held-key flags, and the rudder authority scales with speed so a stopped boat cannot pivot. Audio is synthesised in-file, the score leaves via postMessage to the parent, and there are no external requests.

Hard constraints — the game will be embedded in a sandboxed iframe on a static gallery site, and a headless browser will screenshot it:

1. Output exactly ONE self-contained HTML file. All CSS and JS inline. Zero external requests: no CDNs, no external fonts, images, or sounds, no fetch/XHR/WebSocket. Any sound must be synthesized with WebAudio.
2. The iframe sandbox is "allow-scripts allow-pointer-lock" (plus an autoplay permission). So: no localStorage, cookies, or indexedDB (opaque origin — access throws); no popups, window.open, or navigation; no access to the parent page; alert()/prompt()/confirm() are silently blocked.
3. The playfield must be visible within 800 ms of load — that is when the screenshot is taken. No dead white screen. If there is a start screen, make it visually rich.
4. Controls: keyboard AND mouse must both work, plus on-screen touch buttons for phones. Call preventDefault() on game keys (arrows, space) so the page never scrolls.
5. The view is 16:9 and must scale to fill the viewport with no overflow or scrollbars.
6. Show a visible score. If the genre has a losing state, show a game-over screen with a restart (button and a key). Idle/clicker games skip game over but still need clear numeric feedback.
7. If you track a score, report it to the arcade shell so the player's best is remembered between visits: on every score change, call parent.postMessage({ source: "ai-arcade", type: "score", score: <number> }, "*"). It is safe to call it no matter what the parent does with it.
8. Keep it tiny: aim for under 400 lines, 60 fps, no frameworks. That 400-line target does not apply to 3D games — true 3D (a camera and perspective-projected world) or pseudo-3D (projected road/track, depth-sorted sprites) — which need their own software renderer and typically land at 800–1400 lines. The rest of this constraint still binds every game: 60 fps, no frameworks, no external assets, and no padding to reach a length.
9. Difficulty: the arcade shell passes the player's chosen difficulty as a URL query parameter. Read it with new URLSearchParams(location.search).get("difficulty") — the value is "easy", "medium", or "hard" (missing or unknown means "medium"). Make the three levels meaningfully different — speed, lives, enemy pressure, reaction time, whatever fits the genre; clicker/idle games with no losing state use starting resources and upgrade cost curves instead. Show the active level in the HUD. Without the parameter the game must be fully playable at its default (medium) tuning.

Return only the complete HTML file, no commentary.