labyrinth-game-logic
Version:
A libary that implements the boardgame labyrinth and exposes the functionality to create and interact with games
245 lines (224 loc) • 7.75 kB
text/typescript
/**
* @author Timo lehnertz
*/
import { AllPlayerStates } from "./AllPlayerStates";
import { Board } from "./Board";
import { BoardPosition } from "./BoardPosition";
import { ShiftPosition } from "./ShiftPosition";
import { Move } from "./Move";
import { PlayerState } from "./PlayerState";
import { Treasure } from "./Treasure";
import { Cyrb64 } from "./Cyrb64";
/**
* GameState class representing the state of a game
* This class exposes all information about the game that a player could legally know by looking at the board
*/
export class GameState {
public readonly board: Board;
public readonly allPlayerStates: AllPlayerStates;
public readonly historyMoves: Move[];
private hashCache: Cyrb64 | null = null;
public constructor(
board: Board,
allPlayerStates: AllPlayerStates,
historyMoves: Move[]
) {
this.board = board;
this.allPlayerStates = allPlayerStates;
this.historyMoves = historyMoves;
}
/**
* @throws Error in case the move is not valid
*/
public validateMove(move: Move) {
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: Treasure | null = null;
const treasureAtTo = gameStatedAfterSlide.board.getTile(move.to).treasure;
if (treasureAtTo !== null) {
if (playerState.currentTreasure?.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
*/
public move(move: Move): GameState {
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);
}
public undoMove(): { newGameState: GameState; undoneMove: Move } {
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,
};
}
public rotateLooseTile(rotateBeforeShift: number): GameState {
return new GameState(
this.board.rotateLooseTile(rotateBeforeShift),
this.allPlayerStates,
this.historyMoves
);
}
public setShiftPosition(shiftPosition: ShiftPosition): GameState {
const newBoard = this.board.setShiftPosition(shiftPosition);
return new GameState(newBoard, this.allPlayerStates, this.historyMoves);
}
public insertLooseTile(): GameState {
const newAllPlayerStates = this.allPlayerStates.mutateAll(
(playerState: PlayerState): 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);
}
private movePlayer(playerIndex: number, to: BoardPosition) {
const newAllPlayerStates = this.allPlayerStates.movePlayer(playerIndex, to);
return new GameState(this.board, newAllPlayerStates, this.historyMoves);
}
private collectTreasure(
playerIndex: number,
foundTreasure: Treasure | null
): GameState {
if (foundTreasure === null) {
return this;
}
const newAllPlayerStates = this.allPlayerStates.collectTreasure(
playerIndex,
foundTreasure
);
return new GameState(this.board, newAllPlayerStates, this.historyMoves);
}
public getWinnerIndex(): number | null {
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;
}
private removeLastTreasure(
playerIndex: number,
foundTreasure: Treasure | null
): GameState {
if (foundTreasure === null) {
return this;
}
const newAllPlayerStates =
this.allPlayerStates.removeLastTreasure(playerIndex);
return new GameState(this.board, newAllPlayerStates, this.historyMoves);
}
private nextPlayer(): GameState {
return new GameState(
this.board,
this.allPlayerStates.nextPlayer(),
this.historyMoves
);
}
private prevPlayer(): GameState {
return new GameState(
this.board,
this.allPlayerStates.prevPlayer(),
this.historyMoves
);
}
private copyHistory(): Move[] {
const newHistory: Move[] = [];
for (const historyMove of this.historyMoves) {
newHistory.push(historyMove);
}
return newHistory;
}
private addMoveToHistory(move: Move): GameState {
const newHistory = this.copyHistory();
newHistory.push(move);
return new GameState(this.board, this.allPlayerStates, newHistory);
}
private removeLastHistoryMove(): GameState {
const newHistory = this.copyHistory();
newHistory.pop();
return new GameState(this.board, this.allPlayerStates, newHistory);
}
public equals(other: GameState): boolean {
// if (!other.hash().equals(this.hash())) {
// console.log(this.board.looseTile);
// console.log(other.board.looseTile);
// }
return other.hash().equals(this.hash());
}
public static create(instance: GameState): GameState {
const historyMoves: Move[] = [];
for (const historyMove of instance.historyMoves) {
historyMoves.push(Move.create(historyMove));
}
return new GameState(
Board.create(instance.board),
AllPlayerStates.create(instance.allPlayerStates),
historyMoves
);
}
public hash(): Cyrb64 {
if (this.hashCache === null) {
this.hashCache = Cyrb64.hashString(JSON.stringify(this), 0);
}
return this.hashCache;
}
}