UNPKG

labyrinth-game-logic

Version:

A libary that implements the boardgame labyrinth and exposes the functionality to create and interact with games

460 lines (437 loc) 13.8 kB
/** * @author Timo lehnertz */ import { AllPlayerStates } from "./AllPlayerStates"; import { Board } from "./Board"; import { GameState } from "./GameState"; import { Heading } from "./Heading"; import { Move } from "./Move"; import { PathTile, TileType } from "./PathTile"; import { PlayerState } from "./PlayerState"; import { RandomNumberGenerator } from "./RandomNumberGenerator"; import { ShiftPosition } from "./ShiftPosition"; import { Treasure } from "./Treasure"; export interface CardRatios { lCards: number; streightCards: number; tCards: number; } export interface TreasureCardChances { lCardTreasureChance: number; streightCardTreasureChance: number; tCardTreasureChance: number; fixCardTreasureChance: number; } export interface GameSetup { seed: string; playerCount: number; boardWidth: number; boardHeight: number; cardsRatio: CardRatios; treasureCardChances: TreasureCardChances; } export type GameTileType = "fix" | "homePoint" | TileType; export interface PlayerListener { yourTurn: (gameState: GameState, move: (move: Move) => boolean) => void; } export interface WinListener { gameEnded: (winnerIndex: number) => void; } /** * Game class representing a game */ export class Game { // Game state private _gameState: GameState; // stuff private redoHistory: Move[] = []; private constructor(gameState: GameState) { this._gameState = gameState; } /** * Game Logic ------------------- */ public move(move: Move) { if (this.redoHistory.length > 0) { throw new Error("redo all moves before moving a new move"); } if (this._gameState.getWinnerIndex() !== null) { throw new Error("cant move after game has ended"); } this._gameState.validateMove(move); // throws on error this._gameState = this._gameState.move(move); this.redoHistory = []; } public undoLastMove() { const result = this._gameState.undoMove(); this._gameState = result.newGameState; this.redoHistory.push(result.undoneMove); } public redoLastMove() { const redoMove = this.redoHistory.pop(); if (redoMove === undefined) { throw new Error("no move to redo"); } this._gameState = this._gameState.move(redoMove); } public get gameState(): GameState { return this._gameState; } /** * Setup logic */ public static getDefaultSetup(): GameSetup { return { playerCount: 4, seed: "seed", boardHeight: 7, boardWidth: 7, cardsRatio: { lCards: 15 / 34, streightCards: 13 / 34, tCards: 6 / 34, }, treasureCardChances: { lCardTreasureChance: 6 / 15, streightCardTreasureChance: 0, tCardTreasureChance: 1, fixCardTreasureChance: 1, }, }; } public static finalizeSetup(setup?: Partial<GameSetup>): GameSetup { const defaultSetup = Game.getDefaultSetup(); const finalSetup = { boardHeight: setup?.boardHeight ?? defaultSetup.boardHeight, boardWidth: setup?.boardWidth ?? defaultSetup.boardWidth, cardsRatio: setup?.cardsRatio ?? defaultSetup.cardsRatio, playerCount: setup?.playerCount ?? defaultSetup.playerCount, seed: setup?.seed ?? defaultSetup.seed, treasureCardChances: setup?.treasureCardChances ?? defaultSetup.treasureCardChances, }; if ( !Board.isSizeValid(finalSetup.boardWidth) || !Board.isSizeValid(finalSetup.boardHeight) ) { throw new Error(`Invalid board size`); } const maxPlayers = Board.getMaxPlayerCount( finalSetup.boardWidth, finalSetup.boardHeight ); if (finalSetup.playerCount > maxPlayers) { throw new Error(`Too many players. Max ${maxPlayers}`); } if (finalSetup.playerCount < 2) { throw new Error("Too few players. Min 2"); } return finalSetup; } public static buildFromSetup(partialSetup?: Partial<GameSetup>): Game { // apply defaults const setup = Game.finalizeSetup(partialSetup); const generator = new RandomNumberGenerator(setup.seed); const treasures = Game.generateTreasures( Game.getTreasureCount(setup.playerCount) ); const allPlayerStates = Game.generatePlayerStates( generator, setup.playerCount, treasures, setup.boardWidth, setup.boardHeight ); const board = Game.generateRandomBoard( generator, treasures, setup.boardWidth, setup.boardHeight, setup.cardsRatio, setup.treasureCardChances ); const gameState = new GameState(board, allPlayerStates, []); return new Game(gameState); } private static getTreasureCount(playerCount: number): number { if (playerCount <= 4) { return 24; } else { return playerCount * 6; } } private static generateRandomBoard( generator: RandomNumberGenerator, treasures: Treasure[], width: number, height: number, cardsRatios: CardRatios, treasureCardChances: TreasureCardChances ) { // create board tile placeholders const tiles: (PathTile | null)[][] = []; for (let x = 0; x < width; x++) { const column: (PathTile | null)[] = []; for (let y = 0; y < height; y++) { column[y] = null; } tiles.push(column); } // fill corners tiles[0][0] = new PathTile(TileType.L, null, 0, null); tiles[width - 1][0] = new PathTile(TileType.L, null, 1, null); tiles[width - 1][height - 1] = new PathTile(TileType.L, null, 2, null); tiles[0][height - 1] = new PathTile(TileType.L, null, 3, null); // get generator const getRandomTreasure = Game.getRandomTreasureProvider( generator, treasures, treasureCardChances ); const homePoints = Board.generatePlayerHomePositions(width, height); function isHome(x: number, y: number): boolean { for (const homePoint of homePoints) { if (homePoint.x === x && homePoint.y === y) { return true; } } return false; } // // fill fix edges // NORTH for (let x = 2; x < width - 2; x += 2) { tiles[x][0] = new PathTile( TileType.T, getRandomTreasure(isHome(x, 0) ? "homePoint" : "fix"), 2, null ); } // EAST for (let y = 2; y < height - 2; y += 2) { tiles[width - 1][y] = new PathTile( TileType.T, getRandomTreasure(isHome(width - 1, y) ? "homePoint" : "fix"), 3, null ); } // SOUTH for (let x = 2; x < width - 2; x += 2) { tiles[x][height - 1] = new PathTile( TileType.T, getRandomTreasure(isHome(x, height - 1) ? "homePoint" : "fix"), 0, null ); } // WEST for (let y = 2; y < height - 2; y += 2) { tiles[0][y] = new PathTile( TileType.T, getRandomTreasure(isHome(0, y) ? "homePoint" : "fix"), 1, null ); } // fill fix center for (let x = 2; x < width - 2; x += 2) { for (let y = 2; y < height - 2; y += 2) { tiles[x][y] = new PathTile( TileType.T, getRandomTreasure(isHome(x, y) ? "homePoint" : "fix"), Math.floor(generator.rand() * 4), null ); } } // fill remaining const treasureCandidates: ({ x: number; y: number } | "loose-tile")[][] = [ [], // T cards [], // L cards [], // Streight cards ]; const tileTypes = [TileType.T, TileType.L, TileType.STREIGHT]; function idxFromTileType(tileType: TileType): number { switch (tileType) { case TileType.T: return 0; case TileType.L: return 1; case TileType.STREIGHT: return 2; } } for (let x = 0; x < width; x++) { for (let y = 0; y < height; y++) { if (tiles[x][y] === null) { const pathTile = new PathTile( Game.generateRandomTileType(generator), null, Math.floor(generator.rand() * 4), null ); tiles[x][y] = pathTile; treasureCandidates[idxFromTileType(pathTile.tileType)].push({ x, y }); } } } let looseTile = new PathTile( Game.generateRandomTileType(generator), null, Math.floor(generator.rand() * 4), null ); treasureCandidates[idxFromTileType(looseTile.tileType)].push("loose-tile"); // distribute remaining treasures for (let i = 0; i < treasureCandidates.length; i++) { const treasureCandidatesOfType = treasureCandidates[i]; for (const chosenCandidate of treasureCandidatesOfType) { const treasure = getRandomTreasure(tileTypes[i]); if (chosenCandidate === "loose-tile") { looseTile = looseTile.setTreasure(treasure); } else { tiles[chosenCandidate.x][chosenCandidate.y] = tiles[chosenCandidate.x][chosenCandidate.y]?.setTreasure( treasure ) ?? null; } } } // set homePoints for (let i = 0; i < homePoints.length; i++) { const homePoint = homePoints[i]; tiles[homePoint.x][homePoint.y] = tiles[homePoint.x][homePoint.y]?.setHomeOfPlayerIndex(i) ?? null; } const startingShiftPosition = new ShiftPosition(Heading.NORTH, 0); return new Board(tiles as PathTile[][], looseTile, startingShiftPosition); } private static generateRandomTileType( generator: RandomNumberGenerator ): TileType { const rand = generator.rand() * 3; if (rand < 1) { return TileType.L; } else if (rand < 2) { return TileType.T; } else { return TileType.STREIGHT; } } public static getRandomTreasureProvider( generator: RandomNumberGenerator, treasures: Treasure[], treasureCardChances?: TreasureCardChances ): (cardType?: GameTileType) => Treasure | null { const remainingTreasures: Treasure[] = []; for (const treasure of treasures) { remainingTreasures.push(treasure); } function getRandomTreasure(cardType?: GameTileType): Treasure | null { let skipChance = 0; if (treasureCardChances !== undefined && cardType !== undefined) { switch (cardType) { case TileType.L: skipChance = 1 - treasureCardChances.lCardTreasureChance; break; case TileType.T: skipChance = 1 - treasureCardChances.tCardTreasureChance; break; case TileType.STREIGHT: skipChance = 1 - treasureCardChances.streightCardTreasureChance; break; case "fix": skipChance = 1 - treasureCardChances.fixCardTreasureChance; break; case "homePoint": skipChance = 1; break; } } const rand = generator.rand(); if (rand < skipChance) { return null; } const index = Math.floor( Math.min(0.99, rand) * remainingTreasures.length ); if (index >= remainingTreasures.length) { return null; } const randomTreasure = remainingTreasures[index]; remainingTreasures.splice(index, 1); return randomTreasure; } return getRandomTreasure; } /** * @param numberOfPlayers Has to be either 2, 3 or 4 */ private static generatePlayerStates( generator: RandomNumberGenerator, numberOfPlayers: number, treasures: Treasure[], boardWidth: number, boardHeight: number ): AllPlayerStates { // generate treasures const getRandomTreasure = Game.getRandomTreasureProvider( generator, treasures ); const treasuresPerPlayer = treasures.length / numberOfPlayers; // distribute all treasures evenly to all players const allPlayersTreasures: Treasure[][] = []; for (let i = 0; i < numberOfPlayers; i++) { const playerTreasures: Treasure[] = []; for (let i = 0; i < treasuresPerPlayer; i++) { const treasure = getRandomTreasure(); if (treasure) { playerTreasures.push(treasure); } } allPlayersTreasures.push(playerTreasures); } // create playerStates const playerStates: PlayerState[] = []; for (let i = 0; i < allPlayersTreasures.length; i++) { const playersTreasures = allPlayersTreasures[i]; const currentTreasure = playersTreasures.pop() ?? null; const playerStartPosition = Board.getPlayerHomePosition( i, boardWidth, boardHeight ); playerStates.push( new PlayerState( [], playersTreasures, currentTreasure, playerStartPosition ) ); } return new AllPlayerStates(playerStates, 0); } public static generateTreasures(amount: number): Treasure[] { const treasures: Treasure[] = []; for (let i = 0; i < amount; i++) { treasures.push(new Treasure(i)); } return treasures; } public static buildFromString(str: string): Game { const obj = JSON.parse(str); if (!("gameState" in obj)) { throw new Error("Invalid game string"); } return new Game(GameState.create(obj.gameState)); } public stringify(): string { return JSON.stringify({ gameState: this._gameState, }); } }