initial commit
This commit is contained in:
commit
733ac3891f
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
34
.hmrc
Normal file
34
.hmrc
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"path": "G:\\Other\\Development\\Projects\\[ideas]\\domino",
|
||||||
|
"name": "domino",
|
||||||
|
"initialVersion": "1.0.0",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"docker": {
|
||||||
|
"repository": "arhuako/domino"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "github",
|
||||||
|
"user": "jmconde",
|
||||||
|
"name": "domino",
|
||||||
|
"manage": true,
|
||||||
|
"createOnInit": true
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"create": true,
|
||||||
|
"managed": true,
|
||||||
|
"createHTML": true,
|
||||||
|
"htmlPath": "public"
|
||||||
|
},
|
||||||
|
"_backupInitial": {
|
||||||
|
"name": "domino",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
|
}
|
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
Initial commit
|
1638
package-lock.json
generated
Normal file
1638
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "domino",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.6.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "node --env-file=.env --watch -r ts-node/register src/server/index.ts",
|
||||||
|
"test": "node --env-file=.env -r ts-node/register src/test.ts",
|
||||||
|
"test:watch": "node --env-file=.env --watch -r ts-node/register src/test.ts",
|
||||||
|
"docker-build": "docker build -t arhuako/domino:latest .",
|
||||||
|
"docker-tag": "docker tag arhuako/domino:latest arhuako/domino:1.0.0",
|
||||||
|
"docker-push": "docker push arhuako/domino:latest && docker push arhuako/domino:1.0.0",
|
||||||
|
"publish": "npm run docker-build && npm run docker-tag && npm run docker-push"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "arhuako",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"reposityory": "github:jmconde/domino",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"pino": "^9.2.0",
|
||||||
|
"pino-pretty": "^11.2.1",
|
||||||
|
"seedrandom": "^3.0.5",
|
||||||
|
"socket.io": "^4.7.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^20.14.8",
|
||||||
|
"@types/seedrandom": "^3.0.8",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.5.2"
|
||||||
|
}
|
||||||
|
}
|
22
public/index.html
Normal file
22
public/index.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Domino Tiles</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="domino" data-top="1" data-bottom="5">
|
||||||
|
<div class="half top"></div>
|
||||||
|
<div class="half bottom"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="domino" data-top="1" data-bottom="5">
|
||||||
|
<div class="half top"></div>
|
||||||
|
<div class="half bottom"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
35
public/script.js
Normal file
35
public/script.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
document.querySelectorAll('.domino').forEach(domino => {
|
||||||
|
const topValue = domino.getAttribute('data-top');
|
||||||
|
const bottomValue = domino.getAttribute('data-bottom');
|
||||||
|
|
||||||
|
addPips(domino.querySelector('.top'), topValue);
|
||||||
|
addPips(domino.querySelector('.bottom'), bottomValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
function addPips(half, value) {
|
||||||
|
const pipPositions = getPipPositions(value);
|
||||||
|
pipPositions.forEach(pos => {
|
||||||
|
const pip = document.createElement('div');
|
||||||
|
pip.className = 'pip';
|
||||||
|
pip.style.left = pos[0];
|
||||||
|
pip.style.top = pos[1];
|
||||||
|
half.appendChild(pip);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPipPositions(value) {
|
||||||
|
const p1 = '17%';
|
||||||
|
const p2 = '42%';
|
||||||
|
const p3 = '67%';
|
||||||
|
|
||||||
|
const positions = {
|
||||||
|
0: [],
|
||||||
|
1: [[p2, p2]],
|
||||||
|
2: [[p1, p1], [p3, p3]],
|
||||||
|
3: [[p1, p1], [p2, p2], [p3, p3]],
|
||||||
|
4: [[p1, p1], [p1, p3], [p3, p1], [p3, p3]],
|
||||||
|
5: [[p1, p1], [p1, p3], [p2, p2], [p3, p1], [p3, p3]],
|
||||||
|
6: [[p1, p1], [p1, p3], [p2, p1], [p2, p3], [p3, p1], [p3, p3]],
|
||||||
|
};
|
||||||
|
return positions[value];
|
||||||
|
}
|
77
public/styles.css
Normal file
77
public/styles.css
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f0f0f0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domino {
|
||||||
|
width: 100px;
|
||||||
|
height: 200px;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.half {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
background: black;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip[data-number="1"] {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip[data-number="2"] {
|
||||||
|
left: 25%;
|
||||||
|
top: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip[data-number="3"] {
|
||||||
|
right: 25%;
|
||||||
|
top: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip[data-number="4"] {
|
||||||
|
left: 25%;
|
||||||
|
bottom: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip[data-number="5"] {
|
||||||
|
right: 25%;
|
||||||
|
bottom: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip[data-number="6"] {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip[data-number="7"] {
|
||||||
|
right: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(50%, -50%);
|
||||||
|
}
|
102
src/common/LoggingService.ts
Normal file
102
src/common/LoggingService.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import pino, { BaseLogger } from 'pino';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export class LoggingService {
|
||||||
|
static instance: LoggingService;
|
||||||
|
logsPath: string = path.join(process.cwd(), 'app', 'server', 'logs');
|
||||||
|
logger!: BaseLogger;
|
||||||
|
level: string = process.env.LOG_LEVEL || 'info';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ogger.fatal('fatal');
|
||||||
|
logger.error('error');
|
||||||
|
logger.warn('warn');
|
||||||
|
logger.info('info');
|
||||||
|
logger.debug('debug');
|
||||||
|
logger.trace('trace');
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
if ((!LoggingService.instance)) {
|
||||||
|
LoggingService.instance = this;
|
||||||
|
this.logger = pino({
|
||||||
|
level: this.level,
|
||||||
|
timestamp: pino.stdTimeFunctions.isoTime,
|
||||||
|
}, this.transports);
|
||||||
|
}
|
||||||
|
return LoggingService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
get commonRorationOptions() : any {
|
||||||
|
return {
|
||||||
|
interval: '1d',
|
||||||
|
maxFiles: 10,
|
||||||
|
path: this.logsPath,
|
||||||
|
size: '10M',
|
||||||
|
maxSize: '100M',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get transports() {
|
||||||
|
return pino.transport({
|
||||||
|
targets: [
|
||||||
|
// {
|
||||||
|
// target: 'pino-rotating-file-stream',
|
||||||
|
// level: this.level,
|
||||||
|
// options: {
|
||||||
|
// filename: 'app.log',
|
||||||
|
// ...this.commonRorationOptions
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
target: 'pino-pretty',
|
||||||
|
level: this.level,
|
||||||
|
options: {
|
||||||
|
sync: true,
|
||||||
|
colorized: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: string, data?: any) {
|
||||||
|
this.logger.debug(this._getMessageWidthObject(message, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, data?: any) {
|
||||||
|
this.logger.info(this._getMessageWidthObject(message, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, data?: any) {
|
||||||
|
this.logger.warn(this._getMessageWidthObject(message, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
error(error: any, message?: string) {
|
||||||
|
this.logger.error(error, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal(message: string, data?: any) {
|
||||||
|
this.logger.fatal(this._getMessageWidthObject(message, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
trace(message: string, data?: any) {
|
||||||
|
this.logger.trace(this._getMessageWidthObject(message, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
object(message: any) {
|
||||||
|
this.logger.info(JSON.stringify(message, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMessageWidthObject(message: string, data?: any) {
|
||||||
|
if (!data) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return `${message}\n${this._getStringObject(data)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getStringObject(data: any) {
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
src/common/exceptions/ErrorBase.ts
Normal file
8
src/common/exceptions/ErrorBase.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export class ErrorBase extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
console.log('this.constructor.name :>> ', this.constructor.name);
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
this.stack = (new Error()).stack;
|
||||||
|
}
|
||||||
|
}
|
7
src/common/exceptions/SocketDisconnectedError.ts
Normal file
7
src/common/exceptions/SocketDisconnectedError.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ErrorBase } from "./ErrorBase";
|
||||||
|
|
||||||
|
export class SocketDisconnectedError extends ErrorBase {
|
||||||
|
constructor() {
|
||||||
|
super('Socket disconnected');
|
||||||
|
}
|
||||||
|
}
|
63
src/common/utilities.ts
Normal file
63
src/common/utilities.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
|
||||||
|
import { randomBytes, randomUUID } from 'crypto';
|
||||||
|
import * as readline from 'readline';
|
||||||
|
import { Tile } from '../game/entities/Tile';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { Board } from '../game/entities/Board';
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function wait(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function askQuestion(question: string): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// console.log(chalk.yellow(question));
|
||||||
|
rl.question(`${chalk.yellow(question + ' > ')}`, (answer) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomSeed(): string {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomPart = Math.random().toString(36).substring(2);
|
||||||
|
const securePart = randomBytes(4).toString('hex');
|
||||||
|
return `${timestamp}-${randomPart}-${securePart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printTiles(prefix:string, tiles: Tile[]): void {
|
||||||
|
console.log(`${prefix}${tiles.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printSelection(prefix:string, tiles: Tile[]): void {
|
||||||
|
const line: string = tiles.map((t,i) => {
|
||||||
|
const index = i + 1;
|
||||||
|
return `(${index > 9 ? `${index})`: `${index}) `} `
|
||||||
|
}).join(' ');
|
||||||
|
printTiles(prefix, tiles);
|
||||||
|
console.log(`${Array(prefix.length).join((' '))} ${line}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printBoard(board: Board, highlighted: boolean = false): void {
|
||||||
|
if (highlighted)
|
||||||
|
console.log(chalk.cyan(`Board: ${board.tiles.length > 0 ? board.tiles.join(' ') : '--empty--'}`));
|
||||||
|
else
|
||||||
|
console.log(chalk.gray(`Board: ${board.tiles.length > 0 ? board.tiles.join(' ') : '--empty--'}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printLine(msg: string): void {
|
||||||
|
console.log(chalk.grey(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printError(msg: string): void {
|
||||||
|
console.log(chalk.red(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uuid() {
|
||||||
|
return randomUUID();
|
||||||
|
}
|
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;
|
||||||
|
}
|
5
src/server/controllers/ControllerBase.ts
Normal file
5
src/server/controllers/ControllerBase.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { LoggingService } from "../../common/LoggingService";
|
||||||
|
|
||||||
|
export class ControllerBase {
|
||||||
|
protected logger = new LoggingService();
|
||||||
|
}
|
77
src/server/controllers/SessionController.ts
Normal file
77
src/server/controllers/SessionController.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { LoggingService } from "../../common/LoggingService";
|
||||||
|
import { GameSession } from "../../game/GameSession";
|
||||||
|
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
|
||||||
|
|
||||||
|
import { ControllerBase } from "./ControllerBase";
|
||||||
|
|
||||||
|
export class SessionController extends ControllerBase{
|
||||||
|
private static sessions: any = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.logger.info('SessionController created');
|
||||||
|
}
|
||||||
|
|
||||||
|
createSession(data: any, socketId: string): any {
|
||||||
|
const { user, sessionName } = data;
|
||||||
|
const player = new NetworkPlayer(user, socketId);
|
||||||
|
const session = new GameSession(player, sessionName);
|
||||||
|
SessionController.sessions[session.id] = session;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
sessionId: session.id,
|
||||||
|
playerId: player.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
joinSession(data: any, socketId: string): any {
|
||||||
|
this.logger.debug('joinSession data :>> ')
|
||||||
|
this.logger.object(data);
|
||||||
|
const { user, sessionId } = data;
|
||||||
|
const session = SessionController.sessions[sessionId];
|
||||||
|
const player = new NetworkPlayer(user, socketId);
|
||||||
|
session.addPlayer(player);
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
sessionId: session.id,
|
||||||
|
playerId: player.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
startSession(data: any): any {
|
||||||
|
const sessionId: string = data.sessionId;
|
||||||
|
const seed: string | undefined = data.seed;
|
||||||
|
const session = SessionController.sessions[sessionId];
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return ({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Session not found'
|
||||||
|
});
|
||||||
|
} else if (session.gameInProgress) {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Game already in progress'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const missingHumans = session.maxPlayers - session.numPlayers;
|
||||||
|
for (let i = 0; i < missingHumans; i++) {
|
||||||
|
session.addPlayer(session.createPlayerAI(i));
|
||||||
|
}
|
||||||
|
session.start(seed);
|
||||||
|
return {
|
||||||
|
status: 'ok'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getSession(id: string) {
|
||||||
|
return SessionController.sessions[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSession(id: string) {
|
||||||
|
delete SessionController.sessions[id];
|
||||||
|
}
|
||||||
|
}
|
89
src/server/index.html
Normal file
89
src/server/index.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
<title>Socket.IO chat</title>
|
||||||
|
<style>
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
#response {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ul id="messages"></ul>
|
||||||
|
<form id="form" action="">
|
||||||
|
<p>
|
||||||
|
<select id="event" autocomplete="off">
|
||||||
|
<option value="">Select event</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<!-- <p><input id="room" autocomplete="off" /></p> -->
|
||||||
|
<p><textarea id="message" autocomplete="off" placeholder="Data"></textarea></p>
|
||||||
|
<p><button>Send</button></p>
|
||||||
|
<p><textarea id="response" autocomplete="off" placeholder="Response"></textarea></p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
<script>
|
||||||
|
const socket = io();
|
||||||
|
|
||||||
|
const form = document.getElementById("form");
|
||||||
|
const event = document.getElementById("event");
|
||||||
|
// const room = document.getElementById("room");
|
||||||
|
const message = document.getElementById("message");
|
||||||
|
const responseEl = document.getElementById("response");
|
||||||
|
const messages = document.getElementById("messages");
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ value: "createSession", default: '{"user": "arhuako"}' },
|
||||||
|
{ value: "startSession", default: '{"sessionId": "arhuako"}' },
|
||||||
|
{ value: "joinSession", default: '{"user": "pepe", "sessionId": "arhuako"}' },
|
||||||
|
{ value: "leaveSession", default: '{"user": "pepe", "sessionId": "arhuako"}' },
|
||||||
|
{ value: "chat message", default: "chat message" }
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
//`<option value="${option.value}">${option.value}</option>`
|
||||||
|
options.forEach((option) => {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = option.value;
|
||||||
|
opt.textContent = option.value;
|
||||||
|
event.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.addEventListener("change", (e) => {
|
||||||
|
const option = options.find((option) => option.value === e.target.value);
|
||||||
|
message.value = option.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMessage = (msg) => {
|
||||||
|
if (msg.startsWith("{") && msg.endsWith("}")) return JSON.parse(msg);
|
||||||
|
return msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (event.value.trim() && message.value.trim()) {
|
||||||
|
const response = await socket.emitWithAck(event.value.trim(), getMessage(message.value.trim()));
|
||||||
|
console.log('response :>> ', response);
|
||||||
|
message.value = "";
|
||||||
|
const responseStr = JSON.stringify(response, null, 2);
|
||||||
|
responseEl.value = !responseEl.value ? responseStr : responseEl.value + '\n---\n ' + responseStr;
|
||||||
|
event.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.onAny((eventName, msg) => {
|
||||||
|
const item = document.createElement("li");
|
||||||
|
item.textContent = `${eventName}: ${msg}`;
|
||||||
|
messages.appendChild(item);
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
25
src/server/index.ts
Normal file
25
src/server/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import http from 'http';
|
||||||
|
import cors from 'cors';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { NetworkClientNotifier } from '../game/NetworkClientNotifier';
|
||||||
|
import { SocketIoService } from './services/SocketIoService';
|
||||||
|
|
||||||
|
const clientNotifier = new NetworkClientNotifier();
|
||||||
|
const app = express();
|
||||||
|
const httpServer = http.createServer(app);
|
||||||
|
const socketIoService = new SocketIoService(httpServer);
|
||||||
|
clientNotifier.setSocket(socketIoService.getServer());
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
console.log('__dirname :>> ', __dirname);
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(join(__dirname, 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.listen(PORT, () => {
|
||||||
|
console.log(`listening on *:${PORT}`);
|
||||||
|
});
|
5
src/server/services/ServiceBase.ts
Normal file
5
src/server/services/ServiceBase.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { LoggingService } from "../../common/LoggingService";
|
||||||
|
|
||||||
|
export class ServiceBase {
|
||||||
|
protected logger = new LoggingService();
|
||||||
|
}
|
75
src/server/services/SocketIoService.ts
Normal file
75
src/server/services/SocketIoService.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { Server as HttpServer } from "http";
|
||||||
|
import { ServiceBase } from "./ServiceBase";
|
||||||
|
import { Server } from "socket.io";
|
||||||
|
import { SessionController } from "../controllers/SessionController";
|
||||||
|
|
||||||
|
export class SocketIoService extends ServiceBase{
|
||||||
|
io: Server
|
||||||
|
constructor(private httpServer: HttpServer) {
|
||||||
|
super()
|
||||||
|
this.io = this.socketIo(httpServer);
|
||||||
|
this.initListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getServer(): Server {
|
||||||
|
return this.io;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initListeners() {
|
||||||
|
const sessionController = new SessionController();
|
||||||
|
this.io.on('connection', (socket) => {
|
||||||
|
console.log(`connect ${socket.id}`);
|
||||||
|
if (socket.recovered) {
|
||||||
|
// recovery was successful: socket.id, socket.rooms and socket.data were restored
|
||||||
|
console.log("recovered!");
|
||||||
|
console.log("socket.rooms:", socket.rooms);
|
||||||
|
console.log("socket.data:", socket.data);
|
||||||
|
} else {
|
||||||
|
console.log("new connection");
|
||||||
|
socket.join('room-general')
|
||||||
|
socket.data.foo = "bar";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('user disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('createSession', (data, callback) => {
|
||||||
|
const response = sessionController.createSession(data, socket.id);
|
||||||
|
callback(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('startSession', (data, callback) => {
|
||||||
|
const response = sessionController.startSession(data);
|
||||||
|
callback(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('joinSession', (data, callback) => {
|
||||||
|
const response = sessionController.joinSession(data, socket.id);
|
||||||
|
callback(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
// socket.on('chat message', (msg, callback) => {
|
||||||
|
// io.emit('chat message', msg);
|
||||||
|
// callback({
|
||||||
|
// status: 'ok',
|
||||||
|
// message: 'Message received',
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private socketIo(httpServer: HttpServer): Server {
|
||||||
|
return new Server(httpServer, {
|
||||||
|
cors: {
|
||||||
|
origin: '*',
|
||||||
|
},
|
||||||
|
connectionStateRecovery: {
|
||||||
|
maxDisconnectionDuration: 15 * 60 * 1000,
|
||||||
|
skipMiddlewares: true,
|
||||||
|
},
|
||||||
|
connectTimeout: 15 * 60 * 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
56
src/test.ts
Normal file
56
src/test.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { PlayerAI } from "./game/entities/player/PlayerAI";
|
||||||
|
import { PlayerHuman } from "./game/entities/player/PlayerHuman";
|
||||||
|
import {LoggingService} from "./common/LoggingService";
|
||||||
|
import { GameSession } from "./game/GameSession";
|
||||||
|
|
||||||
|
console.log('process.arg :>> ', process.argv);
|
||||||
|
|
||||||
|
// const game = new DominoesGame([
|
||||||
|
// new PlayerAI("1", "Player 1"),
|
||||||
|
// new PlayerAI("2", "Player 2"),
|
||||||
|
// new PlayerAI("3", "Player 3"),
|
||||||
|
// new PlayerAI("4", "Player 4"),
|
||||||
|
// ]);
|
||||||
|
// const logger = new LoggingService();
|
||||||
|
|
||||||
|
// async function wait(ms: number) {s
|
||||||
|
// return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
// }
|
||||||
|
|
||||||
|
async function playSolo(seed?: string) {
|
||||||
|
const session = new GameSession(new PlayerHuman( "Jose"), "Test Game");
|
||||||
|
console.log(`Session (${session.id}) created by: ${session.creator.name}`);
|
||||||
|
setTimeout(() => session.addPlayer(new PlayerAI("AI 2")), 1000);
|
||||||
|
setTimeout(() => session.addPlayer(new PlayerAI("AI 3")), 2000);
|
||||||
|
setTimeout(() => session.addPlayer(new PlayerAI("AI 4")), 3000);
|
||||||
|
session.start(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playHumans(seed?: string) {
|
||||||
|
const session = new GameSession(new PlayerHuman("Jose"), "Test Game");
|
||||||
|
session.addPlayer(new PlayerHuman("Pepe"));
|
||||||
|
session.addPlayer(new PlayerHuman("Juan"));
|
||||||
|
session.addPlayer(new PlayerHuman("Luis"));
|
||||||
|
session.start(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playAIs(seed?: string) {
|
||||||
|
const session = new GameSession(new PlayerAI("AI 1"), "Test Game");
|
||||||
|
session.addPlayer(new PlayerAI("AI 2"));
|
||||||
|
session.addPlayer(new PlayerAI("AI 3"));
|
||||||
|
session.addPlayer(new PlayerAI("AI 4"));
|
||||||
|
session.start(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playTeams(seed?: string) {
|
||||||
|
const session = new GameSession(new PlayerHuman("Jose"), "Test Game");
|
||||||
|
session.addPlayer(new PlayerAI("AI 1"));
|
||||||
|
session.addPlayer(new PlayerHuman("Juan"));
|
||||||
|
session.addPlayer(new PlayerAI("AI 2"));
|
||||||
|
session.start(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockedSeed = '1719236688462-ytwrwzfzoi-01aad98f';
|
||||||
|
const seed2 = '1719237652000-09vddd3hsth7-adbc1842';
|
||||||
|
|
||||||
|
playSolo('1719248315701-itmcciws3oi-e5dd2024');
|
110
tsconfig.json
Normal file
110
tsconfig.json
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user