game flow revamp

This commit is contained in:
Jose Conde
2024-07-06 20:32:41 +02:00
parent 733ac3891f
commit a974f576b3
50 changed files with 2994 additions and 268 deletions

View File

@ -25,7 +25,7 @@ export class DominoesGame {
winner: PlayerInterface | null = null;
rng: PRNG;
handSize: number = 7;
notificationManager: PlayerNotificationManager = new PlayerNotificationManager(this);
notificationManager: PlayerNotificationManager = new PlayerNotificationManager();
lastMove: PlayerMove | null = null;
constructor(public players: PlayerInterface[], seed: PRNG) {
@ -44,6 +44,14 @@ export class DominoesGame {
this.board.boneyard = this.generateTiles();
}
reset() {
this.board.reset();
this.initializeGame();
for (let player of this.players) {
player.hand = [];
}
}
generateTiles(): Tile[] {
const tiles: Tile[] = [];
for (let i = 6; i >= 0; i--) {
@ -142,18 +150,25 @@ export class DominoesGame {
this.nextPlayer();
}
resetPlayersScore() {
for (let player of this.players) {
player.score = 0;
}
}
async start(): Promise<GameSummary> {
this.resetPlayersScore();
this.gameInProgress = false;
this.tileSelectionPhase = true;
await this.notificationManager.notifyGameState();
await this.notificationManager.notifyPlayersState();
await this.notificationManager.notifyGameState(this);
await this.notificationManager.notifyPlayersState(this.players);
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();
await this.notificationManager.notifyGameState(this);
await this.notificationManager.notifyPlayersState(this.players);
} else {
await this.tilesSelection();
}
@ -164,8 +179,8 @@ export class DominoesGame {
printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`);
while (!this.gameOver) {
await this.playTurn();
await this.notificationManager.notifyGameState();
await this.notificationManager.notifyPlayersState();
await this.notificationManager.notifyGameState(this);
await this.notificationManager.notifyPlayersState(this.players);
this.gameBlocked = this.isBlocked();
this.gameOver = this.isGameOver();
}
@ -196,8 +211,8 @@ export class DominoesGame {
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();
await this.notificationManager.notifyGameState(this);
await this.notificationManager.notifyPlayersState(this.players);
if (this.board.boneyard.length === 0) {
break;
}
@ -217,16 +232,8 @@ export class DominoesGame {
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
},
players: this.players.map(player => player.getState()),
currentPlayer: currentPlayer.getState(),
board: this.board.tiles.map(tile => ({
id: tile.id,
pips: tile.pips

View File

@ -1,149 +0,0 @@
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'
};
}
}

250
src/game/MatchSession.ts Normal file
View File

@ -0,0 +1,250 @@
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 { MatchSessionState } from "./dto/MatchSessionState";
import { PlayerNotificationManager } from './PlayerNotificationManager';
import seedrandom, { PRNG } from "seedrandom";
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
import { PlayerHuman } from "./entities/player/PlayerHuman";
export class MatchSession {
private currentGame: DominoesGame | null = null;
private minHumanPlayers: number = 1;
private waitingForPlayers: boolean = true;
private waitingSeconds: number = 0;
private logger: LoggingService = new LoggingService();
private playerNotificationManager = new PlayerNotificationManager();
id: string;
matchInProgress: boolean = false;
matchWinner: PlayerInterface | null = null;
maxPlayers: number = 4;
mode: string = 'classic';
players: PlayerInterface[] = [];
pointsToWin: number = 50;
rng!: PRNG
scoreboard: Map<string, number> = new Map();
seed!: string
sessionInProgress: boolean = false;
state: string = 'created'
constructor(public creator: PlayerInterface, public name?: string) {
this.id = uuid();
this.name = name || `Game ${this.id}`;
this.addPlayer(creator);
this.creator = creator;
this.logger.info(`Match session created by: ${creator.name}`);
this.logger.info(`Match session ID: ${this.id}`);
this.logger.info(`Match session name: ${this.name}`);
this.logger.info(`Points to win: ${this.pointsToWin}`);
this.sessionInProgress = true;
this.matchInProgress = false;
this.playerNotificationManager.notifyMatchState(this);
this.playerNotificationManager.notifyPlayersState(this.players);
}
get numPlayers() {
return this.players.length;
}
get numPlayersReady() {
return this.players.filter(player => player.ready).length;
}
get numHumanPlayers() {
return this.players.filter(player => player instanceof PlayerHuman).length;
}
private async startMatch(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.state = 'ready'
this.resetScoreboard()
let gameNumber: number = 0;
this.matchInProgress = true
this.playerNotificationManager.notifyMatchState(this);
while (this.matchInProgress) {
this.currentGame = new DominoesGame(this.players, this.rng);
gameNumber += 1;
this.state = 'started'
this.logger.info(`Game #${gameNumber} started`);
// this.game.reset()
await this.currentGame.start();
this.setScores();
this.checkMatchWinner();
this.resetReadiness();
this.state = 'waiting'
await this.playerNotificationManager.notifyMatchState(this);
this.playerNotificationManager.sendEventToPlayers('game-finished', this.players);
await this.checkHumanPlayersReady();
}
this.state = 'end'
// await this.game.start();
return this.endGame();
}
async checkHumanPlayersReady() {
this.logger.info('Waiting for human players to be ready');
return new Promise((resolve) => {
const interval = setInterval(() => {
this.logger.debug(`Human players ready: ${this.numPlayersReady}/${this.numHumanPlayers}`)
if (this.numPlayersReady === this.numHumanPlayers) {
clearInterval(interval);
resolve(true);
}
}, 1000);
});
}
resetReadiness() {
this.players.forEach(player => {
player.ready = false
});
}
checkMatchWinner() {
const scores = Array.from(this.scoreboard.values());
const maxScore = Math.max(...scores);
if (maxScore >= this.pointsToWin) {
this.matchWinner = this.players.find(player => this.scoreboard.get(player.id) === maxScore)!;
this.logger.info(`Match winner: ${this.matchWinner.name} with ${maxScore} points`);
this.matchInProgress = false;
}
}
resetScoreboard() {
this.scoreboard = new Map();
this.players.forEach(player => {
this.scoreboard.set(player.id, 0);
});
}
setScores() {
const totalPips = this.currentGame?.players.reduce((acc, player) => acc + player.pipsCount(), 0);
if (this.currentGame && this.currentGame.winner !== null) {
const winner = this.currentGame.winner;
this.scoreboard.set(winner.id, this.scoreboard.get(winner.id)! + totalPips!);
if (winner.teamedWith !== null) {
this.scoreboard.set(winner.teamedWith.id, this.scoreboard.get(winner.teamedWith.id)! + totalPips!);
}
}
}
private endGame(): any {
if (this.currentGame !== null) {
const { gameBlocked, gameTied, winner } = this.currentGame;
gameBlocked ? this.logger.info('Game blocked!') : gameTied ? this.logger.info('Game tied!') : this.logger.info('Game over!');
this.logger.info('Winner: ' + winner?.name + ' with ' + winner?.pipsCount() + ' points');
this.getScore(this.currentGame);
this.logger.info('Game ended');
this.currentGame = null;
this.playerNotificationManager.notifyMatchState(this);
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()}`;
});
this.logger.info(`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}`;
});
this.logger.info(`Scores: ${scores.join(', ')}`);
}
createPlayerAI(i: number) {
const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
const player = new PlayerAI(AInames[i], this.rng);
player.ready = true;
return player;
}
async start(seed?: string) {
this.seed = seed || getRandomSeed();
console.log('seed :>> ', this.seed);
if (this.matchInProgress) {
throw new Error("Game already in progress");
}
this.waitingForPlayers = true;
this.logger.info('Waiting for players to be ready');
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');
await this.startMatch(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!`);
}
setPlayerReady(user: string) {
const player = this.players.find(player => player.name === user);
if (!player) {
throw new Error("Player not found");
}
player.ready = true;
this.logger.info(`${player.name} is ready!`);
this.playerNotificationManager.notifyMatchState(this);
}
toString() {
return `GameSession:(${this.id} ${this.name})`;
}
getState(): MatchSessionState {
return {
id: this.id,
name: this.name!,
creator: this.creator.id,
players: this.players.map(player =>( {
id: player.id,
name: player.name,
ready: player.ready,
})),
playersReady: this.numPlayersReady,
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',
scoreboard: this.scoreboard,
matchWinner: this.matchWinner?.getState() || null,
matchInProgress: this.matchInProgress
};
}
}

View File

@ -17,7 +17,7 @@ export class NetworkClientNotifier {
this.io = io;
}
async notifyPlayer(player: NetworkPlayer, event: string, data: any = {}, timeoutSecs: number = 300): Promise<any> {
async notifyPlayer(player: NetworkPlayer, event: string, data: any = {}, timeoutSecs: number = 900): Promise<any> {
try {
const response = await this.io.to(player.socketId)
.timeout(timeoutSecs * 1000)
@ -29,6 +29,10 @@ export class NetworkClientNotifier {
}
}
async sendEvent(player: NetworkPlayer, event: string, data?: any) {
this.io.to(player.socketId).emit(event, data);
}
async broadcast(event: string, data: any) {
const responses = await this.io.emit(event, data);
this.logger.debug('responses :>> ', responses);

View File

@ -1,39 +1,36 @@
import { DominoesGame } from "./DominoesGame";
import { GameSession } from "./GameSession";
import { MatchSession } from "./MatchSession";
import { GameState } from "./dto/GameState";
import { PlayerInterface } from "./entities/player/PlayerInterface";
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;
async notifyGameState(game: DominoesGame) {
const gameState: GameState = game.getGameState();
const { players } = 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;
async notifyPlayersState(players: PlayerInterface[]) {
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()));
async notifyMatchState(session: MatchSession) {
const { players } = session;
let promises: Promise<void>[] = players.map(player => player.notifyMatchState(session.getState()));
return await Promise.all(promises);
}
async waitForPlayersAction(actionId: string, data: any = {}, players: PlayerInterface[]) {
let promises: Promise<boolean>[] = players.map(player => player.waitForAction(actionId, data));
return await Promise.all(promises);
}
async sendEventToPlayers(event: string, players: PlayerInterface[]) {
let promises: Promise<void>[] = players.map(player => player.sendEvent(event));
return await Promise.all(promises);
}
}

View File

@ -1,6 +1,6 @@
import { PlayerDto } from "./PlayerDto";
export interface GameSessionState {
export interface MatchSessionState {
id: string;
name: string;
creator: string;
@ -14,4 +14,8 @@ export interface GameSessionState {
maxPlayers: number;
numPlayers: number;
waitingSeconds: number;
scoreboard: Map<string, number>;
matchWinner: PlayerDto | null;
matchInProgress: boolean;
playersReady: number
}

View File

@ -2,5 +2,7 @@ export interface PlayerDto {
id: string;
name: string;
score?: number;
hand?: string[];
hand?: any[];
teamedWith?: PlayerDto | null;
ready: boolean;
}

View File

@ -1,7 +0,0 @@
export interface PlayerState {
id: string;
name: string;
score: number;
hand: any[];
teamedWith: string | undefined;
}

View File

@ -38,6 +38,11 @@ export class Board {
return this.rightEnd?.flippedPips[1];
}
reset() {
this.tiles = [];
this.boneyard = [];
}
getFreeEnds() {
if(this.count === 0) {
return [];

View File

@ -7,8 +7,8 @@ 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";
import { MatchSessionState } from "../../dto/MatchSessionState";
import { PlayerDto } from "../../dto/PlayerDto";
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
hand: Tile[] = [];
@ -17,6 +17,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
teamedWith: PlayerInterface | null = null;
playerInteraction: PlayerInteractionInterface = undefined as any;
id: string = uuid();
ready: boolean = false;
constructor(public name: string) {
super();
@ -29,10 +30,17 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
async notifyGameState(state: GameState): Promise<void> {
}
async notifyPlayerState(state: PlayerState): Promise<void> {
async notifyPlayerState(state: PlayerDto): Promise<void> {
}
async notifySessionState(state: GameSessionState): Promise<void> {
async notifyMatchState(state: MatchSessionState): Promise<void> {
}
async waitForAction(actionId: string): Promise<boolean> {
return true;
}
async sendEvent(event: string): Promise<void> {
}
pipsCount(): number {
@ -54,17 +62,24 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
return highestPair;
}
getState(): PlayerState {
getState(showPips: boolean = false): PlayerDto {
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,
hand: this.hand.map(tile => {
const d = {
id: tile.id,
pips: tile.pips,
flipped: tile.revealed,
};
if (showPips) {
d.pips = tile.pips;
}
return d;
}),
teamedWith: this.teamedWith?.getState() ?? null,
ready: this.ready,
};
}
}

View File

@ -5,8 +5,8 @@ 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 { PlayerDto } from "../../dto/PlayerDto";
import { MatchSessionState } from "../../dto/MatchSessionState";
import { SocketDisconnectedError } from "../../../common/exceptions/SocketDisconnectedError";
export class NetworkPlayer extends PlayerHuman {
@ -27,7 +27,7 @@ export class NetworkPlayer extends PlayerHuman {
}
}
async notifyPlayerState(state: PlayerState): Promise<void> {
async notifyPlayerState(state: PlayerDto): Promise<void> {
const response = await this.clientNotifier.notifyPlayer(this, 'playerState', state);
console.log('player state notified :>> ', response);
if (response === undefined || response.status !== 'ok' ) {
@ -35,14 +35,25 @@ export class NetworkPlayer extends PlayerHuman {
}
}
async notifySessionState(state: GameSessionState): Promise<void> {
const response = await this.clientNotifier.notifyPlayer(this, 'sessionState', state);
async notifyMatchState(state: MatchSessionState): Promise<void> {
const response = await this.clientNotifier.notifyPlayer(this, 'matchState', state);
console.log('session state notified :>> ', response);
if (response === undefined || response.status !== 'ok' ) {
throw new SocketDisconnectedError();
}
}
async waitForAction(actionId: string): Promise<boolean> {
const response = await this.clientNotifier.notifyPlayer(this, actionId);
if (response === undefined || response.status !== 'ok' ) {
throw new SocketDisconnectedError();
}
const { actionResult } = response;
return actionResult;
}
async sendEvent(event: string): Promise<void> {
this.clientNotifier.sendEvent(this, event);
}
async chooseTile(board: Board): Promise<Tile> {
return await this.playerInteraction.chooseTile(board);

View File

@ -2,9 +2,9 @@ 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";
import { MatchSessionState } from "../../dto/MatchSessionState";
import { PlayerDto } from "../../dto/PlayerDto";
export interface PlayerInterface {
id: string;
@ -13,12 +13,15 @@ export interface PlayerInterface {
hand: Tile[];
teamedWith: PlayerInterface | null;
playerInteraction: PlayerInteractionInterface;
ready: boolean;
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;
notifyPlayerState(state: PlayerDto): Promise<void>;
notifyMatchState(state: MatchSessionState): Promise<void>;
waitForAction(actionId: string, data: any): Promise<boolean>;
sendEvent(event: string): Promise<void>;
getState(): PlayerDto;
}