From 793fb23f6445d7c2e39362632615a89ea05d369d Mon Sep 17 00:00:00 2001 From: Ian Haken Date: Thu, 19 Feb 2026 17:23:02 -0800 Subject: [PATCH] Fix checking for connection to master network --- src/maps/montreal_metro/government_track.ts | 184 +++++++++++++++----- src/maps/montreal_metro/settings.ts | 2 + 2 files changed, 144 insertions(+), 42 deletions(-) diff --git a/src/maps/montreal_metro/government_track.ts b/src/maps/montreal_metro/government_track.ts index adb9b23f..3f964ef7 100644 --- a/src/maps/montreal_metro/government_track.ts +++ b/src/maps/montreal_metro/government_track.ts @@ -1,6 +1,6 @@ import z from "zod"; import { BuildAction, BuildData } from "../../engine/build/build"; -import { BaseBuildPhase } from "../../engine/build/phase"; +import { BaseBuildPhase, BuildPhase } from "../../engine/build/phase"; import { injectState } from "../../engine/framework/execution_context"; import { Key } from "../../engine/framework/key"; import { PHASE } from "../../engine/game/phase"; @@ -11,7 +11,7 @@ import { injectInGamePlayers } from "../../engine/game/state"; import { City } from "../../engine/map/city"; import { calculateTrackInfo, Land } from "../../engine/map/location"; import { isTownTile } from "../../engine/map/tile"; -import { TOWN, TrackInfo } from "../../engine/map/track"; +import { Exit, TOWN } from "../../engine/map/track"; import { Phase } from "../../engine/state/phase"; import { PlayerColor, @@ -20,8 +20,12 @@ import { } from "../../engine/state/player"; import { allDirections, Direction } from "../../engine/state/tile"; import { isNotNull } from "../../utils/functions"; -import { assert } from "../../utils/validate"; +import { assert, fail } from "../../utils/validate"; import { GOVERNMENT_COLOR } from "./government_engine_level"; +import { Grid } from "../../engine/map/grid"; +import { Coordinates } from "../../utils/coordinates"; +import { getNeighbor } from "../moon/wrap_around"; +import { getOpposite } from "../../engine/map/direction"; const GovernmentTrackState = PlayerColorZod.array(); type GovernmentTrackState = z.infer; @@ -69,6 +73,18 @@ export class MontrealMetroGovernmentBuildPhase extends BaseBuildPhase { forcedAction(): ActionBundle | undefined { return undefined; } + + onEndTurn(): void { + super.onEndTurn(); + checkHasContiguousMasterNetwork(this.grid()); + } +} + +export class MontrealMetroBuildPhase extends BuildPhase { + onEndTurn(): void { + super.onEndTurn(); + checkHasContiguousMasterNetwork(this.grid()); + } } export class MontrealMetroBuildAction extends BuildAction { @@ -86,40 +102,6 @@ export class MontrealMetroBuildAction extends BuildAction { return this.phase() === MontrealMetroGovernmentBuildPhase.phase; } - private isFirstBuildOfGame(): boolean { - return ( - this.isGovtBuildPhase() && - this.round() === 1 && - this.buildState().buildCount! === 0 - ); - } - - private isContiguousWithExistingTrack(data: BuildData): boolean { - const trackInfo = calculateTrackInfo(data); - const trackIsContiguous = (track: TrackInfo) => - track.exits.some((exit) => { - if (exit === TOWN) return false; - if (this.grid().getTrackConnection(data.coordinates, exit) != null) { - return true; - } - const neighbor = this.grid().getNeighbor(data.coordinates, exit); - if (!(neighbor instanceof City)) { - return false; - } - return allDirections.some((direction) => { - return ( - this.grid().getTrackConnection(neighbor.coordinates, direction) != - null - ); - }); - }); - if (isTownTile(data.tileType)) { - return trackInfo.some(trackIsContiguous); - } else { - return trackInfo.every(trackIsContiguous); - } - } - private isContinuingExistingLink(data: BuildData): boolean { if (this.buildState().buildCount! === 0) { return true; @@ -189,11 +171,6 @@ export class MontrealMetroBuildAction extends BuildAction { validate(data: BuildData): void { super.validate(data); - assert( - this.isFirstBuildOfGame() || this.isContiguousWithExistingTrack(data), - { invalidInput: "must be contiguous with existing track" }, - ); - if (this.isGovtBuildPhase()) { this.validateGovtBuild(data); } @@ -213,3 +190,126 @@ export class MontrealMetroBuildAction extends BuildAction { }); } } + +function checkHasContiguousMasterNetwork(grid: Grid): boolean { + // Locate any tile with track on it as the starting point to begin a DFS of connected track + let startingTrack: Land | undefined; + for (const [_, space] of grid.entries()) { + if (space instanceof Land && space.getTrack().length > 0) { + startingTrack = space; + break; + } + } + assert(startingTrack !== undefined); + + const seenTrack: { [key: string]: boolean } = {}; + exploreConnectedTrack( + grid, + startingTrack.coordinates, + startingTrack.getTrack()[0].getExits(), + seenTrack, + ); + + // Now having explored everything, validate that all track on the grid is in the seenTrack map + for (const [_, space] of grid.entries()) { + if (space instanceof Land) { + for (const track of space.getTrack()) { + const label = serializeTrack(track.coordinates, track.getExits()); + if (!seenTrack[label]) { + fail({ + invalidInput: + "All track must be connected to the master network at the end of the build.", + }); + } + } + } + } + + return false; +} + +function exploreConnectedTrackFromCity( + grid: Grid, + city: City, + seenTrack: { [key: string]: boolean }, +) { + const label = serializeTrack(city.coordinates, [TOWN, TOWN]); + if (seenTrack[label]) { + return; + } + seenTrack[label] = true; + + for (const direction of allDirections) { + const track = grid.getTrackConnection(city.coordinates, direction); + if (track !== undefined) { + exploreConnectedTrack( + grid, + track.coordinates, + track.getExits(), + seenTrack, + ); + } + } + for (const other of grid.getSameCities(city)) { + if (!other.coordinates.equals(city.coordinates)) { + exploreConnectedTrackFromCity(grid, other, seenTrack); + } + } +} + +function exploreConnectedTrack( + grid: Grid, + coordinates: Coordinates, + track: [Exit, Exit], + seenTrack: { [key: string]: boolean }, +) { + const label = serializeTrack(coordinates, track); + if (seenTrack[label]) { + return; + } + seenTrack[label] = true; + + for (const exit of track) { + if (exit === TOWN) { + const space = grid.get(coordinates); + assert(space instanceof Land); + for (const track of space.getTrack()) { + exploreConnectedTrack( + grid, + track.coordinates, + track.getExits(), + seenTrack, + ); + } + } else { + const neighbor = getNeighbor(grid, coordinates, exit); + if (neighbor === undefined) { + continue; + } + if (neighbor instanceof City) { + exploreConnectedTrackFromCity(grid, neighbor, seenTrack); + } else { + const nextTrack = neighbor.trackExiting(getOpposite(exit)); + if (nextTrack !== undefined) { + exploreConnectedTrack( + grid, + neighbor.coordinates, + nextTrack.getExits(), + seenTrack, + ); + } + } + } + } +} + +function serializeTrack(coordinates: Coordinates, track: [Exit, Exit]): string { + return ( + coordinates.serialize() + + "|" + + track + .sort() + .map((exit) => exit.toString()) + .join("|") + ); +} diff --git a/src/maps/montreal_metro/settings.ts b/src/maps/montreal_metro/settings.ts index 75846518..cfcedcbc 100644 --- a/src/maps/montreal_metro/settings.ts +++ b/src/maps/montreal_metro/settings.ts @@ -22,6 +22,7 @@ import { MontrealMetroProfitHelper } from "./expenses"; import { MontrealMetroMoveHelper } from "./government_engine_level"; import { MontrealMetroBuildAction, + MontrealMetroBuildPhase, MontrealMetroPhaseDelegator, } from "./government_track"; import { map } from "./grid"; @@ -58,6 +59,7 @@ export class MontrealMetroMapSettings implements MapSettings { MontrealMetroBuilderHelper, MontrealMetroUrbanizeAction, MontrealActionNamingProvider, + MontrealMetroBuildPhase, ]; } }