BeamNG.Earth
A 1:1 scale streamed Earth for BeamNG.drive: real elevation and satellite imagery, floating origin, LOD streaming from a companion server
A browser-based seeded Pokemon roguelike. Stage 1: a seeded run — pick a starter, choose your path, beat the gym.
GamesTypeScript · CSSGitHub @y-wang217OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
A browser-based seeded Pokémon roguelike: an eight-gym randomizer run through regions you choose, with a party you catch, a kit that starts weak and climbs, and relics that open the roads a party alone cannot.Read the full README ↗
A browser-based seeded Pokémon roguelike: an eight-gym randomizer run through regions you choose, with a party you catch, a kit that starts weak and climbs, and relics that open the roads a party alone cannot.
Stage 0 proved the battle engine. Stage 1 made it a run. Stage 2 made it a randomizer and built the instrument that says whether the randomizer is playable. Stage 3 added tiers, rewards, items, shops and events. Stage 4 added party slots and switching. Stage 4.5 added no mechanics at all and made the existing ones legible. Stage 4.5.1 put prices back on things. Stage 4.6a gave the map a geography. Stage 4.6b turned the run into a ramp. Stage 4.6c added relics. Stage 4.7 is a legibility pass and one balance change. the map a geography. Stage 4.6b turned the run into a ramp. Stage 4.6c made a capability a relic.
There is still no battle engine here. GYMRUN wraps Pokémon
Showdown via @pkmn/sim.
This file is the front door: how to run it, and what each stage added. The rules, the numbers and the current state are not here, and where this file disagrees with one of the documents below, the document is right.
| you want | read |
|---|---|
| the invariants, and nothing else | CLAUDE.md |
| current state, open items, the design lineage, and the map to every other document | docs/README.md |
npm install
npm run dev # play it: / is the card run, /gymrun.html is GYMRUN
npm run check # the nine-leg gate; every leg runs, table at the end
npm run build # static bundle in dist/, deploys to any static host
The deployed page is the card run (index.html): the card battler's
scenarios stitched into three floors, each two fights and a boss fight, with
a Town or Wild stop between the two fights and a City, Town or Wild stop
before the boss; floor 1's fights are one wave each, and each floor after
adds one (docs/generation.md sections 125r and 148). GYMRUN itself is
built beside it as gymrun.html, unlinked; the smoke run plays that page.
npm run cards:run plays whole card runs headless and prints how far each got.
After a build:
npm run smoke # play a whole run in a real Chromium, fail on any error
npm run sim # play N runs headless and report the balance
npm run measure # per-dependency gzipped bundle sizes
What a card run and the sandbox deal before any reward, for tuning by eye.
The data is the truth: the run's deck is STARTING_DECK in
src/cardData/cardrunTables.ts, the
Puppeteer's is DECKS.puppeteer in src/cardData/cards.ts,
cards and upgrades are in those two files and
src/cardData/runCards.ts, and units in
src/cardData/units.ts. test/cards-readme-decks.test.ts
fails when a deck changes and this sheet does not.
MP. One pool for the whole team, refilled at the start of every turn to
the units' max MP added together, leftovers lost (RULES.mp in
src/cardData/rules.ts). A card's MP comes out of
that pool, whichever unit plays it. "1× a wave" is Uses: after its last use
the card waits for the next wave.
Units
| unit | HP | base shield | card slots | max MP |
|---|---|---|---|---|
| A Commander | 1 | 1 | 1 | 1 |
| B Gunner | 2 | 1 | 2 | 1 |
| C Sword dasher | 3 | 2 | 3 | 1 |
The card run (11 cards; a hand is 5)
| card | id | owner | MP | does | upgrade (+) |
|---|---|---|---|---|---|
| Baton | baton | A Commander | 1 | Strike 1 | Strike 2 |
| Brace | brace-a | A Commander | 1 | Shield 1 | Shield 2 |
| Spark | spark | A Commander | 0 | +1 MP now · 1× a wave | 2× a wave |
| Shoot | shoot | B Gunner | 1 | Strike 1 | Strike 2 |
| Brace | brace-b | B Gunner | 1 | Shield 1 | Shield 2 |
| Slash | slash | C Sword dasher | 1 | Slash 1 | Slash 2 |
| Brace | brace-c | C Sword dasher | 1 | Shield 1 | Shield 2 |
| 2× Move | move | any | 1 | Move 1 | Move 2 |
| Attack | attack | any | 1 | Strike 1 | Strike 2 |
| Dig In | dig-in | any | 1 | Shield 1 · 1× a wave | Shield 2 |
The sandbox's Puppeteer deck (15 cards; the tutorial and the sandbox scenarios deal it)
| card | id | owner | MP | does | upgrade (+) |
|---|---|---|---|---|---|
| Call Medic | call-medic | A Commander | 1 | Shield 1 (ally) | Shield 2 (ally) |
| Command | command | A Commander | 1 | Ally Move 1 | Ally Move 2 |
| Focus | focus | A Commander | 1 | +1 MP for 2 turns | +1 MP for 3 turns |
| Moon Strike | moon-strike | A Commander | 3 | Strike 2, Target 1 · ult | cost 2 |
| Shoot | shoot | B Gunner | 1 | Strike 1 | Strike 2 |
| Resupply | resupply | B Gunner | 1 | +2 MP next turn | +3 MP next turn |
| Artillery | artillery | B Gunner | 3 | Blast 2, Target 1 · ult | cost 2 |
| Fire! | fire | B Gunner | 1 | Blast 1 | Blast 2 |
| Dash | dash | C Sword dasher | 1 | Move 2 | Move 3 |
| Slash | slash | C Sword dasher | 1 | Slash 1 | Slash 2 |
| Need Help | need-help | C Sword dasher | 1 | Next hand +1 | Next hand +2 |
| Prep | prep | C Sword dasher | 1 | Shield 2 · 1× a wave | Shield 3 |
| Move | move | any | 1 | Move 1 | Move 2 |
| Dig In | dig-in | any | 1 | Shield 1 · 1× a wave | Shield 2 |
| Attack | attack | any | 1 |
contentHash release addsSeeds carry their balance version. The seed bar, the corner stamp, the summary
and the share text all show GYMRUN-<six hex>-<seed>, where the six characters
are the head of a hash over the data tables computed at build time. Paste one
made on another build and the game says so before the run starts, and offers
the bare seed for a fresh run instead. The run log carries a versions block —
run log, content hash, AI version, randomizer version — and a log from another
build is refused at replay naming which axis moved. previewRun(seed, contentHash) draws a seed's whole map without playing it. Nothing about the
generated run moved. docs/generation.md section 9.
The player was making decisions without being able to see the state those decisions act on, and could not read what was in front of them when they did.
Anything joining the party now arrives at the segment's player level. 4.6a's "exactly as it was fought" read as generosity and was the opposite: wild encounters are drawn 16 to 26 levels under the party, so a capture was a slot that could not fight for the rest of the segment it was taken in — including that segment's gym.
It was made on a hypothesis: that the tax suppressed swapping, so runs converged
on the starter. The pre-patch numbers did not support it and were checked
first — catch-greedy already beat catch-averse, and 37 gym-8 parties carried
81 distinct species between them. So it ships as a correctness fix with the swap
question open.
Measured at 400 seeds on the same prefix as the pinned 4.6c benchmark:
completion 5.25% → 9.8%, mean gyms 3.03 → 3.41, and the capture take rate
down slightly at 29.4%, so the feared "captures become strictly better than any
reward" did not happen. Two findings the report gave back that the patch did not
go looking for, both in docs/balance.md §13: the level tax turns out to have
been most of the punishment for indiscriminate catching, so removing it weakened
4.6a's "the value is in the declining" conclusion — and the pre-gym lead choice
does not measure at all (lead-static and lead-swap both reach 3.19 gyms at
200 seeds), which is exactly the thing the brief asked to find out before the
screen got polished.
Roads a party alone cannot open, and permanent objects that open them.
core/events.ts learning to run one.latent is type-based, not learnset-based. The bundle ships no learnsets
and never will; src/data/capabilityTypes.ts records the measurement and the
three reasons.This is the third design for the capability system and the two before it are
still in docs/spec/. docs/README.md says which is live.
A run that starts with Tackle and Growl and ends with something that hits like a truck.
-enditem line and destroyed — it does not
restock. They occupy backpack slots, which is what makes dropping them a
decision once the economy moves past them.A map with places in it, and a party you catch rather than one you are given.
docs/keyed-streams.md
and docs/spec/gymrun-seeds-and-mappability.md.Three decisions that used to be rules, and one that used to be free.
tuning.backpackCapacity), so acquiring is still a choice; assigning is
not. Nothing is ever destroyed except by an explicit discard.docs/engine-notes.md.Atk versus SpA is the one distinction a non-player cannot infer.Rest nodes were also cut roughly in half. They were tuned for a world where healing was free, and this is the stage that makes healing cost a node.
Nothing you can play. No mechanics, no state, no randomness, and no balance change — the 1000-seed report is byte identical across the stage, which is the stage's headline result rather than a footnote to it.
What it adds is sight. The physical/special split has been resolving correctly since Stage 0, Choice Band has been finding Attack and Choice Specs Special Attack since Stage 3, and none of it was on screen — so the decisions those mechanics create were invisible and the game read as a coin flip with sprites.
core/battle/stats.ts an answer rather than a guess.Atk 152 +1 228 — and nothing
but the bare number when a stage is zero, so the panel is quiet until
something changes.Ability randomization is what makes the last two load-bearing. Abilities come
off the full pool rather than a species' legal set, so Levitate on a Rhydon
is routine, and a UI that showed 2x on that Earthquake would train the player
to distrust every number it prints. The rule is that ability effects are folded
into the badge only when the ability is visible; otherwise the naive chart
result is shown, which is exactly what a player reasoning from types alone would
conclude.
tuning.revealOpponentAbility and revealOpponentItem both default to true.
With abilities drawn off-species there is no legal set to narrow and no meta
knowledge to infer from, so hiding one does not create a deduction — it converts
a skill decision into a coin flip. Same reasoning Stage 1 used to clear status
between encounters. Both are flags so the opposite can be playtested cheaply.
Everything earlier still holds: the seed is shown, editable and in the URL; the same seed plus the same decisions gives an identical run on any machine; HP and PP carry across encounters; the run log is the seed plus the decision sequence and nothing else, so closing the tab mid-run and reopening it puts you back where you were.
All of it lives in docs/architecture.md: the five
boundary rules and which are lint-enforced, the layer diagram, the four seams,
and the run log structure. It is the owner, and this file used to restate it.
docs/keyed-streams.md
for what shipped, docs/spec/gymrun-seeds-and-mappability.md
for the design it implements.docs/generation.md.@pkmn/sim findings and the bundle: docs/engine-notes.md.The display face is Pixelify Sans,
copyright 2021 The Pixelify Sans Project Authors, used under the SIL Open Font
License 1.1. Two latin-subset weights are self-hosted under public/fonts/ with
the licence beside them in OFL.txt. Numbers and body copy render in the system
monospace stack; --font-body in src/ui/theme/tokens.css is the one line that
changes that.
The centrepiece is not a game feature. It is the simulator. You cannot balance a roguelike by playing it: fifty runs is an afternoon and three anecdotes, a thousand runs is forty seconds and a distribution, and a difficulty curve is a distribution.
docs/balance.md owns every figure, the standing policy
that balance is not a gate, and the benchmark table. No number is repeated here,
because a number copied into two files disagrees with itself within two stages.
| Script | What it does |
|---|---|
npm run dev | Vite dev server |
npm run build | Typecheck, then static bundle to dist/ |
npm test | Vitest, headless, the whole suite |
npm run test:unit | The suite's Node-only half — no browser needed |
npm run test:browser | The suite's browser half; GYMRUN_ENGINE picks the engine |
npm run test:webkit | The browser half on WebKit |
npm run types | tsc --noEmit on its own |
npm run test:trim-strict | Tests with the bundle trim's stubs set to throw on any access |
npm run check | The gate. Nine legs, every one of them run; see below |
npm run sim | Play N runs headless and report the balance |
npm run gen:pools | Regenerate the species, move and ability tables from the dex |
npm run pool-report | What the move pool holds, how thin each band's per-type slice is, and what a real starter roll produces |
npm run content-hash | Print the contentHash of the working tree; --files lists what it covers |
npm run smoke | Browser smoke test against dist/ (build first) |
npm run measure | Gzipped bundle size per dependency (build first) |
GYMRUN_FULL_DEX=1 npm run build | Build without the bundle trim |
npm run check is scripts/check.mjs, and it runs every leg rather than
stopping at the first failure. Nine legs, in order:
| leg | needs a browser |
|---|---|
lint | |
typecheck | |
test:node — the suite's 111 Node-only files | |
test:chromium — the 24 browser files | yes |
test:webkit — the same 24, other engine | yes |
trim:node — strict trim, Node half | |
trim:browser — strict trim, Chromium half | yes |
build | |
smoke — needs dist/, so it follows build | yes |
node scripts/check.mjs --list prints them without running anything, and
--only=lint,build runs a named subset with the same reporting.
CI runs the same legs, split across five jobs so the slowest engine rather than
the sum of everything sets the wall clock. Each job calls the runner with
--only, because a leg SKIPPED for a missing browser is promoted to FAILED
when CI is set and that promotion only happens if the runner is what CI
invokes.
It exits 1 if any leg failed. A leg can also report SKIPPED, for one of two reasons, and the difference matters:
CI is set — an engine the workflow was supposed to install and did not is
a broken workflow, not a pass.smoke cannot run without a dist/. Never promoted
by CI: the leg it depended on already reported FAILED and already fails the
run, so promoting this one would print two failures for one cause.Which test files need a browser is computed, not listed —
scripts/browser-tests.mjs looks for the files that reach Playwright, directly
or through test/visual/harness.ts. A missing-browser SKIPPED is only ever
honoured on a leg declared as a browser leg, so if that detection ever misses a
file, the Node leg goes red on it rather than quietly skipping.
WebKit is the engine this project is most likely to be wrong on, and the one
least likely to have been run. It is Mobile Safari, the app is phone-first and
pinned at 390x844, and for most of this repo's life the WebKit leg had never
executed at all — the development containers ship Chromium only, so it reported
as absent and the && chain died on it. Three patches shipped with WebKit
listed as their one open gate.
It is two commands, and the second is the one people miss:
npx playwright install webkit # the binary alone cannot launch
npx playwright install-deps webkit # GTK4, gstreamer, flite, ~25 libraries
npm run test:webkit # 24 files, 201 tests, about 9 minutes
Without install-deps the browser downloads and then fails with a list of
missing libraries, which reads like a broken install rather than a missing step.
A container is rebuilt per session, so this does not persist and is not
something a session inherits.
Run it before merging, not after, when the change touches any of these:
| trigger | why |
|---|---|
layout: display, flex, wrapping, sizing, intrinsic size | where the engines genuinely differ, and where every regression so far has been |
a replaced element that can fail — <img>, a sprite, an icon | a broken <img> is not a replaced element, so width stops applying; this shipped once and pushed a 390px page to 401px |
anything under theme/, motion, animation, or a duration | the motion system had never run on WebKit until the iOS patch, and two defects were living in exactly that gap |
| a phone or iOS bug report | the reporter is on WebKit; Chromium cannot reproduce it, and assuming otherwise cost a whole patch chasing a defect that did not exist |
| the guarded screen heights | docs/visual/baseline/heights.json is a Chromium recording, so WebKit is compared within a pixel rather than exactly |
Reading a WebKit failure. Expect the instrument to be wrong before the app
is. The first honest WebKit run in this repo produced 11 failures and seven
were this repo's own test instruments — a screenshot indexed in CSS pixels at
3x density, a motion helper with three separate timing errors, a parallax case
waiting a fixed 150ms for a throttled frame. Check the harness before changing
a stylesheet, and never re-record a baseline to make a WebKit number agree:
expectBaselineHeights already compares within a pixel there, on purpose, and a
re-record would hide a real Chromium regression behind it.
Status. Green as of 2026-09-17 on WebKit 26.6, 24 files and 201 tests,
measured twice — once before this patch's merge with main and once after — so
a future failure has a commit boundary to bisect against.
CI runs both engines, but CI does not gate the heights, and that is the one
thing to know before trusting a green browser job. heights.json is a
recording of one machine's fonts: the UI is set in a system stack with no
@font-face and no font file in the repo, so the same layout measures 897.22 in
the CI container against a recorded 944.5 here. The four height assertions are
therefore scoped to where the recording applies and skip under CI, with the
reason in the test name. Running them is local work, and it is the reason to
run the browser suite yourself before a merge that touches layout. Making them
portable means shipping a webfont, which would move every recorded number; it is
an open item, not a decision taken.
Stage 4.6a shipped six calls flagged for review. All six are ratified and are written up where they take effect; they are listed here so that "was this decided or defaulted?" has one answer.
| call | where it lives |
|---|---|
| Locale select is a pre-step, not a node — the node budget is unchanged from 4.5.1 | tuning.localeOfferCount |
| Routes for every offered locale are generated up front and discarded at selection | docs/generation.md §1c |
| A captured Pokémon arrives at the segment's player level with the moveset it was fought with, and evolves at the next gym clear if its level allows | core/acquisition.ts, core/evolution.ts |
| Its held item goes to the backpack, not into its hands | core/acquisition.ts |
| The guaranteed wild step is all-wild at distinct tiers, not a one-option step | tuning.wildStepOptionCount |
| The capture renders inside the result screen; the standalone acquisition screen is gone | ui/screens/acquisition.ts |
docs/README.md section 5 carries them, one line each with a
pointer to where each is argued.
| Strike 1 |
| Strike 2 |