One shared site rule decides where a structure stands, and structures stand on a city's open ground #129

Owner

Tracker: #3608

This PR puts the structure site rule in one function in src/shared/site.ts, and every site calls it. A structure may now stand on a city's open ground, off its streets, its plots and the monument's block.

The rule

  • siteRefusal answers each tile with a const enum SiteRefusal member, or null when the tile is open. The members are: off the map, terrain closed to the site's domain, a unit holds it, a street, a standing building's plot, the monument's block, and beyond reach. siteOpen accepts a footprint when every tile is open.
  • Which units hold a tile: a live unit that is not an aircraft.
    • The server reads this from its pathfinder. Game.reseat now skips aircraft, as Game.spawn does, so a reload no longer closes the tiles under a hovering Gunship.
    • The client and the AI read it from the units they know, through heldTiles. The replica's route map uses the same holdsGround test.
    • So a ghost can show open over a tile where a unit the player cannot see stands, and the server then refuses the site as held. Reach runs 10 tiles past a city's ground, while a city sees 10 tiles from its town, so such a tile can lie within reach.
  • Which terrain each site reads: its own. The server reads its world. The client and the AI read the replica's, where an unexplored tile reads as Obstacle and is refused as closed terrain.
  • These all call the rule:
    • Game.placeBuilding
    • the client's ghost, per cell, and its commit
    • the AI's siteNear
    • the towns bench's factorySite
  • clearOfCities, canPlaceFootprint, footprintNearCity, Replica.nearCity and Replica.clearOfCities are gone. inReach in cities.ts answers reach per tile.

Where structures go

  • preferredSite, in the same file, ranks the sites the rule accepts. A site whose footprint keeps off a given set of cities' ground ranks above one on it. It returns the first site of the best rank, in the caller's search order.
  • placeNearAnchor ranks its anchor's spawn steps against every city.
  • The net bench's churn stage ranks its PLACE_STEPS the same way. Its remote players keep their rotation, and they skip a try the rule accepts only below the best rank their steps offer.
  • The AI's siteNear ranks against every city it knows the layout of, its own, neutral and enemy alike. So it takes the first site of its ring that keeps off every city's ground, and a site on a city's open ground only when the ring has no other. This amends the plan's D5, which ranked against the AI's own cities only: under that ranking an AI seat in churn-128 re-placed its factory on a neutral city's open ground, and p1's deflated B/s fell 30% from base.
  • So the economy case, the churn case's outposts, the Nuke yard and the tests that place through placeNearAnchor keep main's sites. Tests pin them:
    • net.test.ts stages economy and churn at seed 11, 128 and 256. net.ts exports stageCase for this.
    • The Nuke yard's setup moves into src/mock/strike-setup.ts, which imports no browser or render module. src/mock/strike-setup.test.ts pins its six structures.

Tests

  • src/shared/site.test.ts pins each closed case with its refusal, an open tile of a city's ground, and an open tile under a hovering Gunship.
  • ground.test.ts's two neighbouring towns form the agreement test, with these sweeps:
    • Over every wholly explored 2×2 origin on either ground, the server, the client ghost's cells and the AI's rule agree.
    • A's streets, plots and block refuse.
    • B's explored street, 23,12, refuses a 1×1 Turret.
    • B's explored open ground within reach accepts.
    • A Gunship's tile accepts a Turret, also after a save and load.
    • Wholly unexplored origins on B's open ground are refused by the ghost and the AI as closed terrain, and accepted by the server.
    • Each sweep asserts its set is non-empty.
  • game.test.ts's roundabout case refuses a capture cell and a street, and places a Turret on an open tile of the city's ground.
  • A buildings.test.ts case pins that placeNearAnchor takes a city tile only when its steps offer no site off every city's ground.
  • player.test.ts pins the AI's ranking: its factory keeps off its home city's ground and a neutral city's ground while its ring offers a site off both, and takes a city's open ground only when it offers none.

Help and changelog

  • The help's Build line, Building section and Cities section say a structure may stand on a city's open ground, within reach, and that the city grows around it.
  • The changelog's "Cities grow up, not out" bullet no longer says structures stand off every city's ground. A new Current bullet records building inside cities. It says a ghost that shows open places unless a unit the player cannot see stands there.

Checks, against base 5da81cc

  • sak-gate full run 1 passed on 54d8ad4: npm test 864/864 and test:phone 65/65.

  • The bench and AI checks below ran on a00e00f. 54d8ad4 changes only the changelog's text, so they stand.

  • Net bench at seed 11 over wan, on esbuild bundles of both arms. Idle, battle, assault, economy, cities and siege at 128 and 256 match base exactly: frame, game and command hashes and every byte. Churn moves within 5%:

    case commands p1 raw B/s p1 deflated B/s p2 raw B/s p2 deflated B/s
    churn-128 650 → 547 1600 → 1587 (−0.8%) 697 → 684 (−1.8%) 3110 → 3115 (+0.2%) 969 → 968 (−0.1%)
    churn-256 517 → 444 1785 → 1807 (+1.3%) 558 → 579 (+3.8%) 2966 → 2957 (−0.3%) 966 → 943 (−2.3%)

    Churn's deltas come from the remote players' skipped tries. Each command a remote session sends advances its ack, and that costs a frame. When the skipped tries were sent as refused off-map placements, churn-256 matched base exactly.

  • The 14 AI matches, each seeded through Math.random: small 1–6 with 2 seats, medium 1–4 with 3, and large 1–4 with 4. All ran to their end on both arms. 11 traces match base exactly. medium-1, medium-2 and large-3 differ partway through and still end at the 20-minute limit with the same standing seats.

  • npm run towns, measured at db394b0: "No Factory site at founding" goes from 0 / 5 / 7 to 0 / 0 / 0 at Lakes 0.25 / 0.9 / 1. Its other 20 rows are identical. The towns and traffic benches do not reach siteNear, so a00e00f leaves these rows as they are.

Tracker: [#3608](https://sak.swipelab.com/tasks/3608) This PR puts the structure site rule in one function in `src/shared/site.ts`, and every site calls it. A structure may now stand on a city's open ground, off its streets, its plots and the monument's block. ## The rule - `siteRefusal` answers each tile with a `const enum SiteRefusal` member, or null when the tile is open. The members are: off the map, terrain closed to the site's domain, a unit holds it, a street, a standing building's plot, the monument's block, and beyond reach. `siteOpen` accepts a footprint when every tile is open. - **Which units hold a tile:** a live unit that is not an aircraft. - The server reads this from its pathfinder. `Game.reseat` now skips aircraft, as `Game.spawn` does, so a reload no longer closes the tiles under a hovering Gunship. - The client and the AI read it from the units they know, through `heldTiles`. The replica's route map uses the same `holdsGround` test. - So a ghost can show open over a tile where a unit the player cannot see stands, and the server then refuses the site as held. Reach runs 10 tiles past a city's ground, while a city sees 10 tiles from its town, so such a tile can lie within reach. - **Which terrain each site reads:** its own. The server reads its world. The client and the AI read the replica's, where an unexplored tile reads as Obstacle and is refused as closed terrain. - These all call the rule: - `Game.placeBuilding` - the client's ghost, per cell, and its commit - the AI's `siteNear` - the towns bench's `factorySite` - `clearOfCities`, `canPlaceFootprint`, `footprintNearCity`, `Replica.nearCity` and `Replica.clearOfCities` are gone. `inReach` in cities.ts answers reach per tile. ## Where structures go - `preferredSite`, in the same file, ranks the sites the rule accepts. A site whose footprint keeps off a given set of cities' ground ranks above one on it. It returns the first site of the best rank, in the caller's search order. - `placeNearAnchor` ranks its anchor's spawn steps against every city. - The net bench's churn `stage` ranks its `PLACE_STEPS` the same way. Its remote players keep their rotation, and they skip a try the rule accepts only below the best rank their steps offer. - The AI's `siteNear` ranks against every city it knows the layout of, its own, neutral and enemy alike. So it takes the first site of its ring that keeps off every city's ground, and a site on a city's open ground only when the ring has no other. This amends the plan's D5, which ranked against the AI's own cities only: under that ranking an AI seat in churn-128 re-placed its factory on a neutral city's open ground, and p1's deflated B/s fell 30% from base. - So the economy case, the churn case's outposts, the Nuke yard and the tests that place through `placeNearAnchor` keep main's sites. Tests pin them: - net.test.ts stages economy and churn at seed 11, 128 and 256. net.ts exports `stageCase` for this. - The Nuke yard's setup moves into src/mock/strike-setup.ts, which imports no browser or render module. src/mock/strike-setup.test.ts pins its six structures. ## Tests - src/shared/site.test.ts pins each closed case with its refusal, an open tile of a city's ground, and an open tile under a hovering Gunship. - ground.test.ts's two neighbouring towns form the agreement test, with these sweeps: - Over every wholly explored 2×2 origin on either ground, the server, the client ghost's cells and the AI's rule agree. - A's streets, plots and block refuse. - B's explored street, 23,12, refuses a 1×1 Turret. - B's explored open ground within reach accepts. - A Gunship's tile accepts a Turret, also after a save and load. - Wholly unexplored origins on B's open ground are refused by the ghost and the AI as closed terrain, and accepted by the server. - Each sweep asserts its set is non-empty. - game.test.ts's roundabout case refuses a capture cell and a street, and places a Turret on an open tile of the city's ground. - A buildings.test.ts case pins that `placeNearAnchor` takes a city tile only when its steps offer no site off every city's ground. - player.test.ts pins the AI's ranking: its factory keeps off its home city's ground and a neutral city's ground while its ring offers a site off both, and takes a city's open ground only when it offers none. ## Help and changelog - The help's Build line, Building section and Cities section say a structure may stand on a city's open ground, within reach, and that the city grows around it. - The changelog's "Cities grow up, not out" bullet no longer says structures stand off every city's ground. A new Current bullet records building inside cities. It says a ghost that shows open places unless a unit the player cannot see stands there. ## Checks, against base 5da81cc - `sak-gate full` run 1 passed on 54d8ad4: `npm test` 864/864 and `test:phone` 65/65. - The bench and AI checks below ran on a00e00f. 54d8ad4 changes only the changelog's text, so they stand. - Net bench at seed 11 over wan, on esbuild bundles of both arms. Idle, battle, assault, economy, cities and siege at 128 and 256 match base exactly: frame, game and command hashes and every byte. Churn moves within 5%: | case | commands | p1 raw B/s | p1 deflated B/s | p2 raw B/s | p2 deflated B/s | |---|---|---|---|---|---| | churn-128 | 650 → 547 | 1600 → 1587 (−0.8%) | 697 → 684 (−1.8%) | 3110 → 3115 (+0.2%) | 969 → 968 (−0.1%) | | churn-256 | 517 → 444 | 1785 → 1807 (+1.3%) | 558 → 579 (+3.8%) | 2966 → 2957 (−0.3%) | 966 → 943 (−2.3%) | Churn's deltas come from the remote players' skipped tries. Each command a remote session sends advances its ack, and that costs a frame. When the skipped tries were sent as refused off-map placements, churn-256 matched base exactly. - The 14 AI matches, each seeded through `Math.random`: small 1–6 with 2 seats, medium 1–4 with 3, and large 1–4 with 4. All ran to their end on both arms. 11 traces match base exactly. medium-1, medium-2 and large-3 differ partway through and still end at the 20-minute limit with the same standing seats. - `npm run towns`, measured at db394b0: "No Factory site at founding" goes from 0 / 5 / 7 to 0 / 0 / 0 at Lakes 0.25 / 0.9 / 1. Its other 20 rows are identical. The towns and traffic benches do not reach `siteNear`, so a00e00f leaves these rows as they are.
siteRefusal answers each tile off the map, closed terrain, held by a unit,
a street, a standing building's plot, the monument's block or beyond reach.
Only live units that are not aircraft hold tiles: the server reads its
pathfinder, which Game.reseat now fills as Game.spawn does, and the client
and the AI read the units they know through heldTiles.

placeBuilding, the client's ghost and commit, the AI's siteNear and the
towns bench call it. preferredSite ranks a site off the given cities' ground
first, so placeNearAnchor, the churn outposts and the AI keep to the sites
off the ground while there are any.
agra merged commit 16cad70a29 into task/3558-build-inside-the-city-and-a-flat-map 2026-10-22 05:28:02 +03:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
agra/mini-alert!129
No description provided.