UNPKG

labyrinth-game-logic

Version:

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

1,675 lines (1,657 loc) 53.9 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { AllPlayerStates: () => AllPlayerStates, Board: () => Board, BoardPosition: () => BoardPosition, Game: () => Game, GameState: () => GameState, Heading: () => Heading, HeadingHelper: () => HeadingHelper, Move: () => Move, OpenSides: () => OpenSides, Path: () => Path, PathTile: () => PathTile, PlayerState: () => PlayerState, RandomNumberGenerator: () => RandomNumberGenerator, ShiftPosition: () => ShiftPosition, TileType: () => TileType, Treasure: () => Treasure, Vec2: () => Vec2, buildMoveGenerator: () => buildMoveGenerator, generateMoves: () => generateMoves, generateRandomMove: () => generateRandomMove, generateShiftPositions: () => generateShiftPositions, manhattanEvaluator: () => manhattanEvaluator, positionByTreasure: () => positionByTreasure, printBoard: () => printBoard, stringifyTile: () => stringifyTile }); module.exports = __toCommonJS(src_exports); // src/Vec2.ts var Vec2 = class _Vec2 { x; y; constructor(x, y) { this.x = x; this.y = y; } equals(other) { return this.x === other.x && this.y === other.y; } add(b) { return new _Vec2(this.x + b.x, this.y + b.y); } subtract(b) { return new _Vec2(this.x - b.x, this.y - b.y); } multiply(scalar) { return new _Vec2(this.x * scalar, this.y * scalar); } distanceFrom(other) { return this.subtract(other).length; } manhattanDistanceFrom(other) { const diff = this.subtract(other); return Math.abs(diff.x) + Math.abs(diff.y); } get length() { return Math.sqrt(this.x * this.x + this.y * this.y); } setX(x) { return new _Vec2(x, this.y); } setY(y) { return new _Vec2(this.x, y); } }; // src/BoardPosition.ts var BoardPosition = class _BoardPosition extends Vec2 { isInBounds(width, height) { return this.x >= 0 && this.y >= 0 && this.x < width && this.y < height; } isEdge(width, height) { return this.x === 0 || this.y === 0 || this.x === width - 1 || this.y === height - 1; } add(b) { return new _BoardPosition(this.x + b.x, this.y + b.y); } static create(instance) { return new _BoardPosition(instance.x, instance.y); } setX(x) { return new _BoardPosition(x, this.y); } setY(y) { return new _BoardPosition(this.x, y); } }; // src/Treasure.ts var Treasure = class _Treasure { id; constructor(id) { this.id = id; } equals(other) { return this.id === other.id; } static compare(a, b) { if (a === null && b !== null) { return false; } if (a !== null && b === null) { return false; } if (a !== null && b !== null) { return a.equals(b); } return true; } static create(instance) { return new _Treasure(instance.id); } }; // src/PlayerState.ts var PlayerState = class _PlayerState { foundTreasures; remainingTreasures; currentTreasure; position; constructor(foundTreasures, remainingTreasures, currentTreasure, position) { this.foundTreasures = foundTreasures; this.remainingTreasures = remainingTreasures; this.currentTreasure = currentTreasure; this.position = position; } /** * Returns the number of remaining treasures to be found including the currently open treasure */ get remainingTreasureCount() { return this.remainingTreasures.length + (this.currentTreasure !== null ? 1 : 0); } get lastFoundTreasure() { if (this.foundTreasures.length === 0) { return null; } return this.foundTreasures[this.foundTreasures.length - 1]; } get foundTreasureCount() { return this.foundTreasures.length; } getFoundTreasure(index) { return this.foundTreasures[index]; } setPosition(newPosition) { return new _PlayerState( this.foundTreasures, this.remainingTreasures, this.currentTreasure, newPosition ); } collectTreasure(treasure) { const newFoundTreasures = this.copyFoundTreasures(); const newRemainingTreasures = this.copyRemainingTreasures(); const newCurrentTreasure = newRemainingTreasures.pop() ?? null; newFoundTreasures.push(treasure); return new _PlayerState( newFoundTreasures, newRemainingTreasures, newCurrentTreasure, this.position ); } removeLastTreasure() { const newFoundTreasures = this.copyFoundTreasures(); const newRemainingTreasures = this.copyRemainingTreasures(); if (this.currentTreasure !== null) { newRemainingTreasures.push(this.currentTreasure); } const newCurrentTreasure = newFoundTreasures.pop() ?? null; return new _PlayerState( newFoundTreasures, newRemainingTreasures, newCurrentTreasure, this.position ); } /** * Treasures are immutable so we dont need to make a deep copy here */ copyFoundTreasures() { const newFoundTreasures = []; for (const foundTreasure of this.foundTreasures) { newFoundTreasures.push(foundTreasure); } return newFoundTreasures; } /** * Treasures are immutable so we dont need to make a deep copy here */ copyRemainingTreasures() { const newRemainingTreasures = []; for (const remainingTreasure of this.remainingTreasures) { newRemainingTreasures.push(remainingTreasure); } return newRemainingTreasures; } equals(other) { if (!this.position.equals(other.position)) { return false; } if (!Treasure.compare(this.currentTreasure, other.currentTreasure)) { return false; } if (this.foundTreasures.length !== other.foundTreasures.length) { return false; } for (let i = 0; i < this.foundTreasures.length; i++) { const a = this.foundTreasures[i]; const b = other.foundTreasures[i]; if (!a.equals(b)) { return false; } } if (this.remainingTreasures.length !== other.remainingTreasures.length) { return false; } for (let i = 0; i < this.remainingTreasures.length; i++) { const a = this.remainingTreasures[i]; const b = other.remainingTreasures[i]; if (!a.equals(b)) { return false; } } return true; } static create(instance) { const foundTreasures = []; for (const foundTreasure of instance.foundTreasures) { foundTreasures.push(Treasure.create(foundTreasure)); } const remainingTreasures = []; for (const remainingTreasure of instance.remainingTreasures) { remainingTreasures.push(Treasure.create(remainingTreasure)); } return new _PlayerState( foundTreasures, remainingTreasures, instance.currentTreasure !== null ? Treasure.create(instance.currentTreasure) : null, BoardPosition.create(instance.position) ); } }; // src/AllPlayerStates.ts var AllPlayerStates = class _AllPlayerStates { allPlayerStates; playerIndexToMove; constructor(allPlayerStates, playerIndexToMove) { this.allPlayerStates = allPlayerStates; this.playerIndexToMove = playerIndexToMove; } get playerCount() { return this.allPlayerStates.length; } getPlayerPositions() { const positions = []; for (const playerState of this.allPlayerStates) { positions.push(playerState.position); } return positions; } copyPlayerStates() { const newAllPlayerStates = []; for (const playerState of this.allPlayerStates) { newAllPlayerStates.push(playerState); } return newAllPlayerStates; } collectTreasure(playerIndex, foundTreasure) { const newAllPlayerStates = this.copyPlayerStates(); newAllPlayerStates[playerIndex] = newAllPlayerStates[playerIndex].collectTreasure(foundTreasure); return new _AllPlayerStates(newAllPlayerStates, this.playerIndexToMove); } removeLastTreasure(playerIndex) { const newAllPlayerStates = this.copyPlayerStates(); newAllPlayerStates[playerIndex] = newAllPlayerStates[playerIndex].removeLastTreasure(); return new _AllPlayerStates(newAllPlayerStates, this.playerIndexToMove); } mutateAll(mutationCallback) { const newAllPlayerStates = this.copyPlayerStates(); for (let i = 0; i < newAllPlayerStates.length; i++) { newAllPlayerStates[i] = mutationCallback(newAllPlayerStates[i]); } return new _AllPlayerStates(newAllPlayerStates, this.playerIndexToMove); } movePlayer(playerIndex, to) { const newAllPlayerStates = this.copyPlayerStates(); newAllPlayerStates[playerIndex] = newAllPlayerStates[playerIndex].setPosition(to); return new _AllPlayerStates(newAllPlayerStates, this.playerIndexToMove); } getPlayerToMoveState() { return this.getPlayerState(this.playerIndexToMove); } getPlayerState(playerIndex) { return this.allPlayerStates[playerIndex]; } nextPlayer() { return new _AllPlayerStates( this.allPlayerStates, (this.playerIndexToMove + 1) % this.allPlayerStates.length ); } prevPlayer() { let newPlayerIndexToMove = this.playerIndexToMove - 1; if (newPlayerIndexToMove < 0) { newPlayerIndexToMove = this.allPlayerStates.length - 1; } return new _AllPlayerStates(this.allPlayerStates, newPlayerIndexToMove); } getPlayerStatesWithAllTreasures() { const playerStates = []; for (let i = 0; i < this.allPlayerStates.length; i++) { const playerState = this.allPlayerStates[i]; if (playerState.remainingTreasureCount === 0) { playerStates.push({ playerState, playerIndex: i }); } } return playerStates; } equals(other) { if (this.playerIndexToMove !== other.playerIndexToMove) { return false; } if (this.allPlayerStates.length !== other.allPlayerStates.length) { return false; } for (let i = 0; i < this.allPlayerStates.length; i++) { const a = this.allPlayerStates[i]; const b = other.allPlayerStates[i]; if (!a.equals(b)) { return false; } } return true; } static create(instance) { const allPlayerStates = []; for (const playerState of instance.allPlayerStates) { allPlayerStates.push(PlayerState.create(playerState)); } return new _AllPlayerStates(allPlayerStates, instance.playerIndexToMove); } }; // src/Heading.ts var Heading = /* @__PURE__ */ ((Heading3) => { Heading3[Heading3["NORTH"] = 0] = "NORTH"; Heading3[Heading3["EAST"] = 1] = "EAST"; Heading3[Heading3["SOUTH"] = 2] = "SOUTH"; Heading3[Heading3["WEST"] = 3] = "WEST"; return Heading3; })(Heading || {}); var HeadingHelper = class { heading; constructor(heading) { this.heading = heading; } static getAllHeadings() { return [0 /* NORTH */, 1 /* EAST */, 2 /* SOUTH */, 3 /* WEST */]; } get inverted() { switch (this.heading) { case 0 /* NORTH */: return 2 /* SOUTH */; case 1 /* EAST */: return 3 /* WEST */; case 2 /* SOUTH */: return 0 /* NORTH */; case 3 /* WEST */: return 1 /* EAST */; } } get vec2() { switch (this.heading) { case 0 /* NORTH */: return new Vec2(0, -1); case 1 /* EAST */: return new Vec2(1, 0); case 2 /* SOUTH */: return new Vec2(0, 1); case 3 /* WEST */: return new Vec2(-1, 0); } } }; // src/Path.ts var Path = class { parts; // private readonly indexedParts: (IndexedPathPart | null)[][] = []; constructor(parts) { this.parts = parts; } getHeadings(position) { const headings = []; for (const part of this.parts) { if (part.from.equals(position)) { headings.push(part.heading); } else if (part.to.equals(position)) { headings.push(new HeadingHelper(part.heading).inverted); } } return headings; } get length() { return this.parts.length; } getPart(index) { return this.parts[index]; } }; // src/ShiftPosition.ts var ShiftPosition = class _ShiftPosition { heading; // the direction from wich the shift will start index; // from left to right or top to bottom constructor(heading, index) { this.heading = heading; this.index = index; } get shiftVector() { return new HeadingHelper(this.heading).vec2.multiply(-1); } static create(instance) { return new _ShiftPosition(instance.heading, instance.index); } equals(other) { return this.heading === other.heading && this.index === other.index; } static checkOverflow(position, size) { if (position < 0) { return size - 1; } if (position >= size) { return 0; } return position; } shiftPlayer(playerPosition, width, height) { const isXAxis = this.heading === 1 /* EAST */ || this.heading === 3 /* WEST */; const playerAxis = isXAxis ? playerPosition.y : playerPosition.x; const shiftAxis = this.index * 2 + 1; if (playerAxis === shiftAxis) { const increment = this.heading === 0 /* NORTH */ || this.heading === 3 /* WEST */ ? 1 : -1; if (isXAxis) { return playerPosition.setX( _ShiftPosition.checkOverflow(playerPosition.x + increment, width) ); } else { return playerPosition.setY( _ShiftPosition.checkOverflow(playerPosition.y + increment, height) ); } } else { return playerPosition; } } }; // src/OpenSides.ts var OpenSides = class { northOpen; eastOpen; southOpen; westOpen; constructor(tileType, rotation) { switch (tileType) { case 0 /* STREIGHT */: this.northOpen = false; this.eastOpen = true; this.southOpen = false; this.westOpen = true; break; case 1 /* L */: this.northOpen = false; this.eastOpen = true; this.southOpen = true; this.westOpen = false; break; case 2 /* T */: this.northOpen = true; this.eastOpen = true; this.southOpen = false; this.westOpen = true; break; } while (rotation < 0) { rotation += 4; } for (let i = 0; i < rotation; i++) { const newNorthOpen = this.northOpen; const newEastOpen = this.eastOpen; const newSouthOpen = this.southOpen; const newWestOpen = this.westOpen; this.northOpen = newWestOpen; this.eastOpen = newNorthOpen; this.southOpen = newEastOpen; this.westOpen = newSouthOpen; } } isOpposingOpen(heading) { switch (heading) { case 0 /* NORTH */: return this.southOpen; case 1 /* EAST */: return this.westOpen; case 2 /* SOUTH */: return this.northOpen; case 3 /* WEST */: return this.eastOpen; } } get headings() { const headings = []; if (this.northOpen) { headings.push(0 /* NORTH */); } if (this.eastOpen) { headings.push(1 /* EAST */); } if (this.southOpen) { headings.push(2 /* SOUTH */); } if (this.westOpen) { headings.push(3 /* WEST */); } return headings; } }; // src/PathTile.ts var TileType = /* @__PURE__ */ ((TileType2) => { TileType2[TileType2["STREIGHT"] = 0] = "STREIGHT"; TileType2[TileType2["L"] = 1] = "L"; TileType2[TileType2["T"] = 2] = "T"; return TileType2; })(TileType || {}); var PathTile = class _PathTile { /** * Key */ tileType; treasure; rotation; homeOfPlayerIndex; constructor(tileType, treasure, rotation, homeOfPlayerIndex) { this.tileType = tileType; this.treasure = treasure; this.rotation = _PathTile.normalizeRotation(rotation); this.homeOfPlayerIndex = homeOfPlayerIndex; } get openSides() { return new OpenSides(this.tileType, this.rotation); } static normalizeRotation(rotation) { while (rotation < 0) { rotation += 4; } while (rotation > 4) { rotation -= 4; } return rotation; } rotate(repeat) { if (repeat === 0) { return this; } return new _PathTile( this.tileType, this.treasure, this.rotation + repeat, this.homeOfPlayerIndex ); } setHomeOfPlayerIndex(homeOfPlayerIndex) { return new _PathTile( this.tileType, this.treasure, this.rotation, homeOfPlayerIndex ); } setTreasure(treasure) { return new _PathTile( this.tileType, treasure, this.rotation, this.homeOfPlayerIndex ); } equals(other) { if (this.tileType !== other.tileType) { return false; } if (!Treasure.compare(this.treasure, other.treasure)) { return false; } if (this.rotation !== other.rotation) { return false; } if (this.homeOfPlayerIndex !== other.homeOfPlayerIndex) { return false; } return true; } static create(instance) { return new _PathTile( instance.tileType, instance.treasure === null ? null : Treasure.create(instance.treasure), instance.rotation, instance.homeOfPlayerIndex ); } }; // src/Board.ts var Board = class _Board { tiles; looseTile; shiftPosition; width; height; /** * @param tiles Must a quadratic array * @param looseTile the tile that is currently free */ constructor(tiles, looseTile, shiftPosition) { this.tiles = tiles; this.looseTile = looseTile; this.shiftPosition = shiftPosition; this.width = tiles.length; this.height = tiles[0].length; for (const row of tiles) { if (row.length !== this.height) { throw new Error("Tiles of have invalid shape"); } } if (!_Board.isSizeValid(this.width) || !_Board.isSizeValid(this.height)) { throw new Error( "The provided size is invalid! Size must be >= 7 and cant be prime" ); } } insertLooseTile() { let currentPosition = this.getFirstMovedTilePosition(this.shiftPosition); const newTiles = this.copyTiles(); let lastTile = this.looseTile; while (currentPosition.isInBounds(this.width, this.height)) { const tmp = newTiles[currentPosition.x][currentPosition.y]; newTiles[currentPosition.x][currentPosition.y] = lastTile; lastTile = tmp; currentPosition = currentPosition.add(this.shiftPosition.shiftVector); } return new _Board( newTiles, lastTile, this.invertShiftPosition(this.shiftPosition) ); } generateDistances(from) { const distances = []; for (let x = 0; x < this.width; x++) { const column = []; for (let y = 0; y < this.height; y++) { column[y] = Number.MAX_SAFE_INTEGER; } distances[x] = column; } const checkDistance = (position, distance) => { if (distances[position.x][position.y] > distance) { distances[position.x][position.y] = distance; } else { return; } const tile = this.getTile(position); for (const openHeading of tile.openSides.headings) { const direction = new HeadingHelper(openHeading).vec2; const nextPos = position.add(direction); if (!nextPos.isInBounds(this.width, this.height)) { continue; } const nextTile = this.getTile(nextPos); if (!nextTile.openSides.isOpposingOpen(openHeading)) { continue; } checkDistance(nextPos, distance + 1); } }; checkDistance(from, 0); return distances; } generateShortestPath(from, to) { if (from.equals(to)) { return new Path([]); } const distances = this.generateDistances(to); if (distances[from.x][from.y] === Number.MAX_SAFE_INTEGER) { return null; } const pathParts = []; let currentLocation = from; while (!currentLocation.equals(to)) { let minDistance = Number.MAX_SAFE_INTEGER; let bestHeading = null; const currentTile = this.getTile(currentLocation); for (const heading of currentTile.openSides.headings) { const nextLocation = currentLocation.add( new HeadingHelper(heading).vec2 ); if (!nextLocation.isInBounds(this.width, this.height)) { continue; } const nextTile = this.getTile(nextLocation); if (!nextTile.openSides.isOpposingOpen(heading)) { continue; } if (distances[nextLocation.x][nextLocation.y] < minDistance) { minDistance = distances[nextLocation.x][nextLocation.y]; bestHeading = heading; } } const nextPos = currentLocation.add( new HeadingHelper(bestHeading ?? 0 /* NORTH */).vec2 ); if (bestHeading === null) { throw new Error("Bug"); } pathParts.push({ from: currentLocation, to: nextPos, heading: bestHeading }); currentLocation = nextPos; } return new Path(pathParts); } getReachablePositions(from) { const checkedLocations = []; for (let x = 0; x < this.width; x++) { const column = []; for (let y = 0; y < this.height; y++) { column[y] = false; } checkedLocations[x] = column; } const reachablePositions = []; const check = (position) => { if (!position.isInBounds(this.width, this.height) || checkedLocations[position.x][position.y]) { return; } reachablePositions.push(position); checkedLocations[position.x][position.y] = true; const tile = this.tiles[position.x][position.y]; for (const openHeading of tile.openSides.headings) { const direction = new HeadingHelper(openHeading).vec2; const nextPos = position.add(direction); if (!nextPos.isInBounds(this.width, this.height)) { continue; } const nextTile = this.tiles[nextPos.x][nextPos.y]; if (!nextTile.openSides.isOpposingOpen(openHeading)) { continue; } check(nextPos); } }; check(from); return reachablePositions; } isReachable(from, to) { const distances = this.generateDistances(from); if (distances[to.x][to.y] === Number.MAX_SAFE_INTEGER) { return false; } return true; } rotateLooseTile(rotateBeforeShift) { return new _Board( this.tiles, this.looseTile.rotate(rotateBeforeShift), this.shiftPosition ); } static getValidSizes(maxSize) { const validSizes = []; for (let i = 0; i <= maxSize; i++) { if (_Board.isSizeValid(i)) { validSizes.push(i); } } return validSizes; } /** * @param size Width or height */ static isSizeValid(size) { return size >= 7 && // must be larger than the min size ((size - 1) % 4 === 0 || (size - 1) % 5 === 0 || (size - 1) % 6 === 0) && // must be possible to distribute players home positions equaly size % 2 != 0; } static getMaxPlayerCount(width, height) { return _Board.generatePlayerHomePositions(width, height).length; } static getHomePositionIncrement(size) { if (size < 7) { throw new Error("Invalid size"); } size -= 1; if (size % 4 === 0) { return 4; } if (size % 5 === 0) { return 5; } if (size % 6 === 0) { return 6; } throw new Error("Invalid size"); } static generatePlayerHomePositions(width, height) { const homePositions = []; const xIncrement = _Board.getHomePositionIncrement(width); const YIncrement = _Board.getHomePositionIncrement(height); for (let x = 0; x < width; x += xIncrement) { for (let y = 0; y < height; y += YIncrement) { const pos = new BoardPosition(x, y); if (pos.isEdge(width, height)) { homePositions.push(pos); } } } return homePositions; } static getPlayerHomePosition(playerIndex, width, height) { const homePositions = _Board.generatePlayerHomePositions(width, height); return homePositions[playerIndex]; } generateValidShiftPositions() { const shiftPositions = []; for (const heading of [0 /* NORTH */, 2 /* SOUTH */]) { let index = 0; for (let x = 1; x < this.width - 1; x += 2) { shiftPositions.push(new ShiftPosition(heading, index++)); } } for (const heading of [3 /* WEST */, 1 /* EAST */]) { let index = 0; for (let x = 1; x < this.height - 1; x += 2) { shiftPositions.push(new ShiftPosition(heading, index++)); } } return shiftPositions; } static getShiftPositionCount(size) { return (size - 1) / 2; } isShiftPositionValid(shiftPosition) { if (shiftPosition.index < 0 || !Number.isInteger(shiftPosition.index)) { return false; } switch (shiftPosition.heading) { case 0 /* NORTH */: case 2 /* SOUTH */: return shiftPosition.index < _Board.getShiftPositionCount(this.width); case 1 /* EAST */: case 3 /* WEST */: return shiftPosition.index < _Board.getShiftPositionCount(this.height); } } invertShiftPosition(shiftPosition) { const invertedHeading = new HeadingHelper(shiftPosition.heading).inverted; return new ShiftPosition(invertedHeading, shiftPosition.index); } getLastMovedTilePosition(shiftPosition) { return this.getFirstMovedTilePosition( this.invertShiftPosition(shiftPosition) ); } getFirstMovedTilePosition(shiftPosition) { switch (shiftPosition.heading) { case 0 /* NORTH */: return new BoardPosition(1 + shiftPosition.index * 2, 0); case 1 /* EAST */: return new BoardPosition(this.width - 1, 1 + shiftPosition.index * 2); case 2 /* SOUTH */: return new BoardPosition(1 + shiftPosition.index * 2, this.height - 1); case 3 /* WEST */: return new BoardPosition(0, 1 + shiftPosition.index * 2); } } setShiftPosition(shiftPosition) { return new _Board(this.tiles, this.looseTile, shiftPosition); } /** * Because Tile is immutable we dont need to make a deep copy here */ copyTiles() { const newTiles = []; for (const column of this.tiles) { const newColumn = []; for (const tile of column) { newColumn.push(tile); } newTiles.push(newColumn); } return newTiles; } getTile(position) { return this.tiles[position.x][position.y]; } getTreasureAt(position) { return this.tiles[position.x][position.y].treasure; } equals(other) { if (!this.looseTile.equals(other.looseTile)) { console.log("looseTile"); return false; } if (!this.shiftPosition.equals(other.shiftPosition)) { return false; } if (this.width !== other.width || this.height !== other.height) { return false; } for (let x = 0; x < this.width; x++) { for (let y = 0; y < this.height; y++) { if (!this.tiles[x][y].equals(other.tiles[x][y])) { return false; } } } return true; } static create(instance) { const tiles = []; for (const row of instance.tiles) { const newRow = []; for (const tile of row) { newRow.push(PathTile.create(tile)); } tiles.push(newRow); } return new _Board( tiles, PathTile.create(instance.looseTile), ShiftPosition.create(instance.shiftPosition) ); } }; // src/Move.ts var Move = class _Move { playerIndex; rotateBeforeShift; // multiple of 90deg cw fromShiftPosition; toShiftPosition; from; to; collectedTreasure; constructor(playerIndex, rotateBeforeShift, fromShiftPosition, toShiftPosition, from, to, collectedTreasure) { this.playerIndex = playerIndex; this.rotateBeforeShift = rotateBeforeShift; this.fromShiftPosition = fromShiftPosition; this.toShiftPosition = toShiftPosition; this.from = from; this.to = to; this.collectedTreasure = collectedTreasure; } static create(instance) { return new _Move( instance.playerIndex, instance.rotateBeforeShift, ShiftPosition.create(instance.fromShiftPosition), ShiftPosition.create(instance.toShiftPosition), BoardPosition.create(instance.from), BoardPosition.create(instance.to), instance.collectedTreasure === null ? null : Treasure.create(instance.collectedTreasure) ); } equals(other) { return this.playerIndex !== other.playerIndex && this.rotateBeforeShift !== other.rotateBeforeShift && this.fromShiftPosition.equals(other.fromShiftPosition) && this.toShiftPosition.equals(other.toShiftPosition) && this.from.equals(other.from) && this.to.equals(other.to) && Treasure.compare(this.collectedTreasure, other.collectedTreasure); } }; // src/Cyrb64.ts var Cyrb64 = class _Cyrb64 { a; b; constructor(a, b) { this.a = a; this.b = b; } static hashString(str, seed) { let h1 = 3735928559 ^ seed, h2 = 1103547991 ^ seed; for (let i = 0, ch; i < str.length; i++) { ch = str.charCodeAt(i); h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); } h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507); h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909); h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507); h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909); return new _Cyrb64(h2 >>> 0, h1 >>> 0); } equals(other) { return this.a === other.a && this.b === other.b; } }; // src/GameState.ts var GameState = class _GameState { board; allPlayerStates; historyMoves; hashCache = null; constructor(board, allPlayerStates, historyMoves) { this.board = board; this.allPlayerStates = allPlayerStates; this.historyMoves = historyMoves; } /** * @throws Error in case the move is not valid */ validateMove(move) { var _a; if (!this.board.isShiftPositionValid(move.fromShiftPosition) || !this.board.isShiftPositionValid(move.toShiftPosition)) { throw new Error("Invalid shift position"); } if (!this.board.shiftPosition.equals(move.fromShiftPosition)) { throw new Error("Invalid starting shiftPosition"); } const gameStatedAfterSlide = this.setShiftPosition(move.toShiftPosition).rotateLooseTile(move.rotateBeforeShift).insertLooseTile(); const playerState = gameStatedAfterSlide.allPlayerStates.getPlayerState( move.playerIndex ); if (playerState === null) { throw new Error("Invalid playerIndex"); } if (!move.from.equals(playerState.position)) { console.log(move.from, playerState.position); throw new Error("Invalid starting position"); } if (!gameStatedAfterSlide.board.isReachable(move.from, move.to)) { throw new Error("Destination is not in reach"); } let expectedTreasure = null; const treasureAtTo = gameStatedAfterSlide.board.getTile(move.to).treasure; if (treasureAtTo !== null) { if ((_a = playerState.currentTreasure) == null ? void 0 : _a.equals(treasureAtTo)) { expectedTreasure = treasureAtTo; } } if (!Treasure.compare(expectedTreasure, move.collectedTreasure)) { console.log(expectedTreasure, move.collectedTreasure); console.log(JSON.stringify(move)); throw new Error("Invalid collected treasure"); } } /** * @throws Error in case the move is not valid */ move(move) { this.validateMove(move); return this.rotateLooseTile(move.rotateBeforeShift).setShiftPosition(move.toShiftPosition).insertLooseTile().movePlayer(move.playerIndex, move.to).collectTreasure(move.playerIndex, move.collectedTreasure).nextPlayer().addMoveToHistory(move); } undoMove() { if (this.historyMoves.length === 0) { throw new Error("no moves to undo"); } const move = this.historyMoves[this.historyMoves.length - 1]; return { newGameState: this.removeLastHistoryMove().prevPlayer().removeLastTreasure(move.playerIndex, move.collectedTreasure).movePlayer(move.playerIndex, move.from).insertLooseTile().setShiftPosition(move.fromShiftPosition).rotateLooseTile(-move.rotateBeforeShift), undoneMove: move }; } rotateLooseTile(rotateBeforeShift) { return new _GameState( this.board.rotateLooseTile(rotateBeforeShift), this.allPlayerStates, this.historyMoves ); } setShiftPosition(shiftPosition) { const newBoard = this.board.setShiftPosition(shiftPosition); return new _GameState(newBoard, this.allPlayerStates, this.historyMoves); } insertLooseTile() { const newAllPlayerStates = this.allPlayerStates.mutateAll( (playerState) => { return playerState.setPosition( this.board.shiftPosition.shiftPlayer( playerState.position, this.board.width, this.board.height ) ); } ); const newBoard = this.board.insertLooseTile(); return new _GameState(newBoard, newAllPlayerStates, this.historyMoves); } movePlayer(playerIndex, to) { const newAllPlayerStates = this.allPlayerStates.movePlayer(playerIndex, to); return new _GameState(this.board, newAllPlayerStates, this.historyMoves); } collectTreasure(playerIndex, foundTreasure) { if (foundTreasure === null) { return this; } const newAllPlayerStates = this.allPlayerStates.collectTreasure( playerIndex, foundTreasure ); return new _GameState(this.board, newAllPlayerStates, this.historyMoves); } getWinnerIndex() { const playerStates = this.allPlayerStates.getPlayerStatesWithAllTreasures(); for (const player of playerStates) { const playerHomePoint = Board.getPlayerHomePosition( player.playerIndex, this.board.width, this.board.height ); if (player.playerState.position.equals(playerHomePoint)) { return player.playerIndex; } } return null; } removeLastTreasure(playerIndex, foundTreasure) { if (foundTreasure === null) { return this; } const newAllPlayerStates = this.allPlayerStates.removeLastTreasure(playerIndex); return new _GameState(this.board, newAllPlayerStates, this.historyMoves); } nextPlayer() { return new _GameState( this.board, this.allPlayerStates.nextPlayer(), this.historyMoves ); } prevPlayer() { return new _GameState( this.board, this.allPlayerStates.prevPlayer(), this.historyMoves ); } copyHistory() { const newHistory = []; for (const historyMove of this.historyMoves) { newHistory.push(historyMove); } return newHistory; } addMoveToHistory(move) { const newHistory = this.copyHistory(); newHistory.push(move); return new _GameState(this.board, this.allPlayerStates, newHistory); } removeLastHistoryMove() { const newHistory = this.copyHistory(); newHistory.pop(); return new _GameState(this.board, this.allPlayerStates, newHistory); } equals(other) { return other.hash().equals(this.hash()); } static create(instance) { const historyMoves = []; for (const historyMove of instance.historyMoves) { historyMoves.push(Move.create(historyMove)); } return new _GameState( Board.create(instance.board), AllPlayerStates.create(instance.allPlayerStates), historyMoves ); } hash() { if (this.hashCache === null) { this.hashCache = Cyrb64.hashString(JSON.stringify(this), 0); } return this.hashCache; } }; // src/RandomNumberGenerator.ts var RandomNumberGenerator = class { seed; a; b; c; d; generator; constructor(seed) { this.seed = seed; [this.a, this.b, this.c, this.d] = this.cyrb128(seed); this.generator = this.sfc32(this.a, this.b, this.c, this.d); } rand() { return this.generator(); } cyrb128(str) { let h1 = 1779033703, h2 = 3144134277, h3 = 1013904242, h4 = 2773480762; for (let i = 0; i < str.length; i++) { const k = str.charCodeAt(i); h1 = h2 ^ Math.imul(h1 ^ k, 597399067); h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); h3 = h4 ^ Math.imul(h3 ^ k, 951274213); h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); } h1 = Math.imul(h3 ^ h1 >>> 18, 597399067); h2 = Math.imul(h4 ^ h2 >>> 22, 2869860233); h3 = Math.imul(h1 ^ h3 >>> 17, 951274213); h4 = Math.imul(h2 ^ h4 >>> 19, 2716044179); h1 ^= h2 ^ h3 ^ h4, h2 ^= h1, h3 ^= h1, h4 ^= h1; return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0]; } sfc32(a, b, c, d) { return function() { a |= 0; b |= 0; c |= 0; d |= 0; let t = (a + b | 0) + d | 0; d = d + 1 | 0; a = b ^ b >>> 9; b = c + (c << 3) | 0; c = c << 21 | c >>> 11; c = c + t | 0; return (t >>> 0) / 4294967296; }; } }; // src/Game.ts var Game = class _Game { // Game state _gameState; // stuff redoHistory = []; constructor(gameState) { this._gameState = gameState; } /** * Game Logic ------------------- */ 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); this._gameState = this._gameState.move(move); this.redoHistory = []; } undoLastMove() { const result = this._gameState.undoMove(); this._gameState = result.newGameState; this.redoHistory.push(result.undoneMove); } redoLastMove() { const redoMove = this.redoHistory.pop(); if (redoMove === void 0) { throw new Error("no move to redo"); } this._gameState = this._gameState.move(redoMove); } get gameState() { return this._gameState; } /** * Setup logic */ static getDefaultSetup() { 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 } }; } static finalizeSetup(setup) { const defaultSetup = _Game.getDefaultSetup(); const finalSetup = { boardHeight: (setup == null ? void 0 : setup.boardHeight) ?? defaultSetup.boardHeight, boardWidth: (setup == null ? void 0 : setup.boardWidth) ?? defaultSetup.boardWidth, cardsRatio: (setup == null ? void 0 : setup.cardsRatio) ?? defaultSetup.cardsRatio, playerCount: (setup == null ? void 0 : setup.playerCount) ?? defaultSetup.playerCount, seed: (setup == null ? void 0 : setup.seed) ?? defaultSetup.seed, treasureCardChances: (setup == null ? void 0 : 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; } static buildFromSetup(partialSetup) { 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); } static getTreasureCount(playerCount) { if (playerCount <= 4) { return 24; } else { return playerCount * 6; } } static generateRandomBoard(generator, treasures, width, height, cardsRatios, treasureCardChances) { var _a, _b; const tiles = []; for (let x = 0; x < width; x++) { const column = []; for (let y = 0; y < height; y++) { column[y] = null; } tiles.push(column); } tiles[0][0] = new PathTile(1 /* L */, null, 0, null); tiles[width - 1][0] = new PathTile(1 /* L */, null, 1, null); tiles[width - 1][height - 1] = new PathTile(1 /* L */, null, 2, null); tiles[0][height - 1] = new PathTile(1 /* L */, null, 3, null); const getRandomTreasure = _Game.getRandomTreasureProvider( generator, treasures, treasureCardChances ); const homePoints = Board.generatePlayerHomePositions(width, height); function isHome(x, y) { for (const homePoint of homePoints) { if (homePoint.x === x && homePoint.y === y) { return true; } } return false; } for (let x = 2; x < width - 2; x += 2) { tiles[x][0] = new PathTile( 2 /* T */, getRandomTreasure(isHome(x, 0) ? "homePoint" : "fix"), 2, null ); } for (let y = 2; y < height - 2; y += 2) { tiles[width - 1][y] = new PathTile( 2 /* T */, getRandomTreasure(isHome(width - 1, y) ? "homePoint" : "fix"), 3, null ); } for (let x = 2; x < width - 2; x += 2) { tiles[x][height - 1] = new PathTile( 2 /* T */, getRandomTreasure(isHome(x, height - 1) ? "homePoint" : "fix"), 0, null ); } for (let y = 2; y < height - 2; y += 2) { tiles[0][y] = new PathTile( 2 /* T */, getRandomTreasure(isHome(0, y) ? "homePoint" : "fix"), 1, null ); } for (let x = 2; x < width - 2; x += 2) { for (let y = 2; y < height - 2; y += 2) { tiles[x][y] = new PathTile( 2 /* T */, getRandomTreasure(isHome(x, y) ? "homePoint" : "fix"), Math.floor(generator.rand() * 4), null ); } } const treasureCandidates = [ [], // T cards [], // L cards [] // Streight cards ]; const tileTypes = [2 /* T */, 1 /* L */, 0 /* STREIGHT */]; function idxFromTileType(tileType) { switch (tileType) { case 2 /* T */: return 0; case 1 /* L */: return 1; case 0 /* 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"); 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] = ((_a = tiles[chosenCandidate.x][chosenCandidate.y]) == null ? void 0 : _a.setTreasure( treasure )) ?? null; } } } for (let i = 0; i < homePoints.length; i++) { const homePoint = homePoints[i]; tiles[homePoint.x][homePoint.y] = ((_b = tiles[homePoint.x][homePoint.y]) == null ? void 0 : _b.setHomeOfPlayerIndex(i)) ?? null; } const startingShiftPosition = new ShiftPosition(0 /* NORTH */, 0); return new Board(tiles, looseTile, startingShiftPosition); } static generateRandomTileType(generator) { const rand = generator.rand() * 3; if (rand < 1) { return 1 /* L */; } else if (rand < 2) { return 2 /* T */; } else { return 0 /* STREIGHT */; } } static getRandomTreasureProvider(generator, treasures, treasureCardChances) { const remainingTreasures = []; for (const treasure of treasures) { remainingTreasures.push(treasure); } function getRandomTreasure(cardType) { let skipChance = 0; if (treasureCardChances !== void 0 && cardType !== void 0) { switch (cardType) { case 1 /* L */: skipChance = 1 - treasureCardChances.lCardTreasureChance; break; case 2 /* T */: skipChance = 1 - treasureCardChances.tCardTreasureChance; break; case 0 /* 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 */ static generatePlayerStates(generator, numberOfPlayers, treasures, boardWidth, boardHeight) { const getRandomTreasure = _Game.getRandomTreasureProvider( generator, treasures ); const treasuresPerPlayer = treasures.length / numberOfPlayers; const allPlayersTreasures = []; for (let i = 0; i < numberOfPlayers; i++) { const playerTreasures = []; for (let i2 = 0; i2 < treasuresPerPlayer; i2++) { const treasure = getRandomTreasure(); if (treasure) { playerTreasures.push(treasure); } } allPlayersTreasures.push(playerTreasures); } const playerStates = []; 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); } static generateTreasures(amount) { const treasures = []; for (let i = 0; i < amount; i++) { treasures.push(new Treasure(i)); } return treasures; } static buildFromString(str) { const obj = JSON.parse(str); if (!("gameState" in obj)) { throw new Error("Invalid game string"); } return new _Game(GameState.create(obj.gameState)); } stringify() { return JSON.stringify({ gameState: this._gameState }); } }; // src/MoveGenerator.ts var BEST_MOVE_WEIGHT = 1e4; function generateShiftPositions(gameState) { return gameState.board.generateValidShiftPositions(); } function generateRandomMove(gameState) { const shiftPositions = generateShiftPositions(gameState); const shiftPositionIndex = Math.floor(Math.random() * shiftPositions.length); const moves = generateMoves( gameState, shiftPositions[shiftPositionIndex], Math.floor(Math.random() * 4) ); const index = Math.floor(Math.random() * moves.length); return moves[index]; } function generateMoves(gameState, toShiftPosition, rotation) { const moves = []; const movedGameState = gameState.setShiftPosition(toShiftPosition).rotateLooseTile(rotation).insertLooseTile(); const playerState = movedGameState.allPlayerStates.getPlayerToMoveState(); const reachableFields = movedGameState.board.getReachablePositions( playerState.position ); for (const reachableField of reachableFields) { const treasureAtTile = movedGameState.board.getTreasureAt(reachableField); let collectedTreasure = null; if (Treasure.compare(treasureAtTile, playerState.currentTreasure)) { collectedTreasure = playerState.currentTreasure; } moves.push( new Move( movedGameState.allPlayerStates.playerIndexToMove, rotation, gameState.board.shiftPosition, toShiftPosition, playerState.position, reachableField, collectedTreasure ) ); } return moves; } function positionByTreasure(board, treasure) { for (let x = 0; x < board.width; x++) { for (let y = 0; y < board.height; y++) { const tile = board.getTile(new BoardPosition(x, y)); if (Treasure.compare(tile.treasure, treasure)) { return new BoardPosition(x, y); } }