import { ISpace , SpaceId } from './ISpace' ; import { Player , PlayerId } from '../Player' ; import { SpaceType } from '../SpaceType' ; import { CITY_TILES , OCEAN_TILES , TileType } from '../TileType' ; import { AresHandler } from '../ares/AresHandler' ; import { SerializedBoard , SerializedSpace } from './SerializedBoard' ; /**
 * A representation of any hex board. This is normally Mars (Tharsis, Hellas, Elysium) but can also be The Moon.
 *
 * It also includes additional spaces, known as Colonies, that are not adjacent to other spaces.
 */ export abstract class Board { private maxX : number = 0 ; private maxY : number = 0 ; private readonly adjacentSpaces = new Map < SpaceId , Array < ISpace > > ( ) ; protected constructor ( public spaces ) { this . maxX = Math . max ( ... spaces . map ( ( s ) => s . x ) ) ; this . maxY = Math . max ( ... spaces . map ( ( s ) => s . y ) ) ; spaces . forEach ( ( space ) => { this . adjacentSpaces . set ( space . id , this . computeAdjacentSpaces ( space ) ) ; } ) ; } ; public abstract getVolcanicSpaceIds ( ) ; public abstract getNoctisCitySpaceIds ( ) ; public getSpace ( id ) { const space = this . spaces . find ( ( space ) => space . id === id ) ; if ( space === undefined ) { throw new Error ( `Can't find space with id ${ id }` ) ; } return space ; } protected computeAdjacentSpaces ( space ) { const middleRow = this . maxY / 2 ; if ( space . spaceType !== SpaceType . COLONY ) { if ( space . y < 0 || space . y > this . maxY ) { throw new Error ( 'Unexpected space y value: ' + space . y ) ; } if ( space . x < 0 || space . x > this . maxX ) { throw new Error ( 'Unexpected space x value: ' + space . x ) ; } const leftSpace = [ space . x - 1 , space . y ] ; const rightSpace = [ space . x + 1 , space . y ] ; const topLeftSpace = [ space . x , space . y - 1 ] ; const topRightSpace = [ space . x , space . y - 1 ] ; const bottomLeftSpace = [ space . x , space . y + 1 ] ; const bottomRightSpace = [ space . x , space . y + 1 ] ; if ( space . y < middleRow ) { bottomLeftSpace [ 0 ] -- ; topRightSpace [ 0 ] ++ ; } else if ( space . y === middleRow ) { bottomRightSpace [ 0 ] ++ ; topRightSpace [ 0 ] ++ ; } else { bottomRightSpace [ 0 ] ++ ; topLeftSpace [ 0 ] -- ; } const coords = [ topLeftSpace , topRightSpace , rightSpace , bottomRightSpace , bottomLeftSpace , leftSpace , ] ; const spaces = [ ] ; for ( const [ x , y ] of coords ) { const adj = this . spaces . find ( ( adj ) => space !== adj && adj . spaceType !== SpaceType . COLONY && adj . x === x && adj . y === y ) ; if ( adj !== undefined ) { spaces . push ( adj ) ; } } return spaces ; } return [ ] ; } public getAdjacentSpaces ( space ) { const spaces = this . adjacentSpaces . get ( space . id ) ; if ( spaces === undefined ) { throw new Error ( `Unexpected space ID ${ space . id }` ) ; } return [ ... spaces ] ; } public getSpaceByTileCard ( cardName ) { return this . spaces . find ( ( space ) => space . tile !== undefined && space . tile . card === cardName ) ; } public getOceansOnBoard ( countUpgradedOceans = true ) { return this . getOceansTiles ( countUpgradedOceans ) . length ; } public getOceansTiles ( countUpgradedOceans ) { if ( ! countUpgradedOceans ) { return this . spaces . filter ( ( space ) => space . tile !== undefined && space . tile . tileType === TileType . OCEAN ) ; } else { return this . spaces . filter ( ( space ) => Board . isOceanSpace ( space ) ) ; } } public getSpaces ( spaceType , _player ) { return this . spaces . filter ( ( space ) => space . spaceType === spaceType ) ; } public getEmptySpaces ( ) { return this . spaces . filter ( ( space ) => space . tile === undefined ) ; } public getAvailableSpacesForCity ( player ) { return this . getAvailableSpacesOnLand ( player ) . filter ( ( space ) => this . getAdjacentSpaces ( space ) . some ( ( adjacentSpace ) => Board . isCitySpace ( adjacentSpace ) ) === false ) ; } public getAvailableSpacesForMarker ( player ) { const spaces = this . getAvailableSpacesOnLand ( player ) . filter ( ( space ) => this . getAdjacentSpaces ( space ) . find ( ( adj ) => adj . player === player ) !== undefined ) ; return spaces . filter ( ( space , index ) => spaces . indexOf ( space ) === index ) ; } public getAvailableSpacesForGreenery ( player ) { let spacesOnLand = this . getAvailableSpacesOnLand ( player ) ; if ( player . game . gameOptions . pathfindersExpansion === true ) { spacesOnLand = spacesOnLand . filter ( ( space ) => { return ! this . getAdjacentSpaces ( space ) . some ( ( neighbor ) => neighbor . tile ?. tileType === TileType . RED_CITY ) ; } ) ; } const spacesForGreenery = spacesOnLand . filter ( ( space ) => this . getAdjacentSpaces ( space ) . find ( ( adj ) => adj . tile !== undefined && adj . player === player && adj . tile . tileType !== TileType . OCEAN ) !== undefined ) ; if ( spacesForGreenery . length > 0 ) { return spacesForGreenery ; } return spacesOnLand ; } public getAvailableSpacesForOcean ( player ) { return this . getSpaces ( SpaceType . OCEAN , player ) . filter ( ( space ) => space . tile === undefined && ( space . player === undefined || space . player === player ) ) ; } public getAvailableSpacesOnLand ( player ) { const landSpaces = this . getSpaces ( SpaceType . LAND , player ) . filter ( ( space ) => { const hasPlayerMarker = space . player !== undefined ; const safeForPlayer = ! hasPlayerMarker || space . player === player ; const playableSpace = space . tile === undefined || AresHandler . hasHazardTile ( space ) ; const blockedByDesperateMeasures = space . tile ?. protectedHazard === true ; return safeForPlayer && playableSpace && ! blockedByDesperateMeasures ; } ) ; return landSpaces ; } public getNthAvailableLandSpace ( distance , direction , player = undefined , predicate = ( _x ) => true ) { const spaces = this . spaces . filter ( ( space ) => { return this . canPlaceTile ( space ) && ( space . player === undefined || space . player === player ) ; } ) . filter ( predicate ) ; let idx = ( direction === 1 ) ? distance : ( spaces . length - ( distance + 1 ) ) ; if ( spaces . length === 0 ) { throw new Error ( 'no spaces available' ) ; } while ( idx < 0 ) { idx += spaces . length ; } while ( idx >= spaces . length ) { idx -= spaces . length ; } return spaces [ idx ] ; } public getNonReservedLandSpaces ( ) { return this . spaces . filter ( ( space ) => { return ( space . spaceType === SpaceType . LAND || space . spaceType === SpaceType . COVE ) && ( space . tile === undefined || AresHandler . hasHazardTile ( space ) ) && space . player === undefined ; } ) ; } public canPlaceTile ( space ) { return space . tile === undefined && space . spaceType === SpaceType . LAND ; } public static isCitySpace ( space ) { return space . tile !== undefined && CITY_TILES . has ( space . tile . tileType ) ; } public static isOceanSpace ( space ) { return space . tile !== undefined && OCEAN_TILES . has ( space . tile . tileType ) ; } public serialize ( ) { return { spaces : this . spaces . map ( ( space ) => { return { id : space . id , spaceType : space . spaceType , tile : space . tile , player : space . player ?. id , bonus : space . bonus , adjacency : space . adjacency , x : space . x , y : space . y , } ; } ) , } ; } public static deserializeSpace ( serialized , players ) { const playerId = serialized . player ; const player = players . find ( ( p ) => p . id === playerId ) ; const space = { id : serialized . id , spaceType : serialized . spaceType , bonus : serialized . bonus , x : serialized . x , y : serialized . y , } ; if ( serialized . tile !== undefined ) { space . tile = serialized . tile ; } if ( player !== undefined ) { space . player = player ; } if ( serialized . adjacency !== undefined ) { space . adjacency = serialized . adjacency ; } return space ; } public static deserializeSpaces ( spaces , players ) { return spaces . map ( ( space ) => Board . deserializeSpace ( space , players ) ) ; } } export function nextToNoOtherTileFn ( board ) { return ( space ) => board . getAdjacentSpaces ( space ) . every ( ( space ) => space . tile === undefined ) ; } ; export function playerTileFn ( player ) { return ( space ) => space . player ?. id === player . id ; } export function isSpecialTile ( space ) { switch ( space . tile ?. tileType ) { case TileType . GREENERY : case TileType . OCEAN : case TileType . CITY : case TileType . MOON_COLONY : case TileType . MOON_MINE : case TileType . MOON_ROAD : case undefined : return false ; default : return true ; } } ;