Files
Battle-for-Tismo/AGENTS.md
T
adrien cf75b31e68 Fixed air strikes against garrisons and the round-trip fuel model
- a strike is accepted only when the whole round trip (out to the target, then
  home to a friendly airport) fits the remaining fuel, and the browser mirrors
  the check so an unreachable target is refused with its distance and the
  plane's effective range
- a bomber that bombs between hourly ticks turns for home at once instead of
  idling and crashing on an empty tank; a plane with a runway lands on fumes
- aircraft may now target a garrison inside a hostile city: they bomb it and
  turn away, and captures skip aircraft so the city is never taken
- bomber/jet endurance and mission-range tuning
2026-09-18 22:46:19 +02:00

8.8 KiB

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 <name> 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)://<page-origin>/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/<topic>_test.js for each new pure behaviour.

Deployment

  • flake.nix exposes packages.<system>.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 <dir>/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.