# AGENTS.md Battle for 'Tismo — HTML5 port of the strategy game. Pure JavaScript: shared game logic runs both in Node (authoritative server) and in the browser (client). There is no build step and no runtime dependency; the client vendors jQuery and everything else (transport included) is hand-written. ## Commands - Install dev dependencies once: `npm install` (only `c8` and `jsdom`, used by the test suite — nothing the game runs needs them). - Run the test suite: `node tests/run_tests.js` (alias `npm test`; discovers every `tests/*_test.js` and runs each in its own worker process, prints a summary and exits non-zero on failure). `--jobs N` caps the pool and `--file ` runs a single suite in-process; the worker marker is internal. - Coverage report: `npm run coverage` (c8, writes `coverage/`). - Start the game server and client locally: `node server/server.js --port 27015 --bind 127.0.0.1`. Then open `http://127.0.0.1:27015/` (redirects to `/client/index.html`). The browser connects to `ws(s):///ws`. - Install the versioned git hooks (runs the tests on every commit): `./scripts/install-git-hooks.sh`; uninstall with `git config --unset core.hooksPath`. - Build the deployable packages: `nix build .#server` and `nix build .#web`. - If `node` is not on PATH, use the pinned one: `nix-shell -p nodejs --run "node tests/run_tests.js"`. There is no linter, formatter, or CI. Do not invent commands beyond these. ### Local testing mode - Opening the client from localhost (`http://127.0.0.1:27015/`) adds a **Testing mode** checkbox to the New game screen. When the admin starts such a game, every city's train and build rows gain a second "Free & instant" button. - This is gated on the server, never trusted from the client: `server/server.js` only sets `setup.testing` when the admin's WebSocket peer is loopback (`isLoopback`, handling `127.x`, `::1` and `::ffff:127.0.0.1`), strips a `free` flag from any non-loopback order, and `GameServer` honours it only while `this.testing` is set. `GameState.requestTrain/requestBuild(..., free)` then spawn or raise immediately without cost or queue (placement rules still apply). - The `testing` flag is shipped in the snapshot so joiners see the same buttons. ## Gotchas - Keep the shared modules framework-free: they are imported unchanged by both `server/` (Node, ESM) and `client/js/` (browser). No Node built-ins and no DOM APIs in `shared/` (`server/websocket.js` is the only place Node built-ins are expected). - Do not add JavaScript libraries. The only third-party code allowed is the vendored jQuery under `client/vendor/`; implement everything else by hand (see `server/websocket.js`, a dependency-free RFC 6455 server). - The game server also serves the app as static files from the repository root, so modules resolve as `/client/...` and `/shared/...`. Preserve the `client/` + `shared/` layout when packaging (`nix/package.nix`). - WebSocket state snapshots can be hundreds of KiB; the custom server handles fragmented and large frames (`server/websocket.js`). Do not lower the buffer limits without checking the snapshot path. - Indent `.js` files with 2 spaces. `.editorconfig` only enforces UTF-8. - Keep aircraft out of the melee: `_battleTiles`/`_combatTarget` skip them and their strikes are routed explicitly. Mirror the air rules (airport-only rebasing, strike radius, ferry fuel) on both client and server, or prediction and the authoritative state will disagree. ## Architecture - Game logic is server-authoritative. `shared/game_state.js` (`GameState`) is a framework-free model that owns the world, units, cities, territory, economy, training and visibility. `server/game_server.js` (`GameServer`) owns a `GameState`, validates orders and broadcasts snapshots over the transport in `server/server.js`. The browser client is a view: `client/js/map_view.js` rebuilds terrain from the shared seed, renders snapshots and sends orders. - Movement is continuous: `server/server.js` advances movement every tick and strikes an in-game hour every `SECONDS_PER_HOUR` real seconds. The client extrapolates each unit along its snapshot path and predicts move orders locally (`map_view.js`), mirroring `GameState.find_path` so a click feels instant. **Any pathfinding or movement-cost change must be mirrored on both sides** or prediction will fight the server. Roads (`shared/roads.js`, pre-generated into `GameState.roads`) flatten a tile's movement cost on both sides through `_tileTravelHours` / `_stepHours`; the road set is shipped in the snapshot and drawn in the chunked `layer-roads`. - Air and artillery are both ranged strikers (`GameState._isStrikeUnit`). A ground or naval battery ordered to attack opens a **sustained hourly bombardment**: it stays put and `_tickStrikes` removes a small share of its attack damage each hour (`BOMBARD.hourlyDamageFraction`), clearing the target when it dies or slips out of reach. An aircraft ordered to attack flies a **sortie** (`requestAirStrike`): it bombs once on arrival in `advanceMovement` and then returns to a friendly airport, where landing arms a two-day `strikeReadyHour` cooldown. The strike is charged up front per aircraft (`AIR_STRIKE`). Aircraft may only be rebased to one of their own airports and the ferry is bounded by fuel, not the combat `missionRange`; they never join melee. A strike may target a garrison inside a hostile city — the plane enters that tile as a goal only to bomb and turns for home, and captures skip aircraft, so it never occupies or takes the city. A strike is only accepted when the whole round trip fits the remaining fuel (out to the target, then home to a friendly airport), and the browser mirrors that check so an unreachable target is refused with a message instead of an order the server would drop. A standing `strikeTarget` is shipped in the snapshot and `map_view/motion.js` draws the red dashed line and target ring on `#layer-targets` while either endpoint is selected. - Terrain generation is deterministic from `(seed, mapConfig)`, so `GameState._buildTerrain` and the settlement/road step cache the world shape (`shared/game_state/world.js`) rather than regenerating it for every game with a known seed. The snapshot ships `mapConfig` and `MapView.ensureTerrain` rebuilds the exact same terrain from it (not the global `MAP_CONFIG`), which also lets the DOM tests use a small map fixture. - `shared/` holds the reusable algorithms so they stay testable without a server: `hex.js` (topology/geometry), `map_generator.js`, `hex_pathfinder.js`, `terrain_stats.js`, `rules.js`, `economy` figures in `game_state.js`, `game_clock.js`, `rng.js`, `noise.js`, `text_format.js`, `login_manager.js`. Data-driven content lives in `shared/data/` (one small module per topic: civilisations, units, buildings, governments, technologies, terrain, ...). `shared/data.js` is a barrel re-exporting the whole catalogue, so keep importing from `shared/data.js` and edit the file under `shared/data/`. `shared/game_state.js` is likewise a thin class composed from the mixins in `shared/game_state/` (world, territory, economy, movement, combat, air, status, diplomacy, orders, visibility, serialization). - `client/js/` splits the UI: `app.js` (screens and flow), `net.js` (transport wrapper), `game_screen.js` + `game_screen/` (HUD and panels), `map_view.js` + `map_view/` (DOM map), `modals.js` + `modals/` (city training/buildings/regional budget, nation/technology, confirm, news). - Tests live in `tests/` as `*_test.js` files extending the `TestCase` base in `tests/framework/`; `tests/run_tests.js` discovers and runs them. Add a new `tests/_test.js` for each new pure behaviour. ## Deployment - `flake.nix` exposes `packages..server` and `.web`, plus `nixosModules.default`. There is no compiler step: `nix/package.nix` copies the app and wraps `node server/server.js`; `web` is the static `client/` and `shared/` tree. - `nix/module.nix` runs the server as a `services.battle-for-tismo` systemd unit and serves the static client through nginx, redirecting `/` to `/client/index.html` and proxying `/ws` to the local Node server. The browser client connects to the page's own origin, so HTTPS gives it `wss://`. - Build locally with `nix build .#server` and `nix build .#web`. - Updating without a rebuild: set `services.battle-for-tismo.appDirectory` to a writable checkout (e.g. `/opt/battle-for-tismo`); the unit then runs `node /server/server.js` instead of the Nix-store copy. Serve nginx `client/` and `shared/` from that same checkout (e.g. `alias` locations), then `git pull` and `systemctl restart battle-for-tismo`. Keep the client and server trees on the same commit: the client mirrors the server's movement model, so a mismatched pair makes prediction fight the server.