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
JavaScript
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);
}
}