initial commit
This commit is contained in:
238
src/game/DominoesGame.ts
Normal file
238
src/game/DominoesGame.ts
Normal file
@ -0,0 +1,238 @@
|
||||
import { PRNG } from 'seedrandom';
|
||||
import { Board } from "./entities/Board";
|
||||
import { PlayerMove } from "./entities/PlayerMove";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
import { Tile } from "./entities/Tile";
|
||||
import { LoggingService } from "../common/LoggingService";
|
||||
import { printBoard, printLine, uuid, wait } from '../common/utilities';
|
||||
import { GameSummary } from './dto/GameSummary';
|
||||
import { PlayerNotificationManager } from './PlayerNotificationManager';
|
||||
import { GameState } from './dto/GameState';
|
||||
|
||||
export class DominoesGame {
|
||||
private id: string;
|
||||
private seed: string | undefined;
|
||||
autoDeal: boolean = true;
|
||||
board: Board;
|
||||
currentPlayerIndex: number = 0;
|
||||
gameInProgress: boolean = false;
|
||||
gameOver: boolean = false;
|
||||
gameBlocked: boolean = false;
|
||||
gameTied: boolean = false;
|
||||
tileSelectionPhase: boolean = true;
|
||||
logger: LoggingService = new LoggingService();
|
||||
blockedCount: number = 0;
|
||||
winner: PlayerInterface | null = null;
|
||||
rng: PRNG;
|
||||
handSize: number = 7;
|
||||
notificationManager: PlayerNotificationManager = new PlayerNotificationManager(this);
|
||||
lastMove: PlayerMove | null = null;
|
||||
|
||||
constructor(public players: PlayerInterface[], seed: PRNG) {
|
||||
this.id = uuid();
|
||||
this.logger.info(`Game ID: ${this.id}`);
|
||||
this.logger.info(`Seed: ${this.seed}`);
|
||||
this.rng = seed
|
||||
this.board = new Board(seed);
|
||||
this.initializeGame();
|
||||
}
|
||||
|
||||
async initializeGame() {
|
||||
this.gameOver = false;
|
||||
this.gameBlocked = false;
|
||||
this.gameTied = false;
|
||||
this.board.boneyard = this.generateTiles();
|
||||
}
|
||||
|
||||
generateTiles(): Tile[] {
|
||||
const tiles: Tile[] = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
for (let j = i; j >= 0; j--) {
|
||||
tiles.push(new Tile([i, j]));
|
||||
}
|
||||
}
|
||||
this.logger.debug('tiles :>> ' + tiles);
|
||||
return this.shuffle(tiles);
|
||||
}
|
||||
|
||||
private shuffle(array: Tile[]): Tile[] {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(this.rng() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
nextPlayer() {
|
||||
this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
|
||||
}
|
||||
|
||||
isBlocked(): boolean {
|
||||
return this.blockedCount === this.players.length;
|
||||
}
|
||||
|
||||
isGameOver(): boolean {
|
||||
const hasWinner: boolean = this.players.some(player => player.hand.length === 0);
|
||||
return hasWinner || this.gameBlocked;
|
||||
}
|
||||
|
||||
getWinner(): PlayerInterface | null {
|
||||
if (!this.gameOver) {
|
||||
return null;
|
||||
}
|
||||
const winnerNoTiles = this.players.find(player => player.hand.length === 0);
|
||||
if (winnerNoTiles !== undefined) {
|
||||
return winnerNoTiles;
|
||||
}
|
||||
const winnerMinPipsCount = this.players.reduce((acc, player) => {
|
||||
return player.pipsCount() < acc.pipsCount() ? player : acc;
|
||||
});
|
||||
return winnerMinPipsCount;
|
||||
}
|
||||
|
||||
getStartingPlayerIndex(): number {
|
||||
// Determine starting player
|
||||
let startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 6 && tile.pips[1] === 6));
|
||||
if (startingIndex === -1) {
|
||||
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 5 && tile.pips[1] === 5));
|
||||
if (startingIndex === -1) {
|
||||
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 4 && tile.pips[1] === 4));
|
||||
if (startingIndex === -1) {
|
||||
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 3 && tile.pips[1] === 3));
|
||||
if (startingIndex === -1) {
|
||||
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 2 && tile.pips[1] === 2));
|
||||
if (startingIndex === -1) {
|
||||
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 1 && tile.pips[1] === 1));
|
||||
if (startingIndex === -1) {
|
||||
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 0 && tile.pips[1] === 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return startingIndex === -1 ? 0 : startingIndex;
|
||||
}
|
||||
|
||||
async playTurn(): Promise<void> {
|
||||
const player = this.players[this.currentPlayerIndex];
|
||||
console.log(`${player.name}'s turn (${player.hand.length} tiles)`);
|
||||
printBoard(this.board);
|
||||
|
||||
// let playerMove: PlayerMove | null = null;
|
||||
// while(playerMove === null) {
|
||||
// try {
|
||||
// playerMove = await player.makeMove(this.board);
|
||||
// } catch (error) {
|
||||
// this.logger.error(error, 'Error making move');
|
||||
// }
|
||||
// }
|
||||
const playerMove = await player.makeMove(this.board);
|
||||
printBoard(this.board, true);
|
||||
this.lastMove = playerMove;
|
||||
if (playerMove === null) {
|
||||
console.log('Player cannot move');
|
||||
this.blockedCount += 1;
|
||||
this.nextPlayer();
|
||||
return;
|
||||
}
|
||||
this.blockedCount = 0;
|
||||
this.board.play(playerMove);
|
||||
player.hand = player.hand.filter(tile => tile !== playerMove.tile);
|
||||
this.nextPlayer();
|
||||
}
|
||||
|
||||
async start(): Promise<GameSummary> {
|
||||
this.gameInProgress = false;
|
||||
this.tileSelectionPhase = true;
|
||||
await this.notificationManager.notifyGameState();
|
||||
await this.notificationManager.notifyPlayersState();
|
||||
this.logger.debug('clients received boneyard :>> ' + this.board.boneyard);
|
||||
await wait(1000);
|
||||
|
||||
if (this.autoDeal) {
|
||||
this.dealTiles();
|
||||
await this.notificationManager.notifyGameState();
|
||||
await this.notificationManager.notifyPlayersState();
|
||||
} else {
|
||||
await this.tilesSelection();
|
||||
}
|
||||
|
||||
this.tileSelectionPhase = false;
|
||||
this.gameInProgress = true;
|
||||
this.currentPlayerIndex = this.getStartingPlayerIndex();
|
||||
printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`);
|
||||
while (!this.gameOver) {
|
||||
await this.playTurn();
|
||||
await this.notificationManager.notifyGameState();
|
||||
await this.notificationManager.notifyPlayersState();
|
||||
this.gameBlocked = this.isBlocked();
|
||||
this.gameOver = this.isGameOver();
|
||||
}
|
||||
this.gameInProgress = false;
|
||||
this.winner = this.getWinner();
|
||||
|
||||
return {
|
||||
gameId: this.id,
|
||||
isBlocked: this.gameBlocked,
|
||||
isTied: this.gameTied,
|
||||
winner: this.winner
|
||||
};
|
||||
}
|
||||
|
||||
dealTiles() {
|
||||
for (let i = 0; i < this.handSize; i++) {
|
||||
for (let player of this.players) {
|
||||
const tile: Tile | undefined = this.board.boneyard.pop();
|
||||
if (tile !== undefined) {
|
||||
tile.revealed = true;
|
||||
player.hand.push(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async tilesSelection() {
|
||||
while (this.board.boneyard.length > 0) {
|
||||
for (let player of this.players) {
|
||||
const choosen = await player.chooseTile(this.board);
|
||||
await this.notificationManager.notifyGameState();
|
||||
await this.notificationManager.notifyPlayersState();
|
||||
if (this.board.boneyard.length === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getGameState(): GameState {
|
||||
const currentPlayer = this.players[this.currentPlayerIndex]
|
||||
return {
|
||||
id: uuid(),
|
||||
lastMove: this.lastMove,
|
||||
gameInProgress: this.gameInProgress,
|
||||
winner: this.winner,
|
||||
tileSelectionPhase: this.tileSelectionPhase,
|
||||
gameBlocked: this.gameBlocked,
|
||||
gameTied: this.gameTied,
|
||||
gameId: this.id,
|
||||
boneyard: this.board.boneyard.map(tile => ({ id: tile.id})),
|
||||
players: this.players.map(player => ({
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
score: player.score,
|
||||
hand: player.hand.map(tile => tile.id),
|
||||
})),
|
||||
currentPlayer: {
|
||||
id: currentPlayer.id,
|
||||
name: currentPlayer.name
|
||||
},
|
||||
board: this.board.tiles.map(tile => ({
|
||||
id: tile.id,
|
||||
pips: tile.pips
|
||||
})),
|
||||
boardFreeEnds: this.board.getFreeEnds(),
|
||||
}
|
||||
|
||||
}
|
||||
}
|
149
src/game/GameSession.ts
Normal file
149
src/game/GameSession.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { DominoesGame } from "./DominoesGame";
|
||||
import { PlayerAI } from "./entities/player/PlayerAI";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
import { LoggingService } from "../common/LoggingService";
|
||||
import { getRandomSeed, uuid, wait } from "../common/utilities";
|
||||
import { GameSessionState } from "./dto/GameSessionState";
|
||||
import { PlayerNotificationManager } from './PlayerNotificationManager';
|
||||
import seedrandom, { PRNG } from "seedrandom";
|
||||
|
||||
export class GameSession {
|
||||
private game: DominoesGame | null = null;
|
||||
private minHumanPlayers: number = 1;
|
||||
private waitingForPlayers: boolean = true;
|
||||
private waitingSeconds: number = 0;
|
||||
private logger: LoggingService = new LoggingService();
|
||||
private mode: string = 'classic';
|
||||
private pointsToWin: number = 100;
|
||||
private playerNotificationManager: PlayerNotificationManager;
|
||||
id: string;
|
||||
players: PlayerInterface[] = [];
|
||||
sessionInProgress: boolean = false;
|
||||
maxPlayers: number = 4;
|
||||
seed!: string
|
||||
rng!: PRNG
|
||||
|
||||
constructor(public creator: PlayerInterface, public name?: string) {
|
||||
this.playerNotificationManager = new PlayerNotificationManager(this);
|
||||
this.id = uuid();
|
||||
this.name = name || `Game ${this.id}`;
|
||||
this.addPlayer(creator);
|
||||
this.logger.info(`GameSession created by: ${creator.name}`);
|
||||
this.creator = creator;
|
||||
this.playerNotificationManager.notifySessionState();
|
||||
}
|
||||
|
||||
get numPlayers() {
|
||||
return this.players.length;
|
||||
}
|
||||
|
||||
private async startGame(seed: string) {
|
||||
this.rng = seedrandom(seed);
|
||||
const missingPlayers = this.maxPlayers - this.numPlayers;
|
||||
for (let i = 0; i < missingPlayers; i++) {
|
||||
this.addPlayer(this.createPlayerAI(i));
|
||||
}
|
||||
this.game = new DominoesGame(this.players, this.rng);
|
||||
this.sessionInProgress = true;
|
||||
this.logger.info('Game started');
|
||||
this.playerNotificationManager.notifySessionState();
|
||||
await this.game.start();
|
||||
return this.endGame();
|
||||
}
|
||||
|
||||
private endGame(): any {
|
||||
if (this.game !== null) {
|
||||
this.sessionInProgress = false;
|
||||
const { gameBlocked, gameTied, winner } = this.game;
|
||||
|
||||
gameBlocked ? console.log('Game blocked!') : gameTied ? console.log('Game tied!') : console.log('Game over!');
|
||||
console.log('Winner: ' + winner?.name + ' with ' + winner?.pipsCount() + ' points');
|
||||
this.getScore(this.game);
|
||||
this.sessionInProgress = false;
|
||||
this.logger.info('Game ended');
|
||||
this.game = null;
|
||||
this.playerNotificationManager.notifySessionState();
|
||||
|
||||
return {
|
||||
gameBlocked,
|
||||
gameTied,
|
||||
winner
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getScore(game: DominoesGame) {
|
||||
const pips = game.players
|
||||
.sort((a,b) => (b.pipsCount() - a.pipsCount()))
|
||||
.map(player => {
|
||||
return `${player.name}: ${player.pipsCount()}`;
|
||||
});
|
||||
console.log(`Pips count: ${pips.join(', ')}`);
|
||||
const totalPoints = game.players.reduce((acc, player) => acc + player.pipsCount(), 0);
|
||||
if (game.winner !== null) {
|
||||
game.winner.score += totalPoints;
|
||||
}
|
||||
const scores = game.players
|
||||
.sort((a,b) => (b.score - a.score))
|
||||
.map(player => {
|
||||
return `${player.name}: ${player.score}`;
|
||||
});
|
||||
console.log(`Scores: ${scores.join(', ')}`);
|
||||
}
|
||||
|
||||
createPlayerAI(i: number) {
|
||||
const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
|
||||
return new PlayerAI(AInames[i], this.rng);
|
||||
}
|
||||
|
||||
async start(seed?: string) {
|
||||
this.seed = seed || getRandomSeed();
|
||||
console.log('seed :>> ', this.seed);
|
||||
if (this.sessionInProgress) {
|
||||
throw new Error("Game already in progress");
|
||||
}
|
||||
this.waitingForPlayers = true;
|
||||
this.logger.info('Waiting for players to join');
|
||||
while (this.numPlayers < this.maxPlayers) {
|
||||
this.waitingSeconds += 1;
|
||||
this.logger.info(`Waiting for players to join: ${this.waitingSeconds}`);
|
||||
await wait(1000);
|
||||
}
|
||||
this.waitingForPlayers = false;
|
||||
this.logger.info('All players joined');
|
||||
this.startGame(this.seed);
|
||||
}
|
||||
|
||||
addPlayer(player: PlayerInterface) {
|
||||
if (this.numPlayers >= this.maxPlayers) {
|
||||
throw new Error("GameSession is full");
|
||||
}
|
||||
this.players.push(player);
|
||||
this.logger.info(`${player.name} joined the game!`);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `GameSession:(${this.id} ${this.name})`;
|
||||
}
|
||||
|
||||
getState(): GameSessionState {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name!,
|
||||
creator: this.creator.id,
|
||||
players: this.players.map(player =>( {
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
})),
|
||||
sessionInProgress: this.sessionInProgress,
|
||||
maxPlayers: this.maxPlayers,
|
||||
numPlayers: this.numPlayers,
|
||||
waitingForPlayers: this.waitingForPlayers,
|
||||
waitingSeconds: this.waitingSeconds,
|
||||
seed: this.seed,
|
||||
mode: this.mode,
|
||||
pointsToWin: this.pointsToWin,
|
||||
status: this.sessionInProgress ? 'in progress' : 'waiting'
|
||||
};
|
||||
}
|
||||
}
|
37
src/game/NetworkClientNotifier.ts
Normal file
37
src/game/NetworkClientNotifier.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
|
||||
import { LoggingService } from "../common/LoggingService";
|
||||
|
||||
export class NetworkClientNotifier {
|
||||
static instance: NetworkClientNotifier;
|
||||
io: any;
|
||||
logger: LoggingService = new LoggingService();
|
||||
constructor() {
|
||||
if (!NetworkClientNotifier.instance) {
|
||||
NetworkClientNotifier.instance = this;
|
||||
}
|
||||
|
||||
return NetworkClientNotifier.instance
|
||||
}
|
||||
|
||||
setSocket(io: any) {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
async notifyPlayer(player: NetworkPlayer, event: string, data: any = {}, timeoutSecs: number = 300): Promise<any> {
|
||||
try {
|
||||
const response = await this.io.to(player.socketId)
|
||||
.timeout(timeoutSecs * 1000)
|
||||
.emitWithAck(event, data);
|
||||
return response[0]
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async broadcast(event: string, data: any) {
|
||||
const responses = await this.io.emit(event, data);
|
||||
this.logger.debug('responses :>> ', responses);
|
||||
return true;
|
||||
}
|
||||
}
|
70
src/game/PlayerInteractionConsole.ts
Normal file
70
src/game/PlayerInteractionConsole.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { Board } from "./entities/Board";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
import { askQuestion, printError, printSelection, printTiles, wait } from "../common/utilities";
|
||||
import { PlayerMove } from "./entities/PlayerMove";
|
||||
import { Tile } from "./entities/Tile";
|
||||
import { PlayerMoveSide, PlayerMoveSideType } from "./constants";
|
||||
import { PlayerInteractionInterface } from "./PlayerInteractionInterface";
|
||||
|
||||
export class PlayerInteractionConsole implements PlayerInteractionInterface {
|
||||
player: PlayerInterface;
|
||||
|
||||
constructor(player: PlayerInterface) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
let move: PlayerMove | null = null;
|
||||
let tile: Tile;
|
||||
let side: PlayerMoveSideType | null = null;
|
||||
const { player } = this;
|
||||
|
||||
printSelection('Hand: ', player.hand);
|
||||
while (move === null) {
|
||||
const answer = await askQuestion('Enter your move (tile index side is L or R, e.g. 0L, <Enter> to pass');
|
||||
const char0 = answer.charAt(0);
|
||||
const char1 = answer.charAt(1);
|
||||
if (answer === '' || answer === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (char1 === 'L' || char1 === 'R' || char1 === 'l' || char1 === 'r') {
|
||||
side = char1 === 'L' || char1 === 'l' ? PlayerMoveSide.LEFT : PlayerMoveSide.RIGHT;
|
||||
}
|
||||
|
||||
if (char0 > '0' && char0 <= String(player.hand.length)) {
|
||||
const tileIndex = parseInt(char0) - 1;
|
||||
tile = player.hand[tileIndex];
|
||||
move = board.isValidMove(tile, side, player);
|
||||
if (move === null) {
|
||||
printError('Invalid move');
|
||||
}
|
||||
}
|
||||
}
|
||||
return move;
|
||||
}
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
const { player: { hand} } = this;
|
||||
let index: number = -1;
|
||||
while (index < 0 || index >= board.boneyard.length) {
|
||||
printTiles('Hand: ', hand);
|
||||
printSelection('Boneyard: ', board.boneyard);
|
||||
const answer = await askQuestion('Choose a tile from the boneyard');
|
||||
if (answer === '') {
|
||||
index = 0;
|
||||
continue;
|
||||
}
|
||||
if (answer < '0' || answer > '9') {
|
||||
printError('Invalid selection');
|
||||
continue;
|
||||
}
|
||||
index = parseInt(answer) - 1;
|
||||
}
|
||||
const tile = board.boneyard.splice(index, 1)[0];
|
||||
tile.revealed = true;
|
||||
hand.push(tile);
|
||||
return tile;
|
||||
}
|
||||
}
|
||||
|
11
src/game/PlayerInteractionInterface.ts
Normal file
11
src/game/PlayerInteractionInterface.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Board } from "./entities/Board";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
import { PlayerMove } from "./entities/PlayerMove";
|
||||
import { Tile } from "./entities/Tile";
|
||||
|
||||
export interface PlayerInteractionInterface {
|
||||
player: PlayerInterface;
|
||||
|
||||
makeMove(board: Board): Promise<PlayerMove | null>;
|
||||
chooseTile(board: Board): Promise<Tile>
|
||||
}
|
49
src/game/PlayerInteractionNetwork.ts
Normal file
49
src/game/PlayerInteractionNetwork.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { PlayerInteractionInterface } from './PlayerInteractionInterface';
|
||||
import { Board } from './entities/Board';
|
||||
import { PlayerInterface } from './entities/player/PlayerInterface';
|
||||
import { PlayerMove } from './entities/PlayerMove';
|
||||
import { Tile } from './entities/Tile';
|
||||
import { NetworkClientNotifier } from './NetworkClientNotifier';
|
||||
import { NetworkPlayer } from './entities/player/NetworkPlayer';
|
||||
import { PlayerMoveSide, PlayerMoveSideType } from './constants';
|
||||
import { SocketDisconnectedError } from '../common/exceptions/SocketDisconnectedError';
|
||||
|
||||
export class PlayerInteractionNetwork implements PlayerInteractionInterface {
|
||||
player: PlayerInterface;
|
||||
clientNotifier = new NetworkClientNotifier();
|
||||
|
||||
constructor(player: PlayerInterface) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
let response = undefined;
|
||||
try {
|
||||
response = await this.clientNotifier.notifyPlayer(this.player as NetworkPlayer, 'makeMove', {
|
||||
freeHands: board.getFreeEnds(),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SocketDisconnectedError();
|
||||
}
|
||||
const { tile: tilePlayed, type, direction } = response;
|
||||
if (type === 'pass') {
|
||||
return null;
|
||||
}
|
||||
const { player: { hand} } = this;
|
||||
const index: number = hand.findIndex(t => t.id === tilePlayed.id);
|
||||
const side: PlayerMoveSideType = type === 'left' ? PlayerMoveSide.LEFT : PlayerMoveSide.RIGHT;
|
||||
const tile = hand.splice(index, 1)[0];
|
||||
tile.revealed = true;
|
||||
return board.isValidMove(tile, side, this.player, direction);
|
||||
}
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
const { player: { hand} } = this;
|
||||
const response: any = await this.clientNotifier.notifyPlayer(this.player as NetworkPlayer, 'chooseTile');
|
||||
const index: number = board.boneyard.findIndex(t => t.id === response.tileId);
|
||||
const tile = board.boneyard.splice(index, 1)[0];
|
||||
tile.revealed = true;
|
||||
hand.push(tile);
|
||||
return tile;
|
||||
}
|
||||
}
|
39
src/game/PlayerNotificationManager.ts
Normal file
39
src/game/PlayerNotificationManager.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { DominoesGame } from "./DominoesGame";
|
||||
import { GameSession } from "./GameSession";
|
||||
import { GameState } from "./dto/GameState";
|
||||
|
||||
export class PlayerNotificationManager {
|
||||
game!: DominoesGame;
|
||||
session!: GameSession;
|
||||
|
||||
constructor(game: DominoesGame | GameSession) {
|
||||
if (game instanceof GameSession) {
|
||||
this.session = game;
|
||||
} else {
|
||||
this.game = game;
|
||||
}
|
||||
}
|
||||
|
||||
async notifyGameState() {
|
||||
if(!this.game) throw new Error('Game not initialized');
|
||||
const gameState: GameState = this.game.getGameState();
|
||||
const { players } = this.game;
|
||||
let promises: Promise<void>[] = players.map(player => player.notifyGameState(gameState));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
async notifyPlayersState() {
|
||||
if(!this.game) throw new Error('Game not initialized');
|
||||
const { players } = this.game;
|
||||
let promises: Promise<void>[] = players.map(player => player.notifyPlayerState(player.getState()));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
async notifySessionState() {
|
||||
if(!this.session) throw new Error('Session not initialized');
|
||||
const { players } = this.session;
|
||||
let promises: Promise<void>[] = players.map(player => player.notifySessionState(this.session.getState()));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
}
|
15
src/game/SimulatedBoard.ts
Normal file
15
src/game/SimulatedBoard.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { PRNG } from "seedrandom";
|
||||
import { Board } from "./entities/Board";
|
||||
import { Tile } from "./entities/Tile";
|
||||
|
||||
export class SimulatedBoard extends Board {
|
||||
constructor(tiles: Tile[] = [], rng: PRNG) {
|
||||
super(rng);
|
||||
this.tiles = tiles;
|
||||
}
|
||||
|
||||
evaluate(): number {
|
||||
return this.tiles.length;
|
||||
//return this.tiles.reduce((acc, tile) => acc + tile.count, 0);
|
||||
}
|
||||
}
|
15
src/game/constants.ts
Normal file
15
src/game/constants.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export type PlayerType = 'AI' | 'Human';
|
||||
export type PlayerMoveSideType = 'left' | 'right' | 'both';
|
||||
export type JointValueType = 0 | 1 | 2;
|
||||
|
||||
export const PlayerMoveSide: { [key: string]: PlayerMoveSideType } = {
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right',
|
||||
BOTH: 'both'
|
||||
};
|
||||
|
||||
export const JointValue: { [key: string]: JointValueType } = {
|
||||
LEFT: 0,
|
||||
RIGHT: 1,
|
||||
NONE: 2
|
||||
};
|
0
src/game/dto/Game.ts
Normal file
0
src/game/dto/Game.ts
Normal file
17
src/game/dto/GameSessionState.ts
Normal file
17
src/game/dto/GameSessionState.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { PlayerDto } from "./PlayerDto";
|
||||
|
||||
export interface GameSessionState {
|
||||
id: string;
|
||||
name: string;
|
||||
creator: string;
|
||||
players: PlayerDto[];
|
||||
seed: string;
|
||||
waitingForPlayers: boolean;
|
||||
mode: string;
|
||||
pointsToWin: number;
|
||||
sessionInProgress: boolean;
|
||||
status: string;
|
||||
maxPlayers: number;
|
||||
numPlayers: number;
|
||||
waitingSeconds: number;
|
||||
}
|
18
src/game/dto/GameState.ts
Normal file
18
src/game/dto/GameState.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { PlayerMove } from "../entities/PlayerMove";
|
||||
import { PlayerDto } from "./PlayerDto";
|
||||
|
||||
export interface GameState {
|
||||
id: string;
|
||||
players: PlayerDto[];
|
||||
boneyard: any[];
|
||||
currentPlayer: PlayerDto | null;
|
||||
board: any[];
|
||||
gameInProgress: boolean;
|
||||
winner?: any;
|
||||
gameBlocked: boolean;
|
||||
gameTied: boolean;
|
||||
gameId: string;
|
||||
tileSelectionPhase: boolean;
|
||||
boardFreeEnds: number[];
|
||||
lastMove: PlayerMove | null;
|
||||
}
|
8
src/game/dto/GameSummary.ts
Normal file
8
src/game/dto/GameSummary.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { PlayerInterface } from "../entities/player/PlayerInterface";
|
||||
|
||||
export interface GameSummary {
|
||||
gameId: string;
|
||||
isBlocked: boolean;
|
||||
isTied: boolean;
|
||||
winner: PlayerInterface | null;
|
||||
}
|
6
src/game/dto/PlayerDto.ts
Normal file
6
src/game/dto/PlayerDto.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface PlayerDto {
|
||||
id: string;
|
||||
name: string;
|
||||
score?: number;
|
||||
hand?: string[];
|
||||
}
|
7
src/game/dto/PlayerState.ts
Normal file
7
src/game/dto/PlayerState.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface PlayerState {
|
||||
id: string;
|
||||
name: string;
|
||||
score: number;
|
||||
hand: any[];
|
||||
teamedWith: string | undefined;
|
||||
}
|
125
src/game/entities/Board.ts
Normal file
125
src/game/entities/Board.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { PRNG } from "seedrandom";
|
||||
import { PlayerMoveSideType, PlayerMoveSide, JointValue } from "../constants";
|
||||
import { PlayerInterface } from "./player/PlayerInterface";
|
||||
import { PlayerMove } from "./PlayerMove";
|
||||
import { Tile } from "./Tile";
|
||||
|
||||
export class Board {
|
||||
tiles: Tile[] = [];
|
||||
boneyard: Tile[] = [];
|
||||
|
||||
constructor(private rng: PRNG) {}
|
||||
|
||||
get isGameOver(): boolean {
|
||||
return this.tiles.length === 0;
|
||||
}
|
||||
|
||||
get playedPipsCount() {
|
||||
return this.tiles.reduce((acc, tile) => acc + tile.count, 0);
|
||||
}
|
||||
|
||||
get count () {
|
||||
return this.tiles.length;
|
||||
}
|
||||
|
||||
get leftEnd() {
|
||||
return this.tiles[0];
|
||||
}
|
||||
|
||||
get rightEnd() {
|
||||
return this.tiles[this.tiles.length - 1];
|
||||
}
|
||||
|
||||
get leftFreeEnd() {
|
||||
return this.leftEnd?.flippedPips[0];
|
||||
}
|
||||
|
||||
get rightFreeEnd() {
|
||||
return this.rightEnd?.flippedPips[1];
|
||||
}
|
||||
|
||||
getFreeEnds() {
|
||||
if(this.count === 0) {
|
||||
return [];
|
||||
}
|
||||
return [this.leftEnd.flippedPips[0], this.rightEnd.flippedPips[1]];
|
||||
}
|
||||
|
||||
play(playerMove: PlayerMove): void {
|
||||
const { type, tile } = playerMove;
|
||||
tile.revealed = true;
|
||||
if (type === PlayerMoveSide.LEFT) {
|
||||
this.playTileLeft(tile);
|
||||
// printLine(`${tile} -- left`);
|
||||
} else {
|
||||
this.playTileRight(tile);
|
||||
// printLine(`${tile} -- right`);
|
||||
}
|
||||
}
|
||||
|
||||
playTileLeft(tile: Tile) {
|
||||
if (tile.flippedPips[1] !== this.leftFreeEnd) {
|
||||
tile.flip();
|
||||
}
|
||||
this.tiles.unshift(tile);
|
||||
}
|
||||
|
||||
playTileRight(tile: Tile) {
|
||||
if (tile.flippedPips[0] !== this.rightFreeEnd) {
|
||||
tile.flip();
|
||||
}
|
||||
this.tiles.push(tile);
|
||||
}
|
||||
|
||||
matchesFreeEnd(tile: Tile, freeEnd: number): boolean {
|
||||
return tile.pips[0] === freeEnd || tile.pips[1] === freeEnd;
|
||||
}
|
||||
|
||||
isValidMove(tile: Tile, side: PlayerMoveSideType | null, player: PlayerInterface, direction?: string): PlayerMove | null {
|
||||
if (this.count === 0) {
|
||||
return new PlayerMove(tile, PlayerMoveSide.BOTH, player.id, direction);
|
||||
}
|
||||
|
||||
const freeEnds = this.getFreeEnds();
|
||||
const leftEnd = freeEnds[0];
|
||||
const rightEnd = freeEnds[1];
|
||||
|
||||
if (side !== null) {
|
||||
if (side === PlayerMoveSide.LEFT) {
|
||||
if (this.matchesFreeEnd(tile, leftEnd)) {
|
||||
return new PlayerMove(tile, side, player.id, direction);
|
||||
}
|
||||
} else {
|
||||
if (this.matchesFreeEnd(tile, rightEnd)) {
|
||||
return new PlayerMove(tile, side, player.id, direction);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((this.matchesFreeEnd(tile, leftEnd) && this.matchesFreeEnd(tile, rightEnd))) {
|
||||
const side = this.rng() < 0.5 ? PlayerMoveSide.LEFT : PlayerMoveSide.RIGHT;
|
||||
return new PlayerMove(tile, side, player.id, direction);
|
||||
} else if (this.matchesFreeEnd(tile, leftEnd)) {
|
||||
return new PlayerMove(tile, PlayerMoveSide.LEFT, player.id, direction);
|
||||
} else if (this.matchesFreeEnd(tile, rightEnd)) {
|
||||
return new PlayerMove(tile, PlayerMoveSide.RIGHT, player.id, direction);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getValidMoves(player: PlayerInterface): PlayerMove[] {
|
||||
|
||||
return player.hand.reduce((acc, tile) => {
|
||||
const validMove = this.isValidMove(tile, null, player);
|
||||
if (validMove !== null) {
|
||||
acc.push(validMove);
|
||||
}
|
||||
return acc;4
|
||||
}, [] as PlayerMove[]);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.tiles.map(tile => tile.toString()).join(' ');
|
||||
}
|
||||
}
|
12
src/game/entities/PlayerMove.ts
Normal file
12
src/game/entities/PlayerMove.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { uuid } from "../../common/utilities";
|
||||
import { PlayerMoveSideType } from "../constants";
|
||||
import { Tile } from "./Tile";
|
||||
|
||||
export class PlayerMove {
|
||||
id: string = uuid();
|
||||
constructor(public tile: Tile, public type: PlayerMoveSideType | null, public playerId: string, direction?: string) {}
|
||||
|
||||
toString() {
|
||||
return `PlayerMove:([${this.tile.pips[0]}|${this.tile.pips[1]}] ${this.type})`;
|
||||
}
|
||||
}
|
37
src/game/entities/Tile.ts
Normal file
37
src/game/entities/Tile.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { uuid } from "../../common/utilities";
|
||||
|
||||
export class Tile {
|
||||
id: string;
|
||||
pips: [number, number];
|
||||
revealed: boolean = true;
|
||||
flipped: boolean = false;
|
||||
|
||||
constructor(pips: [number, number]) {
|
||||
this.id = uuid();
|
||||
this.pips = pips;
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this.pips[0] + this.pips[1];
|
||||
}
|
||||
|
||||
get isPair(): boolean {
|
||||
return this.pips[0] === this.pips[1];
|
||||
}
|
||||
|
||||
get flippedPips(): [number, number] {
|
||||
return this.flipped ? [this.pips[1], this.pips[0]] : this.pips;
|
||||
}
|
||||
|
||||
flip() {
|
||||
this.flipped = !this.flipped;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (!this.revealed) {
|
||||
return '[ | ]';
|
||||
} else {
|
||||
return `[${this.flippedPips[0]}|${this.flippedPips[1]}]`;
|
||||
}
|
||||
}
|
||||
}
|
70
src/game/entities/player/AbstractPlayer.ts
Normal file
70
src/game/entities/player/AbstractPlayer.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { Board } from "../Board";
|
||||
import { PlayerInterface } from "./PlayerInterface";
|
||||
import { PlayerMove } from "../PlayerMove";
|
||||
import { Tile } from "../Tile";
|
||||
import { LoggingService } from "../../../common/LoggingService";
|
||||
import { EventEmitter } from "stream";
|
||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||
import { uuid } from "../../../common/utilities";
|
||||
import { GameState } from "../../dto/GameState";
|
||||
import { PlayerState } from "../../dto/PlayerState";
|
||||
import { GameSessionState } from "../../dto/GameSessionState";
|
||||
|
||||
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
|
||||
hand: Tile[] = [];
|
||||
score: number = 0;
|
||||
logger: LoggingService = new LoggingService();
|
||||
teamedWith: PlayerInterface | null = null;
|
||||
playerInteraction: PlayerInteractionInterface = undefined as any;
|
||||
id: string = uuid();
|
||||
|
||||
constructor(public name: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
abstract makeMove(board: Board): Promise<PlayerMove | null>;
|
||||
abstract chooseTile(board: Board): Promise<Tile>;
|
||||
|
||||
|
||||
async notifyGameState(state: GameState): Promise<void> {
|
||||
}
|
||||
|
||||
async notifyPlayerState(state: PlayerState): Promise<void> {
|
||||
}
|
||||
|
||||
async notifySessionState(state: GameSessionState): Promise<void> {
|
||||
}
|
||||
|
||||
pipsCount(): number {
|
||||
return this.hand.reduce((acc, tile) => acc + tile.pips[0] + tile.pips[1], 0);
|
||||
}
|
||||
|
||||
getHighestPair(): Tile | null {
|
||||
if (this.hand.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let highestPair: Tile | null = null;
|
||||
const pairs = this.hand.filter(tile => tile.pips[0] === tile.pips[1]);
|
||||
pairs.forEach(tile => {
|
||||
if (tile.count > (highestPair?.count ?? 0)) {
|
||||
highestPair = tile;
|
||||
}
|
||||
});
|
||||
return highestPair;
|
||||
}
|
||||
|
||||
getState(): PlayerState {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
score: this.score,
|
||||
hand: this.hand.map(tile => ({
|
||||
id: tile.id,
|
||||
pips: tile.pips,
|
||||
flipped: tile.revealed,
|
||||
})),
|
||||
teamedWith: this.teamedWith?.id,
|
||||
};
|
||||
}
|
||||
}
|
50
src/game/entities/player/NetworkPlayer.ts
Normal file
50
src/game/entities/player/NetworkPlayer.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||
import { PlayerInteractionNetwork } from "../../PlayerInteractionNetwork";
|
||||
import { PlayerHuman } from "./PlayerHuman";
|
||||
import { NetworkClientNotifier } from "../../NetworkClientNotifier";
|
||||
import { Tile } from "../Tile";
|
||||
import { Board } from "../Board";
|
||||
import { GameState } from "../../dto/GameState";
|
||||
import { PlayerState } from "../../dto/PlayerState";
|
||||
import { GameSessionState } from "../../dto/GameSessionState";
|
||||
import { SocketDisconnectedError } from "../../../common/exceptions/SocketDisconnectedError";
|
||||
|
||||
export class NetworkPlayer extends PlayerHuman {
|
||||
socketId: string;
|
||||
playerInteraction: PlayerInteractionInterface = new PlayerInteractionNetwork(this);
|
||||
clientNotifier: NetworkClientNotifier = new NetworkClientNotifier();
|
||||
|
||||
constructor(name: string, socketId: string) {
|
||||
super(name);
|
||||
this.socketId = socketId;
|
||||
}
|
||||
|
||||
async notifyGameState(state: GameState): Promise<void> {
|
||||
const response = await this.clientNotifier.notifyPlayer(this, 'gameState', state);
|
||||
console.log('game state notified :>> ', response);
|
||||
if (response === undefined || response.status !== 'ok' ) {
|
||||
throw new SocketDisconnectedError();
|
||||
}
|
||||
}
|
||||
|
||||
async notifyPlayerState(state: PlayerState): Promise<void> {
|
||||
const response = await this.clientNotifier.notifyPlayer(this, 'playerState', state);
|
||||
console.log('player state notified :>> ', response);
|
||||
if (response === undefined || response.status !== 'ok' ) {
|
||||
throw new SocketDisconnectedError();
|
||||
}
|
||||
}
|
||||
|
||||
async notifySessionState(state: GameSessionState): Promise<void> {
|
||||
const response = await this.clientNotifier.notifyPlayer(this, 'sessionState', state);
|
||||
console.log('session state notified :>> ', response);
|
||||
if (response === undefined || response.status !== 'ok' ) {
|
||||
throw new SocketDisconnectedError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
return await this.playerInteraction.chooseTile(board);
|
||||
}
|
||||
}
|
145
src/game/entities/player/PlayerAI.ts
Normal file
145
src/game/entities/player/PlayerAI.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { PlayerMoveSide } from "../../constants";
|
||||
import { printLine, wait } from "../../../common/utilities";
|
||||
import { AbstractPlayer } from "../../entities/player/AbstractPlayer";
|
||||
import { Board } from "../Board";
|
||||
import { PlayerMove } from "../PlayerMove";
|
||||
import { SimulatedBoard } from "../../SimulatedBoard";
|
||||
import { Tile } from "../Tile";
|
||||
import { PRNG } from "seedrandom";
|
||||
|
||||
export class PlayerAI extends AbstractPlayer {
|
||||
constructor(name: string, private rng: PRNG) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
await wait(500); // Simulate thinking time
|
||||
if (board.tiles.length === 0) {
|
||||
printLine('playing the first tile');
|
||||
const highestPair = this.getHighestPair();
|
||||
if (highestPair !== null) {
|
||||
return new PlayerMove(highestPair, PlayerMoveSide.BOTH, this.id);
|
||||
}
|
||||
const maxTile = this.getMaxTile();
|
||||
if (maxTile !== null) {
|
||||
return new PlayerMove(maxTile, PlayerMoveSide.BOTH, this.id);
|
||||
}
|
||||
}
|
||||
// Analyze the game state
|
||||
// Return the best move based on strategy
|
||||
return this.chooseTileGreed(board);
|
||||
}
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
const randomWait = Math.floor((Math.random() * 1000) + 500);
|
||||
await wait(randomWait); // Simulate thinking time
|
||||
const randomIndex = Math.floor(this.rng() * board.boneyard.length);
|
||||
const tile = board.boneyard.splice(randomIndex, 1)[0];
|
||||
this.hand.push(tile);
|
||||
printLine(`${this.name} has chosen a tile`);
|
||||
return tile;
|
||||
}
|
||||
|
||||
getMaxTile(): Tile | null {
|
||||
if (this.hand.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let maxTile: Tile | null = null;
|
||||
this.hand.forEach(tile => {
|
||||
if (tile.count > (maxTile?.count ?? 0)) {
|
||||
maxTile = tile;
|
||||
}
|
||||
});
|
||||
return maxTile;
|
||||
}
|
||||
|
||||
chooseTileGreed(board: Board): PlayerMove | null { // greed algorithm
|
||||
let bestMove: PlayerMove |null = null;
|
||||
let bestTileScore: number = -1;
|
||||
const validMoves: PlayerMove[] = board.getValidMoves(this);
|
||||
|
||||
validMoves.forEach(move => {
|
||||
const { tile } = move;
|
||||
const tileScore = tile.pips[0] + tile.pips[1];
|
||||
if (tileScore > bestTileScore) {
|
||||
bestMove = move;
|
||||
bestTileScore = tileScore;
|
||||
}
|
||||
});
|
||||
return bestMove;
|
||||
}
|
||||
|
||||
chooseTileRandom(board: Board): Tile | null { // random algorithm
|
||||
const validTiles: Tile[] = this.hand.filter(tile => board.isValidMove(tile, null, this));
|
||||
return validTiles[Math.floor(this.rng() * validTiles.length)];
|
||||
}
|
||||
|
||||
chooseTileMinMax(board: Board, depth: number = 3): Tile | null { // minmax algorithm
|
||||
const bestMove: Tile | null = null;
|
||||
let bestScore: number = -Infinity;
|
||||
const validMoves: PlayerMove[] = board.getValidMoves(this);
|
||||
|
||||
validMoves.forEach(move => {
|
||||
const simulatedBoard = new SimulatedBoard([ ...board.tiles ], this.rng);
|
||||
simulatedBoard.play(move);
|
||||
const score = this.minmax(simulatedBoard, depth - 1, false);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
minmax(simulatedBoard: SimulatedBoard, depth: number, isMaximizing: boolean): number {
|
||||
if (depth === 0 || simulatedBoard.isGameOver) {
|
||||
return simulatedBoard.evaluate();
|
||||
}
|
||||
|
||||
if (isMaximizing) {
|
||||
let maxEval = -Infinity;
|
||||
const validMoves = simulatedBoard.getValidMoves(this);
|
||||
validMoves.forEach((move: PlayerMove) => {
|
||||
const newSimulatedBoard = new SimulatedBoard([ ...simulatedBoard.tiles ], this.rng);
|
||||
newSimulatedBoard.play(move);
|
||||
const evaluation: number = this.minmax(newSimulatedBoard, depth - 1, false);
|
||||
maxEval = Math.max(maxEval, evaluation);
|
||||
});
|
||||
return maxEval;
|
||||
}
|
||||
else {
|
||||
let minEval = Infinity;
|
||||
const validMoves = simulatedBoard.getValidMoves(this);
|
||||
validMoves.forEach((move: PlayerMove) => {
|
||||
const newSimulatedBoard = new SimulatedBoard([ ...simulatedBoard.tiles ], this.rng);
|
||||
newSimulatedBoard.play(move);
|
||||
const evaluation: number = this.minmax(newSimulatedBoard, depth - 1, true);
|
||||
minEval = Math.min(minEval, evaluation);
|
||||
});
|
||||
return minEval;
|
||||
}
|
||||
|
||||
// if (isMaximizing) {
|
||||
// let bestScore = -Infinity;
|
||||
// boardTiles.forEach(tile => {
|
||||
// const newBoardTiles = [ ...boardTiles ];
|
||||
// newBoardTiles.push(tile);
|
||||
// const score = this.minmax(newBoardTiles, depth - 1, false);
|
||||
// bestScore = Math.max(score, bestScore);
|
||||
// });
|
||||
// return bestScore;
|
||||
// } else {
|
||||
// let bestScore = Infinity;
|
||||
// boardTiles.forEach(tile => {
|
||||
// const newBoardTiles = [ ...boardTiles ];
|
||||
// newBoardTiles.push(tile);
|
||||
// const score = this.minmax(newBoardTiles, depth - 1, true);
|
||||
// bestScore = Math.min(score, bestScore);
|
||||
// });
|
||||
// return bestScore;
|
||||
// }
|
||||
}
|
||||
|
||||
evaluateBoard(board: Board): number {
|
||||
// Custom heuristic to evaluate the board state
|
||||
return board.tiles.length; // Simplistic example
|
||||
}
|
||||
}
|
22
src/game/entities/player/PlayerHuman.ts
Normal file
22
src/game/entities/player/PlayerHuman.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { PlayerMoveSide, PlayerMoveSideType } from '../../constants';
|
||||
import { AbstractPlayer } from './AbstractPlayer';
|
||||
import { Board } from '../Board';
|
||||
import { PlayerMove } from '../PlayerMove';
|
||||
import { Tile } from '../Tile';
|
||||
import { PlayerInteractionConsole } from '../../PlayerInteractionConsole';
|
||||
import { PlayerInteractionInterface } from '../../PlayerInteractionInterface';
|
||||
|
||||
export class PlayerHuman extends AbstractPlayer {
|
||||
playerInteraction: PlayerInteractionInterface = new PlayerInteractionConsole(this);
|
||||
constructor(name: string) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
return await this.playerInteraction.makeMove(board);
|
||||
}
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
return this.playerInteraction.chooseTile(board);
|
||||
}
|
||||
}
|
24
src/game/entities/player/PlayerInterface.ts
Normal file
24
src/game/entities/player/PlayerInterface.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||
import { Board } from "../Board";
|
||||
import { GameState } from "../../dto/GameState";
|
||||
import { PlayerMove } from "../PlayerMove";
|
||||
import { PlayerState } from "../../dto/PlayerState";
|
||||
import { Tile } from "../Tile";
|
||||
import { GameSessionState } from "../../dto/GameSessionState";
|
||||
|
||||
export interface PlayerInterface {
|
||||
id: string;
|
||||
name: string;
|
||||
score: number;
|
||||
hand: Tile[];
|
||||
teamedWith: PlayerInterface | null;
|
||||
playerInteraction: PlayerInteractionInterface;
|
||||
|
||||
makeMove(gameState: Board): Promise<PlayerMove | null>;
|
||||
chooseTile(board: Board): Promise<Tile>;
|
||||
pipsCount(): number;
|
||||
notifyGameState(state: GameState): Promise<void>;
|
||||
notifyPlayerState(state: PlayerState): Promise<void>;
|
||||
notifySessionState(state: GameSessionState): Promise<void>;
|
||||
getState(): PlayerState;
|
||||
}
|
Reference in New Issue
Block a user