diff --git a/src/engine/state/inter_city_connection.ts b/src/engine/state/inter_city_connection.ts index 79472a11..b3c9baab 100644 --- a/src/engine/state/inter_city_connection.ts +++ b/src/engine/state/inter_city_connection.ts @@ -12,9 +12,11 @@ export type Offset = z.infer; export const InterCityConnection = z.object({ id: z.string(), connects: CoordinatesZod.array(), + connectedTownExits: DirectionZod.array().optional(), cost: z.number(), center: CoordinatesZod.optional(), offset: Offset.optional(), + // No owner means the connection isn't built. An owner but no color means it's built but unowned. owner: z.object({ color: PlayerColorZod.optional() }).optional(), }); diff --git a/src/maps/denmark/build_validator.ts b/src/maps/denmark/build_validator.ts index 2363abed..5b435b6c 100644 --- a/src/maps/denmark/build_validator.ts +++ b/src/maps/denmark/build_validator.ts @@ -174,4 +174,4 @@ function hasDuplicateOwnedRoute(routes: RouteInfo[]): boolean { } } return false; -} +} \ No newline at end of file diff --git a/src/maps/denmark/settings.ts b/src/maps/denmark/settings.ts index 6a7127fa..b4af449d 100644 --- a/src/maps/denmark/settings.ts +++ b/src/maps/denmark/settings.ts @@ -114,4 +114,4 @@ export class DenmarkMapSettings implements MapSettings { }), ]; } -} +} \ No newline at end of file diff --git a/src/maps/factory.ts b/src/maps/factory.ts index 1c3d7d7c..b41be9fe 100644 --- a/src/maps/factory.ts +++ b/src/maps/factory.ts @@ -11,6 +11,7 @@ import { OnRoll, OnRollData } from "../engine/state/roll"; import { CityData, LandData } from "../engine/state/space"; import { Coordinates } from "../utils/coordinates"; import { duplicate } from "../utils/functions"; +import { Direction } from "../engine/state/tile"; export const PLAIN: LandData = { type: SpaceType.PLAIN, @@ -150,6 +151,7 @@ interface InterCityConnectionFactoryProps { cost?: number; center?: [number, number]; offset?: Offset; + connectedTownExits?: Direction[]; } export function interCityConnections( @@ -167,6 +169,7 @@ export function interCityConnections( id: "" + (idx + 1), connects: [cities.get(spec.connects[0])!, cities.get(spec.connects[1])!], cost: spec.cost ?? 2, + connectedTownExits: spec.connectedTownExits, offset: spec.offset, center: spec.center ? Coordinates.from({ q: spec.center[0], r: spec.center[1] }) diff --git a/src/maps/portugal/lisboa.ts b/src/maps/portugal/lisboa.ts index cb831f8d..e2f20194 100644 --- a/src/maps/portugal/lisboa.ts +++ b/src/maps/portugal/lisboa.ts @@ -5,12 +5,6 @@ import { ConnectCitiesData, } from "../../engine/build/connect_cities"; import { BuildPhase } from "../../engine/build/phase"; -import { Validator, InvalidBuildReason } from "../../engine/build/validator"; -import { BOTTOM, Direction } from "../../engine/state/tile"; -import { MoveValidator, RouteInfo } from "../../engine/move/validator"; -import { PlayerColor } from "../../engine/state/player"; -import { DanglerInfo } from "../../engine/map/grid"; -import { OwnedInterCityConnection } from "../../engine/state/inter_city_connection"; import { injectState } from "../../engine/framework/execution_context"; import { Key } from "../../engine/framework/key"; import { City } from "../../engine/map/city"; @@ -66,8 +60,6 @@ export class LisboaBuildAction extends BuildAction { export class LisboaConnectAction extends ConnectCitiesAction { private readonly connected = injectState(CONNECTED_TO_LISBOA); - protected validateUrbanizedCities(): void {} - validate(data: ConnectCitiesData): void { super.validate(data); // Only one connection out of Lisboa can be built per turn, per player. @@ -89,89 +81,6 @@ export class LisboaConnectAction extends ConnectCitiesAction { } } -export class PortugalValidator extends Validator { - protected connectionAllowed( - land: Land, - exit: Direction, - ): InvalidBuildReason | undefined { - if ( - (land.name() === "Sagres" || land.name() === "Sines") && - land.hasTown() && - exit === BOTTOM - ) { - return undefined; - } - - return super.connectionAllowed(land, exit); - } -} - -export class PortugalMoveValidator extends MoveValidator { - protected getAdditionalRoutesFromLand(location: Land): RouteInfo[] { - const grid = this.grid(); - return grid.connections - .filter((connection) => - connection.connects.some((c) => c.equals(location.coordinates)), - ) - .filter((connection) => connection.owner != null) - .flatMap((connection) => { - const otherEnd = grid.get( - connection.connects.find((c) => !location.coordinates.equals(c))!, - ); - if (!(otherEnd instanceof City)) { - return []; - } - if ( - (isLisboa(otherEnd) || otherEnd.name() === "Madeira") && - (location.name() === "Sagres" || location.name() === "Sines") && - location.hasTown() - ) { - return [ - { - type: "connection", - destination: otherEnd.coordinates, - connection: connection as OwnedInterCityConnection, - owner: connection.owner!.color, - }, - ]; - } - return []; - }); - } - - protected getAdditionalRoutesFromCity(location: City): RouteInfo[] { - const grid = this.grid(); - return grid.connections - .filter((connection) => - connection.connects.some((c) => c.equals(location.coordinates)), - ) - .filter((connection) => connection.owner != null) - .flatMap((connection) => { - const otherEnd = grid.get( - connection.connects.find((c) => !location.coordinates.equals(c))!, - ); - if (!(otherEnd instanceof Land)) { - return []; - } - if ( - (isLisboa(location) || location.name() === "Madeira") && - (otherEnd.name() === "Sagres" || otherEnd.name() === "Sines") && - otherEnd.hasTown() - ) { - return [ - { - type: "connection", - destination: otherEnd.coordinates, - connection: connection as OwnedInterCityConnection, - owner: connection.owner!.color, - }, - ]; - } - return []; - }); - } -} - export class PortugalBuildPhase extends BuildPhase { private readonly connected = injectState(CONNECTED_TO_LISBOA); onStartTurn(): void { @@ -183,29 +92,6 @@ export class PortugalBuildPhase extends BuildPhase { this.connected.delete(); super.onEndTurn(); } - - getDanglersAsInfo(color?: PlayerColor): DanglerInfo[] { - return this.grid() - .getDanglers(color) - .filter((track) => { - if ( - //Sines - ((track.coordinates.q === 14 && track.coordinates.r === 7) || - //Sagres - (track.coordinates.q === 17 && track.coordinates.r === 6)) && - this.grid().getImmovableExitReference(track) === BOTTOM - ) { - return false; - } - - return true; - }) - .map((track) => ({ - coordinates: track.coordinates, - immovableExit: this.grid().getImmovableExitReference(track), - length: this.grid().getRoute(track).length, - })); - } } function isLisboa(space: City | Land | undefined): boolean { diff --git a/src/maps/portugal/settings.ts b/src/maps/portugal/settings.ts index 360e8f22..c12e2c05 100644 --- a/src/maps/portugal/settings.ts +++ b/src/maps/portugal/settings.ts @@ -7,14 +7,13 @@ import { import { Module } from "../../engine/module/module"; import { BOTTOM, TOP } from "../../engine/state/tile"; import { OneClaimLimitModule } from "../../modules/one_claim_limit"; +import { TownsAndSeaLinksModule } from "../../modules/towns_and_sea_links"; import { interCityConnections } from "../factory"; import { PortugalGoodsGrowthPhase } from "./goods"; import { map } from "./grid"; import { LisboaBuildAction, LisboaConnectAction, - PortugalValidator, - PortugalMoveValidator, PortugalBuildPhase, } from "./lisboa"; @@ -52,12 +51,12 @@ export class PortugalMapSettings implements MapSettings { center: [11, 13], offset: { direction: TOP, distance: 0.2 }, }, - { connects: ["Sagres", "Madeira"], cost: 6, center: [17, 7] }, - { connects: ["Sagres", "Madeira"], cost: 6, center: [17, 8] }, - { connects: ["Sagres", "Madeira"], cost: 6, center: [17, 9] }, - { connects: ["Sagres", "Madeira"], cost: 6, center: [17, 10] }, + { connects: ["Sagres", "Madeira"], cost: 6, center: [17, 7], connectedTownExits: [BOTTOM] }, + { connects: ["Sagres", "Madeira"], cost: 6, center: [17, 8], connectedTownExits: [BOTTOM] }, + { connects: ["Sagres", "Madeira"], cost: 6, center: [17, 9], connectedTownExits: [BOTTOM] }, + { connects: ["Sagres", "Madeira"], cost: 6, center: [17, 10], connectedTownExits: [BOTTOM] }, { connects: ["Lisboa", "Porto"], cost: 6, center: [6, 13] }, - { connects: ["Lisboa", "Sines"], cost: 6, center: [13, 9] }, + { connects: ["Lisboa", "Sines"], cost: 6, center: [13, 9], connectedTownExits: [BOTTOM] }, ]); getOverrides() { @@ -65,13 +64,14 @@ export class PortugalMapSettings implements MapSettings { LisboaBuildAction, LisboaConnectAction, PortugalGoodsGrowthPhase, - PortugalValidator, - PortugalMoveValidator, PortugalBuildPhase, ]; } getModules(): Array { - return [new OneClaimLimitModule()]; + return [ + new OneClaimLimitModule(), + new TownsAndSeaLinksModule(), + ]; } -} +} \ No newline at end of file diff --git a/src/modules/towns_and_sea_links.ts b/src/modules/towns_and_sea_links.ts new file mode 100644 index 00000000..4b81f08f --- /dev/null +++ b/src/modules/towns_and_sea_links.ts @@ -0,0 +1,161 @@ +import { Module } from "../engine/module/module"; +import { ConnectCitiesAction } from "../engine/build/connect_cities"; +import { SimpleConstructor } from "../engine/framework/dependency_stack"; +import { Land } from "../engine/map/location"; +import { Direction } from "../engine/state/tile"; +import { InvalidBuildReason, Validator } from "../engine/build/validator"; +import { MoveValidator, RouteInfo } from "../engine/move/validator"; +import { SpaceType } from "../engine/state/location_type"; +import { City } from "../engine/map/city"; +import { OwnedInterCityConnection } from "../engine/state/inter_city_connection"; +import { BuildPhase } from "../engine/build/phase"; +import { DanglerInfo } from "../engine/map/grid"; +import { PlayerColor } from "../engine/state/player"; + +export class TownsAndSeaLinksModule extends Module { + installMixins(): void { + this.installMixin(ConnectCitiesAction, skipCityValidationMixin); + this.installMixin(Validator, allowTownConnectionMixin); + this.installMixin(MoveValidator, allowGoodsMovementMixin); + this.installMixin(BuildPhase, noSeaRouteDanglersMixin); + } +} + +function skipCityValidationMixin( + Ctor: SimpleConstructor +): SimpleConstructor { + return class extends Ctor { + protected validateUrbanizedCities(): void {} + }; +} + +function allowTownConnectionMixin( + Ctor: SimpleConstructor +): SimpleConstructor { + return class extends Ctor { + protected connectionAllowed( + land: Land, + exit: Direction, + ): InvalidBuildReason | undefined { + if (this.isExitTowardsSea(land, exit) + && land.hasTown() + && this.isExitTowardsInterCity(land, exit)) { + return undefined; + } + return super.connectionAllowed(land, exit); + } + + protected isExitTowardsSea(space: Land, exit: Direction): boolean { + const neighbor = this.grid().getNeighbor(space.coordinates, exit)?.data.type; + if (neighbor === SpaceType.WATER) {return true} + return false ; + } + + protected isExitTowardsInterCity(space: Land, exit: Direction): boolean { + return this.grid().connections.some(connection => + connection.connects.some(c => c.equals(space.coordinates)) + && Array.isArray(connection.connectedTownExits) + && connection.connectedTownExits.includes(exit) + ); + } + } +} + +function allowGoodsMovementMixin( + Ctor: SimpleConstructor +): SimpleConstructor { + return class extends Ctor { + protected getAdditionalRoutesFromLand(location: Land): RouteInfo[] { + const grid = this.grid(); + return grid.connections + .filter((connection) => + connection.connects.some((c) => c.equals(location.coordinates)), + ) + .filter((connection) => connection.owner != null) + .flatMap((connection) => { + const otherEnd = grid.get( + connection.connects.find((c) => !location.coordinates.equals(c))!, + ); + if (!(otherEnd instanceof City)) { + return []; + } + return [ + { + type: "connection", + destination: otherEnd.coordinates, + connection: connection as OwnedInterCityConnection, + owner: connection.owner!.color, + }, + ]; + + }); + } + + protected getAdditionalRoutesFromCity(originCity: City): RouteInfo[] { + const grid = this.grid(); + return grid.connections + .filter((connection) => + connection.connects.some((c) => c.equals(originCity.coordinates)), + ) + .filter((connection) => connection.owner != null) + .flatMap((connection) => { + const otherEnd = grid.get( + connection.connects.find((c) => !originCity.coordinates.equals(c))!, + ); + if ( + otherEnd != null && + !(otherEnd instanceof City) && + otherEnd.hasTown() + ) { + return [ + { + type: "connection", + destination: otherEnd.coordinates, + connection: connection as OwnedInterCityConnection, + owner: connection.owner!.color, + }, + ]; + } + return []; + }); + } + } +} + +function noSeaRouteDanglersMixin( + Ctor: SimpleConstructor +): SimpleConstructor { + return class extends Ctor { + getDanglersAsInfo(color?: PlayerColor): DanglerInfo[] { + const ownedConnectionsWithSeaRoutes = this.grid().connections + .filter(connection => + Array.isArray(connection.connectedTownExits) && + connection.owner != undefined + ); + + return this.grid() + .getDanglers(color) + .filter(track => { + const matchingConnection = ownedConnectionsWithSeaRoutes.find(conn => + conn.connects.some(coord => + coord.q === track.coordinates.q + && coord.r === track.coordinates.r + ) + ); + + if (!matchingConnection) return true; + + const immovableExit = this.grid().getImmovableExitReference(track); + const isMatchingExit = matchingConnection.connectedTownExits?.includes(immovableExit) ?? false; + + if (isMatchingExit) { return false } + return true; + }) + .map(track => ({ + coordinates: track.coordinates, + immovableExit: this.grid().getImmovableExitReference(track), + length: this.grid().getRoute(track).length, + })); + } + }; +} \ No newline at end of file