Spaces:
Paused
Download CONTRIBUTING.md from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/CONTRIBUTING.md
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities@d19cbc4fab45274de920705d8b5b01da170c7e89/CONTRIBUTING.md
-
curl -L -o CONTRIBUTING.md https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/CONTRIBUTING.md
Contributing
Stellar Frontier: RTS is a vanilla-JavaScript, ES-module game with no build step and no
runtime dependencies. The files in the repo are exactly what the browser loads. That simplicity
is a feature β please keep it. A few rules are load-bearing; the test suite enforces them, so a
change that breaks one fails npm test rather than shipping.
Getting set up
node --version # must be >= 20
npm start # serve the game at http://localhost:8080 (zero-dep static server)
npm test # run the full suite (node --test)
There is nothing to install β no npm install, no bundler, no transpiler.
The hard rules
These are invariants, not preferences. Each has a guarding test that will go red if you break it.
1. The engine is pure, deterministic, and DOM-free
Everything under engine/ is the simulation: pure logic, no rendering, no browser. It must obey:
- One source of randomness. All randomness comes from the seeded PRNG in
engine/rng.js(mulberry32, plus thehashStrtie-break helper). The engine may never callMath.random,Date.now,new Date, orperformance.nowβ not even in a comment. (test/engine-purity.test.js.) A line that genuinely isn't the sim can opt out with adeterministic-exemptcomment, but that should be vanishingly rare. - No DOM / browser globals. No
document,window,localStorage,fetch,requestAnimationFrame, etc. underengine/. The one sanctioned seam is the render loop inengine/loop.js, whoserequestAnimationFramelines carry abrowser-exemptmarker. (test/engine-purity.test.js.) - Same seed β same game. Two runs from the same seed must produce byte-identical state, on
every world. If you touch the engine, keep replays identical β watch iteration order and
float-accumulation order especially. (
test/determinism.test.js,test/determinism-roster.test.js.)
If you need a stable-but-varying value (a per-unit angle, a tie-break), hash an id through
hashStr β don't reach for a clock or Math.random.
2. No build step, ever
The browser loads the repo as-is. So:
- Ship plain ES modules the browser understands β no JSX, no TypeScript syntax, no bundler-only imports.
- Every
getElementByIdtarget must exist inindex.html(or be created in JS), every relative import must resolve, and every file must parse. (test/static-integrity.test.js.) - UI modules should stay import-safe under Node (guard top-level
window/documentaccess), so their logic can be unit-tested.dom.jsalready resolvesdocumentdefensively; follow that pattern.
3. Saves are versioned
Save data is untrusted input and is version-gated:
engine/persist.jsownsSAVE_VERSION(skirmish) andGALAXY_SAVE_VERSION(Odyssey). Bump the relevant one whenever you change a save's shape in a way older saves can't survive. The version check is exact-match (if (save.v !== SAVE_VERSION) throw) β there is no migration step, so bumping the version makes every save written under the old version unloadable; the load fails fast with a clear "unsupported save version" error instead of feeding stale-shaped data into the sim. If a change is purely additive (a new optional field with a sensible default), you usually don't need to bump the version βsanitizeSave/cleanEntityalready default missing fields for saves at the current version.- Loading always sanitizes and coerces (
sanitizeSave,cleanEntity) β never trust a field's type or range straight off the wire. That coercion covers corrupt or missing fields within a supported version; it's not a substitute for bumping the version when the shape itself changes in an incompatible way.
Types (JSDoc + // @ts-check)
The core sim shapes β State, Unit, Building, Player, Galaxy, and friends β are defined
as JSDoc @typedefs in engine/types.js. That file has no runtime code and is never
imported; it exists purely so the type checker (and any editor with the bundled TypeScript
language service β e.g. VS Code out of the box) can verify field access against a real model
instead of an untyped bag. No build step, no runtime dependency β the shipped code stays plain
ES modules.
Type checking is opt-in per file: a file is checked only if it starts with a // @ts-check
pragma. Twenty engine files opt in today β the core data and hot-path modules (state.js, movement.js,
gather.js, grid.js, fog.js, separation.js, formation.js, haul.js, recycle.js,
wreckage.js) plus supply.js, colliders.js, scout.js, victory.js, production.js,
persist.js, aiCommon.js, aiStrategy.js, aiDifficulty.js and aiArchetypes.js. Expand
coverage file-by-file by adding the pragma and annotating the functions'
state/unit/building params with the shared typedefs.
The annotation half is not optional: strict and noImplicitAny are off, so an un-annotated
parameter is any and the pragma alone checks nothing. test/types-contract.test.js enforces both
halves β every // @ts-check file must annotate its exported functions, and every field a core
factory constructs must be declared on its @typedef.
This is what catches the silent-undefined-field class of bug β a mistyped or renamed field is a
check-time error, not a wrong result the same-seed determinism test can't see.
npm run typecheck # runs `tsc -p jsconfig.json` β needs a TypeScript compiler available
# (global `tsc`, or `npx -y typescript` / a local install). Editors with
# the TS language service check the annotated files live, with no install.
When you add or rename a field on a core shape, update its @typedef in engine/types.js in the
same change.
Style
Match the surrounding code: the same comment density (this codebase explains why, not what), the same naming, the same idioms. Prefer a small pure helper in the right module over a clever one-liner. Add or update a test for any behavioural change.
Test-Driven Development
For a new feature or a behavioural change, write the test(s) first, from the requirement, before writing the implementation:
- Turn the requirement into one or more
node:testcases in the file where that behavior belongs (see the existing suite for the idiom β direct state construction, no mocks). Do this from what the feature is supposed to do, not by reading ahead into a planned implementation. - Run them and confirm they fail for the right reason (a missing export, an assertion against current behavior) β red before green.
- Implement the smallest change that makes them pass, following the rest of this guide (purity, determinism, save versioning).
- Run the whole suite (
npm test) andnpm run typecheckβ a new feature can surface a now-outdated assumption in an older test; update that test's assertion to the new, intended contract rather than deleting coverage.
Commits
Keep commits focused and their messages descriptive β say what changed and why, and note that the suite stays green.
Every commit is signed off with:
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Protecting main (one-time repo setup β not yet done)
Everything above is enforced by tests, and the tests run in CI on every push and pull request. But
nothing stops a red build being merged anyway, and that is not hypothetical: npm run typecheck
failed on every commit from 2026-08-05 to 2026-08-08, and PRs #90 and #91 both merged to main
straight through it. A gate nobody is required to pass is a gate that eventually gets walked past.
This is a repository setting, so it cannot live in a file here. It takes about two minutes:
Settings β Branches β Add branch ruleset (or Add rule on the classic UI)
Target branch:
mainβ Require status checks to pass before merging, and add all three by name:
tests (node 20)tests (node 22)browser smoke test
All three must be listed. The matrix produces one check per Node version, and requiring only one lets a version-specific regression through β which is the whole reason the matrix exists. The smoke job is the only check that can see a page which parses cleanly and then throws on load.
β Require branches to be up to date before merging β so a check that passed against a stale base cannot count for a merge onto a newer one.
β Block force pushes
Leave "Require a pull request before merging" to taste; it is orthogonal to the failure above, which was about a red check rather than an unreviewed one.
The check names come from .github/workflows/test.yml's job name
(name: tests (node ${{ matrix.node-version }})). If that line is ever edited, the required
checks silently stop matching and the gate goes quiet β so change the two together.
Release checklist
When cutting a release:
npm testis green (determinism + purity + static-integrity included), andnpm run typecheckreports no errors on the// @ts-checked files.npm run smokeis green β boots the real page in real Chromium, starts a match and clicks things, failing on any uncaught error (tools/smoke.js; CI runs it as the browser smoke test job). It is shallow on purpose. Still worth ten minutes by hand for anything the script does not cover β an Odyssey run, and a save/reload of both modes.- Bump
APP_VERSIONinversion.jsandversioninpackage.jsonto the new semver, and keepversion.jsonin sync (the auto-update check compares them). (test/release-manifest.test.js,test/version.test.js.) - If any save shape changed, confirm
SAVE_VERSION/GALAXY_SAVE_VERSIONwere bumped and old saves still load. - Add a dated section to
CHANGELOG.md. - Tag the release:
git tag vX.Y.Z && git push --tags.