33 KiB
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.jsis the transport. - Shared logic runs in both Node and the browser.
shared/must stay framework-free: no Node built-ins, no DOM.server/websocket.jsis the only place Node built-ins are expected. The browser is a view (client/js/map_view.jsand friends) that rebuilds terrain from the shared seed and renders snapshots. - GameState is composed of mixins.
shared/game_state.jsimports eachshared/game_state/<topic>.jsandObject.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 thatObject.assign. - Data-driven content lives in
shared/data/(one module per topic:units.js,buildings.js,technologies.js,relations.js, ...).shared/data.jsis a barrel — import from it, edit the file undershared/data/. - Client mirroring. Movement, pathfinding and movement-cost changes must be
mirrored between
shared/game_state/movement.jsand the browser's prediction inclient/js/map_view/motion.js, or prediction fights the server. - Snapshot performance.
_broadcastStaterebuilds the snapshot whenever the state is dirty (orders, hourly ticks). Large collections are versioned inDELTA_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(onlyc8andjsdomfor 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 openhttp://127.0.0.1:27015/. Add--testingfor the free/instant buttons. - If
nodeis missing:nix-shell -p nodejs --run "node tests/run_tests.js".
Conventions
- 2-space indent for
.js. No new libraries (only vendored jQuery underclient/vendor/). - Missing icon → add a dummy SVG reading
dummy icon - <name>. - Tests live in
tests/<topic>_test.js, extendTestCase(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
- Propaganda lives on the Intelligence tab; Politics keeps the rest. Campaigns are a covert action, so they belong with spies.
- 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. - 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).
- 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 (
requestEmbarkacceptsspyas well asinfantry, and_tryTransportOrdermirrors it), so a land-locked spy can reach an island. - New technologies may be unlock-only (
unlocks: true), sointelligence_agenciesandcounter_intelligencecarry no numeric effect. Thedata_testrule was relaxed accordingly. - The spy is gated by
intelligence_agencies(not by a building), because the roadmap specifies no spy building.counter_intelligenceis 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/_unitRequirementinpanels.js, thelockedbranch ofcity.js_renderTrainList), so a player discovers the spy before researching the agency. - 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"). - Sabotage zeroes the tile's production, using the same
productionFactorsmechanism 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). - 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 cancelsESPIONAGE.agentsPerCounterof 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)
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).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.Sabotage specifics.Settled (Phase B): the spy must stand on the target tile (foreign land); the block is the tile's production, zeroed forSABOTAGE.blockDaysdays throughproductionFactors. Stored asGameState.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.Blame model.Settled (Phase C):_blameCulpritreturns the author (identified), the chosen scapegoat (false flag), or null. The message carries onlysuspect, 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.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.Comms spying needs stored messages.Settled (Phase D): a server message store exists (GameState.messages, fed byrecordChat, capped atESPIONAGE.messageLogLimit) and chat gained private recipients. Comms spying reads the lastESPIONAGE.interceptCountmessages involving the target.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).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.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.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.jsSPY_OPERATIONS({ id, name, description }), with the id constantsSPY_SABOTAGE,SPY_CITY,SPY_COMMS. The server will validate orders against these ids. GameState.spies— not needed; spies are ordinary units withproto.spy. Single-use is not stored either: an operation calls_spendSpieson the acting units at once.- Sabotage block (done):
GameState.tileSabotage(key → end hour), pruned in_tickSabotageand read byisTileSabotagedfromproductionFactors. Shipped astileSabotage: [[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 inviewerSnapshot. - Comms store (done):
GameState.messages,{ from, to, text, hours }, capped atESPIONAGE.messageLogLimit; never serialised. - Satellites (done):
GameState.satellitesas{ civ, launchedHours, nextRevealHour }; the reveal adds toexploredand marks visibility dirty. Shipped whole (tiny) assatellites. - 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
GameServervalidation inserver/game_server.js(see how_handleAttack,requestTrain, treaties are handled) and land onGameStatemethods. Never trust client-only flags outside--testing. - News/notifications: add a
NEWS_<THING>constant inshared/data/relations.js(the others live there) and dispatch it from the relevantGameStatemethod; the client feed renders it viaclient/js/modals/format.jsdescribeNews, and the game screen pushes alert lines inclient/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>Chosencallback for the target-picking overlay (mirror the ranged Attack flow:client/js/map_view/motion.jsbeginTargeting/_drawRangeBoundary,client/js/map_view/input.js_onLeftClick/_onRightClick,client/js/game_screen.jsonTargetChosen/onTargetCancelled,client/js/game_screen/panels.js_toggleTargeting/_chooseTarget).
9. Tests
Done in Phase A:
tests/spy_test.jstest_the_covert_operations_are_well_formed: theSPY_OPERATIONScatalogue has unique, non-empty ids/names/descriptions and carries all three operation ids.tests/game_screen_test.jstest_intelligence_subtabs_split_spies_and_propaganda: the two sub-tabs exist, Spies opens first and switching toggles the views.tests/game_screen_test.jstest_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.jstest_spy_actions_live_in_the_unit_panel: selecting a spy reveals#unit-spy-actionswith one button perSPY_OPERATIONSentry, 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.jsaddstest_city_spying_arms_targeting_and_sends(the city-spy button arms the target overlay and sends the order) andtest_launch_satellite_button_sends(the tab's launch control sends the order).tests/game_server_test.jsvalidates thespy_cityandlaunch_satelliteorders and rejects another nation's spy.tests/spy_test.jsgrows: a spy cannot capture, cannot fight, and cannot enter a hostile city tile.tests/intelligence_test.jscovers single-use: an operation destroys the acting spies and no others;_effectiveCounterIntelligencesteps 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.jsfor 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 byrecordChat); it is never serialised, so it stays on the server. - Invisibility and snapshot size.
_hiddenEnemyUnitsis consulted on the snapshot path; keep the check cheap and version-aware. - Prediction mirroring for any movement/visibility change (server
movement.js↔ clientmotion.js). - Do not memoise
getCityEconomyoutside 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.lengthis used by tests) but does change the tree; rungame_state_technology_test.js,data_test.js,modals_test.jsandgame_screen_test.js. unlockstechs are exempt from the effects rule; if you givecounter_intelligencea numeric effect later, dropunlocks.
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(productionFactorssabotaged),shared/game_state/economy.js(getTileGdpPerCapita,_completeBuilding),shared/game_state/serialization.js(_serializeSabotage, viewerspyReports/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"withto,recordChat),shared/game_state/orders.js,shared/data/relations.js(NEWS_SABOTAGE,NEWS_SATELLITE,NEWS_ENRICHMENT),client/js/modals/format.jsdescribeNews. - 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.jscase "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.
12. Related design docs
STACKS.md, AIR_MOVEMENT.md, ECONOMY_BALANCE.md, POLITICS_PERFORMANCE.md,
TRAINING_UI.md, ICONS.md (siblings in the repo root).