Files
Battle-for-Tismo/INTELLIGENCE.md
T

33 KiB
Raw Blame History

Intelligence — design brief

The Intelligence feature: its tab, the spy unit, covert actions (sabotage, city spying, comms spying), counter-intelligence and spy satellites. This document began as the design brief and now also records what was built. All phases are implemented; the status list in §2 is the authoritative summary and §5 records the design decisions each open question settled to.


1. Shared project context (read this first)

Battle for 'Tismo is an HTML5 port of a turn-of-the-millennium strategy game. Pure JavaScript, no build step, no runtime dependencies.

  • Server-authoritative. shared/game_state.js (GameState) is a framework-free model; server/game_server.js (GameServer) owns one and validates orders; server/server.js is the transport.
  • Shared logic runs in both Node and the browser. shared/ must stay framework-free: no Node built-ins, no DOM. server/websocket.js is the only place Node built-ins are expected. The browser is a view (client/js/map_view.js and friends) that rebuilds terrain from the shared seed and renders snapshots.
  • GameState is composed of mixins. shared/game_state.js imports each shared/game_state/<topic>.js and Object.assigns its methods onto the prototype (world, territory, economy, movement, combat, air, status, diplomacy, orders, visibility, serialization, politics, treaties, wmd, ...). A new method file must be imported and added to that Object.assign.
  • Data-driven content lives in shared/data/ (one module per topic: units.js, buildings.js, technologies.js, relations.js, ...). shared/data.js is a barrel — import from it, edit the file under shared/data/.
  • Client mirroring. Movement, pathfinding and movement-cost changes must be mirrored between shared/game_state/movement.js and the browser's prediction in client/js/map_view/motion.js, or prediction fights the server.
  • Snapshot performance. _broadcastState rebuilds the snapshot whenever the state is dirty (orders, hourly ticks). Large collections are versioned in DELTA_COLLECTIONS (server/game_server.js) — if you add a big snapshot collection, pair it with a version counter or it is re-sent whole on every broadcast. Walks are memoised against version counters (_populationVersion, _territoryVersion, _modifiersVersion, _tileImprovementVersion, _gdpEpoch, _visibleVersion, ...).

Commands

  • Install dev deps once: npm install (only c8 and jsdom for tests).
  • Run one suite: node tests/run_tests.js --file <name>.js (note the .js). Never run the full suite during development — the commit hook does that.
  • Server + client locally: node server/server.js --port 27015 --bind 127.0.0.1, then open http://127.0.0.1:27015/. Add --testing for the free/instant buttons.
  • If node is missing: nix-shell -p nodejs --run "node tests/run_tests.js".

Conventions

  • 2-space indent for .js. No new libraries (only vendored jQuery under client/vendor/).
  • Missing icon → add a dummy SVG reading dummy icon - <name>.
  • Tests live in tests/<topic>_test.js, extend TestCase (tests/framework/), and are discovered automatically. Fixtures: tests/framework/helpers.js (smallState(), defaultState(), adjacentLand(), seaTile(), grantBuilding()).
  • Visual changes are reviewed by eye before commit: build a minimal standalone HTML example, link client/css/style.css, copy in the few assets needed, and open it in Firefox (firefox <path>). Do not screenshot.
  • Commit messages are a single evocative sentence in the imperative. Only commit when the user asks; the pre-commit hook runs the whole suite.

2. Where the feature stands

Status legend: [x] done, [~] done with a simplification, [ ] open.

  • Intelligence tab exists, split into Spies and Propaganda sub-tabs. Propaganda owns the campaigns; Spies owns the viewer's spy roster (with a Locate control).
  • Spy unit core: data, gating, invisibility, 1-tile vision, travel through foreign territory.
  • Spy operation buttons live in the unit stack panel (one button per operation, from SPY_OPERATIONS), shown when a spy is selected.
  • Sabotage action: a spy on foreign land zeroes that tile's production for a week. The victim is notified; blame falls on the author, a chosen scapegoat, or no one, and the victim cannot tell which.
  • City spying: a spy beside a foreign city files a report on its people, buildings and garrison. Counter-intelligence can falsify it.
  • Comms spying: a spy beside a foreign capital intercepts the last private messages sent from and to that nation. Counter-intelligence can garble them and swap the correspondents.
  • Counter-intelligence mechanics: the unlock-only technology now shifts the blame roll and distorts incoming reports and intercepts.
  • Spies are single-use: a covert operation spends the whole acting stack, and each spy past the first blunts the target's counter- intelligence, so a bigger team is the one lever against it.
  • Spy satellites + space launch centre: the building gates a launch, everyone is told, and each satellite uncovers a random 2-tile patch every two days.
  • Foreign intelligence may be warned that a nation is enriching uranium (a chance roll when the first enrichment centre is raised).

What is already built

Tab. client/index.html has a <button class="tab" data-tab="intelligence"> Intelligence</button> after Politics and a #tab-intelligence page with #intelligence-sub, #intelligence-subtabs and the two views. client/js/modals/nation.js builds and refreshes it (_renderIntelligence, _buildIntelligence, _refreshIntelligence), and NationModal.show/refresh take a trailing intelligence argument. client/js/game_screen/panels.js supplies it via _intelligenceInfo() → { civs, localCiv, totalHours, campaigns, opinions }.

Propaganda moved. The campaign UI (_fillCampaigns, _buildCampaignRow, _buildCampaignForm, _syncCampaignReadout, _syncCampaignForm) now lives on the Intelligence tab's Propaganda sub-tab; the DOM state uses this.$intelligenceCampaigns and this._intelligenceState.campaign. Politics keeps populations, migrations, minority policies and the opinion breakdown. #campaignSignature / #campaignRows remain the rebuild guards.

Sub-tabs (Phase A). The Intelligence tab is split into two sub-tabs, Spies and Propaganda, mirroring the Economy and Resources tabs (#intelligence-subtabs .subtabs, _buildIntelligenceSubtabs / _setIntelligenceView, and the two .modal-body views #intelligence-spies-view / #intelligence-propaganda-view in client/index.html). Spies opens first; the campaigns moved into the Propaganda view. Switching only toggles .hidden, so a snapshot refresh never tears a view down.

Spy controls (Phase A). The Spies view holds one card, Your spies (_buildSpiesCard, _fillSpies, _hasResearched in client/js/modals/nation.js). The roster rebuilds only when a spy's id, tile or location label changes (_spySignature); it names each spy by position ("Spy 1") and its whereabouts ("at Paris", "in home territory", "in Britain", "in unclaimed territory"). A Locate button calls onLocateUnit(id), wired to _locateUnit in client/js/game_screen/panels.js, which centres the map, selects the spy and closes the panel. _intelligenceInfo() supplies the roster as spies.

Covert operations are unit actions, not tab content. They live in the unit panel (#unit-spy-actions, a row under the pillage/trench/attack buttons), because an operation is something a spy does, tied to the selected stack. _updateSpyActions(units) in client/js/game_screen/panels.js shows the row only when the selection is all spies; _buildSpyActionButtons() fills it once from SPY_OPERATIONS. Sabotage enables when the stack stands on a foreign land tile (_canSabotage, mirroring GameState.canSabotage); the other two stay disabled ("Not yet available") until they are wired. The catalogue lives in shared/data/intelligence.js (SPY_OPERATIONS, with the id constants SPY_SABOTAGE / SPY_CITY / SPY_COMMS), re-exported from shared/data.js.

Sabotage (Phase B). A new model mixin shared/game_state/intelligence.js owns the covert operations; only canSabotage / requestSabotage / isTileSabotaged / _tickSabotage exist so far. The target is the tile the spy stands on, which must be foreign land (civAt >= 0, not the spy's own); no map target-picking is needed. requestSabotage records the tile in GameState.tileSabotage (key → the hour the block ends, SABOTAGE.blockDays = 7) and drops the tile's GDP memo. The block reaches the economy through productionFactors' new sabotaged flag: a last ×0 factor that zeroes the tile's per-head production (and the client's tile readout), mirroring how pillage and trenches already mark a tile. _tickSabotage runs in tickHour and drops lapsed blocks, restoring the tile. The block list ships as tileSabotage: [[x, y, untilHour], ...], rebuilt client-side into GameScreen._tileSabotage. The victim gets a NEWS_SABOTAGE feed line (describeNews: "Saboteurs have crippled production in ") and, if the viewer is the victim, a notification; suspect names the author, a scapegoat, or is null. The order is { type: "sabotage", units, frame? }, validated in GameServer._handleSabotage and sent by _sabotageSelected. The acting spies are spent (_spendSpies): a spy is single-use, so it is removed quietly (no news, no notification). A larger acting stack lowers the victim's chance of identifying the author (_effectiveCounterIntelligence).

Counter-intelligence and blame (Phase C). intelligence.js adds the two level helpers _intelligenceLevel / _counterIntelligenceLevel (0 or 1, read from the researched set via hasTechnology) and _blameCulprit. The blame roll starts at ESPIONAGE.baseIdentifyChance, rises with the victim's counter-intelligence and falls with the author's agencies; a frame scapegoat (another nation, chosen in the unit panel's Blame picker) sticks with ESPIONAGE.frameChance when the author is not identified. A correct blame and a successful false flag are the same suspect field, so the victim cannot tell them apart.

City and comms spying (Phases B/D). canCitySpy/requestCitySpy and canCommsSpy/requestCommsSpy share _spyStackAtOneTile and _spyWatches: a spy can never enter a hostile city, so it watches from the city's own tile or a neighbour. Reports are stored per nation in GameState.spyReports (capped at ESPIONAGE.reportLimit) and shipped only to their owner in viewerSnapshot as spyReports; the chat log they are drawn from never ships. A city report lists the population, building levels and garrison; with the target's counter-intelligence, values are jittered or nudged (_jitterCount). A comms report lists the last ESPIONAGE.interceptCount messages involving the target (GameState.messages, fed by recordChat); counter-intelligence garbles the text and swaps sender/receiver (_garbleMessage/_scramble). Both appear in the Intelligence tab's Intelligence reports card. Both spend the acting spies (a spy is single-use) and a larger stack blunts the target's counter-intelligence when it decides whether to falsify or garble the report. The target picker reuses the ranged Attack overlay: _toggleSpyOperation arms it with operation: "spy_city"/"spy_comms" and _chooseTarget dispatches on it.

Comms store. server/server.js case "chat" now takes an optional to nation, telling one peer (the public channel when absent), and records every line through GameState.recordChat. The client's chat gained a recipient <select id="chat-target">; private lines are marked in the log. This is the one piece of the design that needed a new server-side store (question 6).

Spy satellites (Phase E). A new space_launch_center building (mechanic: "space_launch", extremely expensive, dummy icon) gives a nation one launch pad per level. requestSatellite(civ, city) checks the city holds the centre, pushes a { civ, launchedHours, nextRevealHour } record, and publishes NEWS_SATELLITE to everyone. _tickSatellites runs in tickHour and calls _revealSatelliteChunk, which adds a random patch of SATELLITE.revealRadius tiles to the owner's explored set every SATELLITE.revealEveryDays days and marks visibility dirty. Launches and next-sweep times show in the tab's Spy satellites card.

Uranium warning (Phase F). _completeBuilding calls _warnOfEnrichment when a nation's first uranium enrichment centre is raised; with ESPIONAGE.enrichmentWarningChance it publishes NEWS_ENRICHMENT, which describeNews renders as an intelligence warning.

Spy unit. shared/data/units.js:

{
  id: "spy", advanced: true, name: "Spy", maxHp: 100, cost: 250_000_000,
  materialUpkeep: { steel: 0, energy: 2_000, hightech: 3 }, // mostly high-tech
  moveable: true, speed: 0.5, military: false, attack: 0, defense: 0,
  range: 0, vision: 1, spy: true,
  traversableTerrains: ["Land"],
  requiresTechnology: "intelligence_agencies",
  icon: "icon_spy.svg",
}

spy is in TRAINABLE_UNIT_IDS. Visibility: shared/game_state/visibility.js _hiddenEnemyUnits hides any unit with proto.spy from every viewer except its owner. Movement: shared/game_state/movement.js _canUnitEnter lets a spy cross foreign territory (mirrored in client/js/map_view/motion.js _canEnter), so the foreign-territory wall does not apply to it.

Technologies. shared/data/technologies.js:

{ id: "intelligence_agencies", name: "Intelligence Agencies", theme: "society",
  cost: 250, prerequisites: ["scientific_method"], unlocks: true, effects: [] },
{ id: "counter_intelligence", name: "Counter-Intelligence", theme: "society",
  cost: 1_500, prerequisites: ["intelligence_agencies"], unlocks: true, effects: [] },

unlocks: true marks a technology whose value is content, not a stat; tests/data_test.js exempts those from the "every tech has effects" rule. counter_intelligence currently has no mechanics — wiring it is part of this feature.

Icon. client/assets/icon_spy.svg is a placeholder (dummy icon - spy).

Tests. tests/spy_test.js (4) covers the proto/gating, the covert-operation catalogue, invisibility, and foreign-territory travel. tests/game_screen_test.js has test_intelligence_tab_hosts_propaganda and the campaign tests now drive #tab-intelligence. data_test.js knows about unlocks.

Preview. A standalone mock of the tab is at /tmp/tismo-preview/intelligence.html (links the real style.css, hand-filled representative data, a faux top-left summary to get the modal's 104px dock offset right); /tmp/tismo-preview/intelligence-phase-a.html adds the sub-tabs and the spies roster; /tmp/tismo-preview/intelligence-spy-actions.html shows the spy action buttons in the unit panel. All throwaway — do not commit them.


3. Requirements (from ROADMAP.md, restated)

[ ] Gets its own tab

  • Move propaganda operations to intelligence [ ] Spy unit
  • Invisible to all other players, by default
  • 1 tile-radius vision
  • Mostly high-tech cost
  • Can move into enemy territory
  • [ ] Sabotage action:
    • Blocks production on a given tile for a week, performed while in any country
    • Perpetrator is given a choice to try and blame someone else
    • The victim gets a notification; depending on intelligence and counter-intelligence level, someone or no one will be blamed.
      • No way for the player to differentiate between a correct blame assignment or a successful false flag
  • [ ] City spying
    • Perpetrator gets a report on a city's status, building levels, etc. Counter-intelligence can make the report contain false information
  • [ ] Comms spying
    • Done in the capital of a country
    • Reveals the N last private messages sent from and to that player
    • Counter-intelligence may garble the messages, or change the sender and receiver of messages [ ] Counter-intelligence tech
  • [ ] Extremely expensive [ ] Spy satellites
  • [ ] New space launch center
    • Extremely expensive
    • [ ] allow "send spy satellite" action
      • Everyone is made aware that a satellite was launched
  • Reveal a random 2-tile-radius chunk of the map every two days

Plus the WMD follow-up:

[ ] Foreign intelligence may be warned that you are enriching uranium


4. Design decisions taken so far

  1. Propaganda lives on the Intelligence tab; Politics keeps the rest. Campaigns are a covert action, so they belong with spies.
  2. The spy is military: false — it never joins a melee, never captures a city, and right-click/attack rules treat it like a worker. It fights nothing.
  3. Spy invisibility is unconditional for now. Unlike a submarine (revealed by an adjacent naval unit), a spy is hidden from every other nation. A detected/revealed state, driven by counter-intelligence, is deliberately left open (see §6).
  4. A spy crosses foreign territory as a through-route, not only as a goal. This is the one exception to the "foreign land is a wall" rule and is mirrored client-side so prediction agrees. A spy may also board a friendly launch like infantry (requestEmbark accepts spy as well as infantry, and _tryTransportOrder mirrors it), so a land-locked spy can reach an island.
  5. New technologies may be unlock-only (unlocks: true), so intelligence_agencies and counter_intelligence carry no numeric effect. The data_test rule was relaxed accordingly.
  6. The spy is gated by intelligence_agencies (not by a building), because the roadmap specifies no spy building. counter_intelligence is deliberately "extremely expensive" (1,500 base cost × the 100× research multiplier = 150,000 points). A gated unit is not hidden from the city train list: every trainable unit shows, locked ones as a dimmed card carrying "Requires Intelligence Agencies" (_trainableUnits / _unitRequirement in panels.js, the locked branch of city.js _renderTrainList), so a player discovers the spy before researching the agency.
  7. Spy operations are unit actions in the unit stack panel. An operation belongs to a spy, so the buttons sit beside the ranged Attack in the unit panel (#unit-spy-actions), not in the Intelligence tab. The tab keeps only the roster (with Locate). Sabotage acts on the spy's own tile, so it needs no target picker; a future operation that points at a distant tile can still arm the same overlay the Attack uses. Unwired operations stay disabled ("Not yet available").
  8. Sabotage zeroes the tile's production, using the same productionFactors mechanism as pillaging and trenches rather than a second production model. It lasts a week (SABOTAGE.blockDays). It leaves the physical resource trade alone for now — as pillaging does — and the culprit is not yet named; the blame roll and false-flag choice belong to counter-intelligence (Phase C).
  9. A spy is single-use. Every covert operation (sabotage, city spying, comms spying) removes the whole acting stack through _spendSpies — quietly, with no news line and no notice to the owner or the victim. The only reason to send more than one spy is to overwhelm counter-intelligence: each spy past the first cancels ESPIONAGE.agentsPerCounter of the target's service (_effectiveCounterIntelligence), so three spies beat a full service outright. The operation button's tooltip says the spies are spent.

5. Open design questions (settle these before coding)

  1. Where do spy actions live in the UI? Settled (Phase A): the unit stack panel, plus map targeting. Operations are spy unit actions, so their buttons live in the unit panel (#unit-spy-actions) with the ranged Attack; Phase B arms the map's target-picking overlay (the Attack pattern: _toggleTargeting, map.beginTargeting, #layer-targeting, map.onTargetChosen) once an operation is chosen. The Intelligence tab keeps only the spy roster (decision 7).
  2. How is a job resolved and paid for? Settled: every operation is free, instantaneous and spends the acting spies. A spy is single-use: the whole selected stack is removed quietly, with no news and no notice. The cost is those spies and the risk of being blamed; a bigger stack blunts the target's counter-intelligence (ESPIONAGE.agentsPerCounter), so stacking is the one lever against it. No cooldown.
  3. Sabotage specifics. Settled (Phase B): the spy must stand on the target tile (foreign land); the block is the tile's production, zeroed for SABOTAGE.blockDays days through productionFactors. Stored as GameState.tileSabotage (key → end hour), pruned on the hourly tick and shipped in the snapshot, so it survives a reload. The physical resource trade is not (yet) affected — see decision 8.
  4. Blame model. Settled (Phase C): _blameCulprit returns the author (identified), the chosen scapegoat (false flag), or null. The message carries only suspect, so a correct blame and a successful false flag read identically. The framed nation has no special reaction yet — the blame is only a news line.
  5. City spying report. Settled (Phase B): a one-off report listing population, building levels and garrison, kept in the owner's report list. Counter-intelligence jitters the population and nudges building/garrison counts by one, never marking the report as suspect.
  6. Comms spying needs stored messages. Settled (Phase D): a server message store exists (GameState.messages, fed by recordChat, capped at ESPIONAGE.messageLogLimit) and chat gained private recipients. Comms spying reads the last ESPIONAGE.interceptCount messages involving the target.
  7. Counter-intelligence mechanics. Settled (Phase C): one permanent unlock tech, read through _counterIntelligenceLevel(civ). It raises blame accuracy, falsifies city reports and garbles comms. It does not hide spies — they stay unconditionally invisible (decision 3).
  8. Satellites. Settled (Phase E): one satellite per centre level, launched from a city holding the centre, free once built; the launch is public news. Each satellite uncovers a random 2-tile patch of permanently explored land every two days, forever, and multiple satellites stack.
  9. Uranium warning. Settled (Phase F): an automatic chance roll when a nation's first enrichment centre is completed publishes a world news warning; no spy needs to be present.
  10. Spy visibility on the map for its owner. Settled: the owner always sees its own spy. A spy cannot enter a hostile city tile (the movement rules keep non-military units out), so city and comms spying watch from the city tile's own hex or a neighbour.

6. Suggested implementation plan

Phase A — tab completion. [done] Added the spy-controls section to the Intelligence tab (the "Your spies" roster with Locate, and the "Covert operations" list) and settled question 1: the tab is the control surface, the map overlay will pick targets. Added shared/data/intelligence.js (the operation catalogue) and the spies/spyOperations fields of _intelligenceInfo().

Phase B — one covert action end to end. [done: sabotage] Picked sabotage: it is self-contained (no private report UI) and its block is world state. Added the sabotage order + GameServer._handleSabotage validation, the tileSabotage snapshot field, the NEWS_SABOTAGE notification, the productionFactors block and the client mirror. City spying is next. The target-picking overlay was not needed because sabotage acts on the spy's own tile.

Phase C — counter-intelligence. [done] _intelligenceLevel / _counterIntelligenceLevel feed _blameCulprit, report falsification and comms garbling.

Phase D — comms spying. [done] The server records chat through GameState.recordChat (with private recipients), and requestCommsSpy files an interception report, garbled when the target has counter-intelligence.

Phase E — satellites. [done] space_launch_center, requestSatellite, the _tickSatellites reveal cadence and the NEWS_SATELLITE launch notice.

Phase F — uranium warning. [done] _completeBuilding → _warnOfEnrichment → NEWS_ENRICHMENT, a chance roll on the first enrichment centre.


7. Data-model sketch

Names for discussion; keep them cheap to serialise.

  • Operation catalogue: shared/data/intelligence.js SPY_OPERATIONS ({ id, name, description }), with the id constants SPY_SABOTAGE, SPY_CITY, SPY_COMMS. The server will validate orders against these ids.
  • GameState.spies — not needed; spies are ordinary units with proto.spy. Single-use is not stored either: an operation calls _spendSpies on the acting units at once.
  • Sabotage block (done): GameState.tileSabotage (key → end hour), pruned in _tickSabotage and read by isTileSabotaged from productionFactors. Shipped as tileSabotage: [[x, y, untilHour], ...]; small enough not to need a version counter.
  • Reports (done): GameState.spyReports (civ → capped list) holding city dossiers and comms intercepts; shipped to their owner only in viewerSnapshot.
  • Comms store (done): GameState.messages, { from, to, text, hours }, capped at ESPIONAGE.messageLogLimit; never serialised.
  • Satellites (done): GameState.satellites as { civ, launchedHours, nextRevealHour }; the reveal adds to explored and marks visibility dirty. Shipped whole (tiny) as satellites.
  • Counter-intelligence: _counterIntelligenceLevel(civ) read from the researched set; no stored state, one permanent tech. _effectiveCounterIntelligence(civ, agents) scales it down by the acting stack, and is the one place the team size touches the blame roll, report falsification and comms garbling.

No new large snapshot collection was added, so no DELTA_COLLECTIONS entry or version counter is needed. covertOps was never needed — each operation acts immediately rather than as a stored job.


8. Server / orders / client plumbing

  • New orders go through GameServer validation in server/game_server.js (see how _handleAttack, requestTrain, treaties are handled) and land on GameState methods. Never trust client-only flags outside --testing.
  • News/notifications: add a NEWS_<THING> constant in shared/data/relations.js (the others live there) and dispatch it from the relevant GameState method; the client feed renders it via client/js/modals/format.js describeNews, and the game screen pushes alert lines in client/js/game_screen/panels.js _updateFeed/_pushNews. A clickable feed line (like a treaty offer) is the model for "someone blamed X for the sabotage".
  • Client actions: wire a map.on<Action>Chosen callback for the target-picking overlay (mirror the ranged Attack flow: client/js/map_view/motion.js beginTargeting/_drawRangeBoundary, client/js/map_view/input.js _onLeftClick/_onRightClick, client/js/game_screen.js onTargetChosen/onTargetCancelled, client/js/game_screen/panels.js _toggleTargeting/_chooseTarget).

9. Tests

Done in Phase A:

  • tests/spy_test.js test_the_covert_operations_are_well_formed: the SPY_OPERATIONS catalogue has unique, non-empty ids/names/descriptions and carries all three operation ids.
  • tests/game_screen_test.js test_intelligence_subtabs_split_spies_and_propaganda: the two sub-tabs exist, Spies opens first and switching toggles the views.
  • tests/game_screen_test.js test_intelligence_tab_lists_the_viewers_spies_and_locates_them: a spawned spy shows in the roster with its city and a Locate control, and clicking it selects the spy and closes the panel.
  • tests/game_screen_test.js test_spy_actions_live_in_the_unit_panel: selecting a spy reveals #unit-spy-actions with one button per SPY_OPERATIONS entry, selecting a soldier hides it, and the tab carries no operations card.
  • The campaign tests click the Propaganda sub-tab before reaching for a campaign control.

Done in Phases B–F:

  • tests/intelligence_test.js (11 tests): sabotage zeroes and restores a foreign tile; own land and non-spies are refused; the blame roll pins the author, a framed nation, or no one (forced RNG sequences); counter-intelligence raises identification; city reports are accurate, then falsified when the target has counter-intelligence; comms are intercepted clean, then garbled; a satellite launches, is capped by centre levels, and uncovers explored tiles on its cadence; the enrichment warning fires on a chance roll.
  • tests/game_screen_test.js adds test_city_spying_arms_targeting_and_sends (the city-spy button arms the target overlay and sends the order) and test_launch_satellite_button_sends (the tab's launch control sends the order).
  • tests/game_server_test.js validates the spy_city and launch_satellite orders and rejects another nation's spy.
  • tests/spy_test.js grows: a spy cannot capture, cannot fight, and cannot enter a hostile city tile.
  • tests/intelligence_test.js covers single-use: an operation destroys the acting spies and no others; _effectiveCounterIntelligence steps 1 → 0.5 → 0 as the team grows; a three-spy team escapes a blame roll a lone spy would fail; and a three-spy city report stays honest against a counter-intelligence service.
  • Client: tests/map_view_input_test.js for target-picking of a spy action.

Determinism: state._random = () => value is the established way to pin RNG outcomes.


10. Gotchas

  • Chat is now stored in GameState.messages (fed by recordChat); it is never serialised, so it stays on the server.
  • Invisibility and snapshot size. _hiddenEnemyUnits is consulted on the snapshot path; keep the check cheap and version-aware.
  • Prediction mirroring for any movement/visibility change (server movement.js ↔ client motion.js).
  • Do not memoise getCityEconomy outside a snapshot and keep new snapshot walks version-keyed (see the AGENTS.md notes on the snapshot path).
  • Tech-tree ripple. Adding technologies changes nothing count-based (TECHNOLOGIES.length is used by tests) but does change the tree; run game_state_technology_test.js, data_test.js, modals_test.js and game_screen_test.js.
  • unlocks techs are exempt from the effects rule; if you give counter_intelligence a numeric effect later, drop unlocks.

11. Relevant files

  • Tab / modal: client/index.html (#tab-intelligence, #unit-spy-actions), client/js/modals/nation.js (_renderIntelligence, campaign methods), client/js/game_screen/panels.js (_intelligenceInfo, _updateSpyActions, _buildSpyActionButtons, news feed).
  • Spy data/behaviour: shared/data/units.js, shared/data/technologies.js, shared/data/intelligence.js (SPY_OPERATIONS, SABOTAGE, ESPIONAGE, SATELLITE, report kinds), shared/game_state/intelligence.js (the whole covert-op mixin: sabotage, blame, city/comms reports, satellites, secrets), shared/game_state/visibility.js, shared/game_state/movement.js, client/js/map_view/motion.js.
  • Production block: shared/rules.js (productionFactors sabotaged), shared/game_state/economy.js (getTileGdpPerCapita, _completeBuilding), shared/game_state/serialization.js (_serializeSabotage, viewer spyReports/satellites).
  • Building: shared/data/buildings.js (space_launch_center, BUILDING_MECHANIC.SPACE_LAUNCH), client/assets/icon_space_launch_center.svg.
  • Orders/server/news: server/game_server.js (_handleSabotage, _handleSpyCity, _handleSpyComms, _handleLaunchSatellite), server/server.js (case "chat" with to, recordChat), shared/game_state/orders.js, shared/data/relations.js (NEWS_SABOTAGE, NEWS_SATELLITE, NEWS_ENRICHMENT), client/js/modals/format.js describeNews.
  • Client views: client/js/modals/nation.js (_buildReportsCard, _fillReports, _buildSatellitesCard, _fillSatellites, onLaunchSatellite), client/js/game_screen.js (_chatTarget, _updateChatTargets, addChat), client/index.html (#chat-target).
  • Target picking to imitate: client/js/map_view/motion.js, client/js/map_view/input.js, client/js/game_screen.js, client/js/game_screen/panels.js.
  • Chat (for comms spying): server/server.js case "chat", client/js/net.js, client/js/app.js, client/js/game_screen.js (openChat/addChat).
  • Tests: tests/intelligence_test.js, tests/spy_test.js, tests/game_screen_test.js, tests/game_server_test.js, tests/modals_test.js, tests/data_test.js.
  • Previews: /tmp/tismo-preview/intelligence-complete.html (reports, satellites, private chat), intelligence-phase-a.html, intelligence.html, intelligence-spy-actions.html.

STACKS.md, AIR_MOVEMENT.md, ECONOMY_BALANCE.md, POLITICS_PERFORMANCE.md, TRAINING_UI.md, ICONS.md (siblings in the repo root).