mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 10:52:06 +00:00
refactor: standardize cardinal-neighbor iteration on neighbors() N,S,W,E order (#4495)
## Summary Follow-up to #4494. That PR added `forEachNeighborNSWE` as a third neighbor iterator because the existing allocation-free helpers (`forEachNeighbor`, `neighbors4`) visit in W,E,N,S order while `neighbors()` visits N,S,W,E — and substituting one for the other changes simulation behavior at order-sensitive call sites. This PR removes that duplication by standardizing on **one order everywhere**: `forEachNeighbor` and `neighbors4` now visit in the same N,S,W,E order as `neighbors()`, and `forEachNeighborNSWE` is deleted. ## ⚠️ Intentional behavior change Callers of the flipped helpers that are order-sensitive now make different (equally valid) decisions: - `AttackExecution.addNeighbors` — PRNG values are drawn per neighbor while building the conquest frontier, so attack expansion patterns differ - `AttackExecution.handleDeadDefender` — a dead defender's tiles go to the *first-visited* adjacent player - `WarshipExecution.bestNeighborToward` — distance ties break by visit order - `PlayerExecution` surrounded-cluster flood fill — set insertion order propagates to conquer order Game outcomes for a given seed differ from previous builds (verified: the 12k-tick reference run ends with 31 players alive vs 24 before). Determinism across clients *within* a build is unaffected — all clients run the same code, so there is no desync risk. Replays/verification pinned to old hashes will not match this build. New reference hashes for the headless perf harness (seed `perf-default`): | Run | Final hash | |---|---| | giantworldmap, 12,000 ticks | `57830793797434300` | | giantworldmap, 2,000 ticks | `55125379638382860` | | world, 1,800 ticks | `32337437717390864` | ## Verification - [x] Full suite green (1,901 tests), including new exact-order contract tests: `forEachNeighbor` and `neighbors4` must match `neighbors()` contents **and order** for every tile - [x] 20-game-minute Giant World Map benchmark: no perf regression (73 ticks/sec, GC 1.2% of wall, allocation profile unchanged) - [x] Order-sensitivity audit of every `forEachNeighbor`/`neighbors4` call site (sensitive ones listed above; the rest are booleans, counts, or min/max accumulations) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,59 +25,57 @@ describe("Neighbor iteration", () => {
|
||||
game = await setup("ocean_and_land"); // 16x16
|
||||
});
|
||||
|
||||
test("forEachNeighbor visits W, E, N, S in that exact order for interior tiles", () => {
|
||||
test("forEachNeighbor visits N, S, W, E in that exact order for interior tiles", () => {
|
||||
const tile = game.ref(5, 7);
|
||||
expect(collectNeighbors(tile)).toEqual([
|
||||
game.ref(4, 7),
|
||||
game.ref(6, 7),
|
||||
game.ref(5, 6),
|
||||
game.ref(5, 8),
|
||||
game.ref(4, 7),
|
||||
game.ref(6, 7),
|
||||
]);
|
||||
});
|
||||
|
||||
test("forEachNeighbor clips at corners and edges", () => {
|
||||
const w = game.width();
|
||||
const h = game.height();
|
||||
// top-left corner: E, S only
|
||||
// top-left corner: S, E only
|
||||
expect(collectNeighbors(game.ref(0, 0))).toEqual([
|
||||
game.ref(1, 0),
|
||||
game.ref(0, 1),
|
||||
game.ref(1, 0),
|
||||
]);
|
||||
// bottom-right corner: W, N only
|
||||
// bottom-right corner: N, W only
|
||||
expect(collectNeighbors(game.ref(w - 1, h - 1))).toEqual([
|
||||
game.ref(w - 2, h - 1),
|
||||
game.ref(w - 1, h - 2),
|
||||
game.ref(w - 2, h - 1),
|
||||
]);
|
||||
// left edge: E, N, S
|
||||
// left edge: N, S, E
|
||||
expect(collectNeighbors(game.ref(0, 5))).toEqual([
|
||||
game.ref(1, 5),
|
||||
game.ref(0, 4),
|
||||
game.ref(0, 6),
|
||||
game.ref(1, 5),
|
||||
]);
|
||||
// bottom edge: W, E, N
|
||||
// bottom edge: N, W, E
|
||||
expect(collectNeighbors(game.ref(5, h - 1))).toEqual([
|
||||
game.ref(5, h - 2),
|
||||
game.ref(4, h - 1),
|
||||
game.ref(6, h - 1),
|
||||
game.ref(5, h - 2),
|
||||
]);
|
||||
});
|
||||
|
||||
test("forEachNeighbor matches map.neighbors() as a set for every tile", () => {
|
||||
// All cardinal-neighbor helpers share neighbors()'s exact N, S, W, E order,
|
||||
// including at edges and corners, so they are interchangeable even in
|
||||
// order-sensitive simulation code.
|
||||
test("forEachNeighbor matches map.neighbors() exactly (contents and order) for every tile", () => {
|
||||
game.forEachTile((tile) => {
|
||||
const a = [...collectNeighbors(tile)].sort((x, y) => x - y);
|
||||
const b = [...game.map().neighbors(tile)].sort((x, y) => x - y);
|
||||
expect(a).toEqual(b);
|
||||
expect(collectNeighbors(tile)).toEqual(game.map().neighbors(tile));
|
||||
});
|
||||
});
|
||||
|
||||
// forEachNeighborNSWE's contract is exact order equality with neighbors(),
|
||||
// including at edges and corners, so order-sensitive code can use the two
|
||||
// interchangeably.
|
||||
test("forEachNeighborNSWE matches map.neighbors() exactly (contents and order) for every tile", () => {
|
||||
test("neighbors4 matches map.neighbors() exactly (contents and order) for every tile", () => {
|
||||
const nbuf: TileRef[] = [0, 0, 0, 0];
|
||||
game.forEachTile((tile) => {
|
||||
const out: TileRef[] = [];
|
||||
game.forEachNeighborNSWE(tile, (n) => out.push(n));
|
||||
expect(out).toEqual(game.map().neighbors(tile));
|
||||
const n = game.map().neighbors4(tile, nbuf);
|
||||
expect(nbuf.slice(0, n)).toEqual(game.map().neighbors(tile));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user