- 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
8.8 KiB
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(onlyc8andjsdom, used by the test suite — nothing the game runs needs them). - Run the test suite:
node tests/run_tests.js(aliasnpm test; discovers everytests/*_test.jsand runs each in its own worker process, prints a summary and exits non-zero on failure).--jobs Ncaps the pool and--file <name>runs a single suite in-process; the worker marker is internal. - Coverage report:
npm run coverage(c8, writescoverage/). - Start the game server and client locally:
node server/server.js --port 27015 --bind 127.0.0.1. Then openhttp://127.0.0.1:27015/(redirects to/client/index.html). The browser connects tows(s)://<page-origin>/ws. - Install the versioned git hooks (runs the tests on every commit):
./scripts/install-git-hooks.sh; uninstall withgit config --unset core.hooksPath. - Build the deployable packages:
nix build .#serverandnix build .#web. - If
nodeis 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.jsonly setssetup.testingwhen the admin's WebSocket peer is loopback (isLoopback, handling127.x,::1and::ffff:127.0.0.1), strips afreeflag from any non-loopback order, andGameServerhonours it only whilethis.testingis set.GameState.requestTrain/requestBuild(..., free)then spawn or raise immediately without cost or queue (placement rules still apply). - The
testingflag 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) andclient/js/(browser). No Node built-ins and no DOM APIs inshared/(server/websocket.jsis 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 (seeserver/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 theclient/+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
.jsfiles with 2 spaces..editorconfigonly enforces UTF-8. - Keep aircraft out of the melee:
_battleTiles/_combatTargetskip 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 aGameState, validates orders and broadcasts snapshots over the transport inserver/server.js. The browser client is a view:client/js/map_view.jsrebuilds terrain from the shared seed, renders snapshots and sends orders. - Movement is continuous:
server/server.jsadvances movement every tick and strikes an in-game hour everySECONDS_PER_HOURreal seconds. The client extrapolates each unit along its snapshot path and predicts move orders locally (map_view.js), mirroringGameState.find_pathso 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 intoGameState.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 chunkedlayer-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_tickStrikesremoves 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 inadvanceMovementand then returns to a friendly airport, where landing arms a two-daystrikeReadyHourcooldown. 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 combatmissionRange; 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 standingstrikeTargetis shipped in the snapshot andmap_view/motion.jsdraws the red dashed line and target ring on#layer-targetswhile either endpoint is selected. - Terrain generation is deterministic from
(seed, mapConfig), soGameState._buildTerrainand 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 shipsmapConfigandMapView.ensureTerrainrebuilds the exact same terrain from it (not the globalMAP_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,economyfigures ingame_state.js,game_clock.js,rng.js,noise.js,text_format.js,login_manager.js. Data-driven content lives inshared/data/(one small module per topic: civilisations, units, buildings, governments, technologies, terrain, ...).shared/data.jsis a barrel re-exporting the whole catalogue, so keep importing fromshared/data.jsand edit the file undershared/data/.shared/game_state.jsis likewise a thin class composed from the mixins inshared/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.jsfiles extending theTestCasebase intests/framework/;tests/run_tests.jsdiscovers and runs them. Add a newtests/<topic>_test.jsfor each new pure behaviour.
Deployment
flake.nixexposespackages.<system>.serverand.web, plusnixosModules.default. There is no compiler step:nix/package.nixcopies the app and wrapsnode server/server.js;webis the staticclient/andshared/tree.nix/module.nixruns the server as aservices.battle-for-tismosystemd unit and serves the static client through nginx, redirecting/to/client/index.htmland proxying/wsto the local Node server. The browser client connects to the page's own origin, so HTTPS gives itwss://.- Build locally with
nix build .#serverandnix build .#web. - Updating without a rebuild: set
services.battle-for-tismo.appDirectoryto a writable checkout (e.g./opt/battle-for-tismo); the unit then runsnode <dir>/server/server.jsinstead of the Nix-store copy. Serve nginxclient/andshared/from that same checkout (e.g.aliaslocations), thengit pullandsystemctl 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.