Big refactor
This commit is contained in:
parent
51b701663d
commit
ad935c0b56
|
@ -0,0 +1,140 @@
|
||||||
|
import { WS } from '.';
|
||||||
|
import { GomokuGame, PlayerColor } from './game/game-instance';
|
||||||
|
import { handleDrawMessage, DrawMessage } from './messages/draw';
|
||||||
|
import { handleMakeMove, MakeMoveMessage } from './messages/make-move';
|
||||||
|
import { Message } from './messages/messages';
|
||||||
|
import { handleRematchMessage, RematchMessage } from './messages/rematch';
|
||||||
|
import { handleResignation, ResignationMessage } from './messages/resign';
|
||||||
|
import { handleTakebackMessage, TakebackMessage } from './messages/takeback';
|
||||||
|
import {
|
||||||
|
handleUpdateDisplayName,
|
||||||
|
UpdateDisplayNameMessage,
|
||||||
|
} from './messages/update-display-name';
|
||||||
|
import { PlayerId, PlayerConnection } from './player-connection';
|
||||||
|
import { broadcastBoard } from './view/board-renderer';
|
||||||
|
import { broadcastButtons } from './view/button-renderer';
|
||||||
|
import { broadcastTitle } from './view/title-renderer';
|
||||||
|
import { WebSocketHandler } from './web-socket-handler';
|
||||||
|
|
||||||
|
export type GameId = string;
|
||||||
|
|
||||||
|
export class GameServer {
|
||||||
|
id: GameId;
|
||||||
|
gomoku: GomokuGame;
|
||||||
|
connections: Map<PlayerId, PlayerConnection>;
|
||||||
|
blackPlayerId?: PlayerId;
|
||||||
|
whitePlayerId?: PlayerId;
|
||||||
|
takebackRequesterId: PlayerId | null = null;
|
||||||
|
drawRequesterId: PlayerId | null = null;
|
||||||
|
rematchRequesterId: PlayerId | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: GameId,
|
||||||
|
public webSocketHandler: WebSocketHandler,
|
||||||
|
) {
|
||||||
|
this.id = id;
|
||||||
|
this.gomoku = new GomokuGame();
|
||||||
|
this.connections = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleConnection(ws: WS) {
|
||||||
|
const { playerId } = ws.data.query;
|
||||||
|
if (this.connections.has(playerId)) {
|
||||||
|
const existingConn = this.connections.get(playerId)!;
|
||||||
|
existingConn.ws = ws; // Update with new WebSocket
|
||||||
|
console.log(
|
||||||
|
`Updated connection for player ${playerId} in game ${this.id}, replacing old WS.`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const playerName = this.webSocketHandler.getPlayerName(playerId); // Retrieve name or use ID
|
||||||
|
const conn = new PlayerConnection(playerId, playerName, ws);
|
||||||
|
this.connections.set(playerId, conn);
|
||||||
|
console.log(
|
||||||
|
`Created new connection with player ${conn.id} in game ${this.id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.blackPlayerId) {
|
||||||
|
this.blackPlayerId = conn.id;
|
||||||
|
} else if (!this.whitePlayerId) {
|
||||||
|
this.whitePlayerId = conn.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.whitePlayerId && this.blackPlayerId) {
|
||||||
|
this.gomoku.status = 'playing';
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastBoard(this);
|
||||||
|
broadcastButtons(this);
|
||||||
|
broadcastTitle(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleDisconnect(ws: WS) {
|
||||||
|
const { playerId } = ws.data.query;
|
||||||
|
this.connections.delete(playerId);
|
||||||
|
broadcastTitle(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleMessage(ws: WS, message: Message): void {
|
||||||
|
const conn = this.connections.get(ws.data.query.playerId);
|
||||||
|
if (!conn) {
|
||||||
|
console.error(
|
||||||
|
`Failed to handle message from player ${ws.data.query.playerId}, because they are not in the game ${this.id}, which it was routed to`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Handling ${message.type} message in game ${this.id} from player ${conn.id}: ${JSON.stringify(message)}`,
|
||||||
|
);
|
||||||
|
switch (message.type) {
|
||||||
|
case 'make_move': {
|
||||||
|
handleMakeMove(this, conn, message as MakeMoveMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'resign': {
|
||||||
|
handleResignation(this, conn, message as ResignationMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'takeback': {
|
||||||
|
handleTakebackMessage(this, conn, message as TakebackMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'draw': {
|
||||||
|
handleDrawMessage(this, conn, message as DrawMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'rematch': {
|
||||||
|
handleRematchMessage(this, conn, message as RematchMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'update_display_name': {
|
||||||
|
handleUpdateDisplayName(
|
||||||
|
this,
|
||||||
|
conn,
|
||||||
|
message as UpdateDisplayNameMessage,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPlayerColor(conn: PlayerConnection): PlayerColor | undefined {
|
||||||
|
if (this.blackPlayerId === conn.id) {
|
||||||
|
return 'black';
|
||||||
|
} else if (this.whitePlayerId === conn.id) {
|
||||||
|
return 'white';
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPlayerFromColor(color: PlayerColor) {
|
||||||
|
if (color === 'white' && this.whitePlayerId) {
|
||||||
|
return this.connections.get(this.whitePlayerId);
|
||||||
|
} else if (color === 'black' && this.blackPlayerId) {
|
||||||
|
return this.connections.get(this.blackPlayerId);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +0,0 @@
|
||||||
export type ActionType = 'request' | 'accept' | 'decline' | 'cancel';
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
type:
|
|
||||||
| 'make_move'
|
|
||||||
| 'resign'
|
|
||||||
| 'takeback'
|
|
||||||
| 'draw'
|
|
||||||
| 'rematch'
|
|
||||||
| 'redirect_to_game'
|
|
||||||
| 'update_display_name';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateDisplayNameMessage extends Message {
|
|
||||||
displayName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MakeMoveMessage extends Message {
|
|
||||||
row: number;
|
|
||||||
col: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResignationMessage extends Message {}
|
|
||||||
|
|
||||||
export interface TakebackMessage extends Message {
|
|
||||||
action: ActionType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DrawMessage extends Message {
|
|
||||||
action: ActionType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RematchMessage extends Message {
|
|
||||||
action: ActionType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RedirectToGameMessage extends Message {
|
|
||||||
gameId: string;
|
|
||||||
}
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { broadcastBoard } from '../view/board-renderer';
|
||||||
|
import { broadcastButtons } from '../view/button-renderer';
|
||||||
|
import { broadcastSound } from '../view/sound-renderer';
|
||||||
|
import { broadcastTitle } from '../view/title-renderer';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
import { ActionType, Message } from './messages';
|
||||||
|
|
||||||
|
export interface DrawMessage extends Message {
|
||||||
|
action: ActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDrawMessage(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
message: DrawMessage,
|
||||||
|
): void {
|
||||||
|
if (server.gomoku.status !== 'playing') {
|
||||||
|
conn.sendMessage(
|
||||||
|
'error',
|
||||||
|
'You can only perform this action in an active game.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (message.action) {
|
||||||
|
case 'request':
|
||||||
|
handleRequestDraw(server, conn);
|
||||||
|
break;
|
||||||
|
case 'accept':
|
||||||
|
if (!server.drawRequesterId) {
|
||||||
|
conn.sendMessage('error', 'No draw has been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleAcceptDraw(server);
|
||||||
|
break;
|
||||||
|
case 'decline':
|
||||||
|
if (!server.drawRequesterId) {
|
||||||
|
conn.sendMessage('error', 'No draw has been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleDeclineDraw(server);
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
if (server.drawRequesterId !== conn.id) {
|
||||||
|
conn.sendMessage('error', 'You are not the one who requested a draw.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleCancelDrawRequest(server);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestDraw(server: GameServer, conn: PlayerConnection): void {
|
||||||
|
if (server.takebackRequesterId) {
|
||||||
|
conn.sendMessage('error', 'A takeback has already been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.drawRequesterId = conn.id;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAcceptDraw(server: GameServer): void {
|
||||||
|
server.gomoku.declareDraw();
|
||||||
|
server.drawRequesterId = null;
|
||||||
|
broadcastBoard(server);
|
||||||
|
broadcastButtons(server);
|
||||||
|
broadcastTitle(server);
|
||||||
|
broadcastSound(server, 'draw');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeclineDraw(server: GameServer): void {
|
||||||
|
server.drawRequesterId = null;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelDrawRequest(server: GameServer): void {
|
||||||
|
server.drawRequesterId = null;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { broadcastBoard } from '../view/board-renderer';
|
||||||
|
import { broadcastButtons } from '../view/button-renderer';
|
||||||
|
import { broadcastSound, broadcastSoundToPlayer } from '../view/sound-renderer';
|
||||||
|
import { broadcastTitle } from '../view/title-renderer';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
import { Message } from './messages';
|
||||||
|
|
||||||
|
export interface MakeMoveMessage extends Message {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleMakeMove(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
message: MakeMoveMessage,
|
||||||
|
): void {
|
||||||
|
const { row, col } = message;
|
||||||
|
|
||||||
|
if (server.gomoku.status !== 'playing') {
|
||||||
|
conn.sendMessage(
|
||||||
|
'error',
|
||||||
|
'You cannot play a move while the game is not ongoing!',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerColor;
|
||||||
|
if (server.blackPlayerId === conn.id) {
|
||||||
|
playerColor = 'black';
|
||||||
|
} else if (server.whitePlayerId == conn.id) {
|
||||||
|
playerColor = 'white';
|
||||||
|
} else {
|
||||||
|
conn.sendMessage(
|
||||||
|
'error',
|
||||||
|
'You are not a player in this game, you cannot make a move!',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (server.gomoku.currentPlayerColor !== playerColor) {
|
||||||
|
conn.sendMessage('error', "It's not your turn");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.takebackRequesterId || server.drawRequesterId) {
|
||||||
|
server.takebackRequesterId = null;
|
||||||
|
server.drawRequesterId = null;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateBeforeMove = server.gomoku.status;
|
||||||
|
const result = server.gomoku.makeMove(playerColor, row, col);
|
||||||
|
if (result.success) {
|
||||||
|
broadcastBoard(server);
|
||||||
|
broadcastTitle(server);
|
||||||
|
// We only need to re-send buttons when the game state changes
|
||||||
|
if (stateBeforeMove != server.gomoku.status) {
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast sounds
|
||||||
|
if (server.gomoku.status === 'playing') {
|
||||||
|
broadcastSound(server, 'move');
|
||||||
|
} else {
|
||||||
|
const whiteConn = server.whitePlayerId
|
||||||
|
? server.connections.get(server.whitePlayerId)
|
||||||
|
: null;
|
||||||
|
const blackConn = server.blackPlayerId
|
||||||
|
? server.connections.get(server.blackPlayerId)
|
||||||
|
: null;
|
||||||
|
switch (server.gomoku.winnerColor) {
|
||||||
|
case 'draw':
|
||||||
|
broadcastSound(server, 'draw');
|
||||||
|
case 'white':
|
||||||
|
if (whiteConn) {
|
||||||
|
broadcastSoundToPlayer(whiteConn, 'victory');
|
||||||
|
}
|
||||||
|
if (blackConn) {
|
||||||
|
broadcastSoundToPlayer(blackConn, 'defeat');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'black':
|
||||||
|
if (whiteConn) {
|
||||||
|
broadcastSoundToPlayer(whiteConn, 'defeat');
|
||||||
|
}
|
||||||
|
if (blackConn) {
|
||||||
|
broadcastSoundToPlayer(blackConn, 'victory');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Move made in game ${server.id} by ${conn.id}: (${row}, ${col})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
conn.sendMessage('error', result.error!);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface Message {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
export type ActionType = 'request' | 'accept' | 'decline' | 'cancel';
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { broadcastButtons } from '../view/button-renderer';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
import { ActionType, Message } from './messages';
|
||||||
|
|
||||||
|
export interface RematchMessage extends Message {
|
||||||
|
action: ActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleRematchMessage(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
message: RematchMessage,
|
||||||
|
): void {
|
||||||
|
if (server.gomoku.status !== 'finished') {
|
||||||
|
conn.sendMessage(
|
||||||
|
'error',
|
||||||
|
'You can only perform this action in a finished game.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (message.action) {
|
||||||
|
case 'request':
|
||||||
|
handleRequestRematch(server, conn);
|
||||||
|
break;
|
||||||
|
case 'accept':
|
||||||
|
if (!server.rematchRequesterId) {
|
||||||
|
conn.sendMessage('error', 'No rematch has been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleAcceptRematch(server);
|
||||||
|
break;
|
||||||
|
case 'decline':
|
||||||
|
if (!server.rematchRequesterId) {
|
||||||
|
conn.sendMessage('error', 'No rematch has been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleDeclineRematch(server);
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
if (server.rematchRequesterId !== conn.id) {
|
||||||
|
conn.sendMessage(
|
||||||
|
'error',
|
||||||
|
'You are not the one who requested a rematch.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleCancelRematchRequest(server);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestRematch(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
): void {
|
||||||
|
server.rematchRequesterId = conn.id;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAcceptRematch(server: GameServer): void {
|
||||||
|
const newGameId = server.webSocketHandler.createGame(
|
||||||
|
undefined,
|
||||||
|
server.whitePlayerId,
|
||||||
|
server.blackPlayerId,
|
||||||
|
);
|
||||||
|
const redirectMessage = {
|
||||||
|
type: 'redirect_to_game',
|
||||||
|
gameId: newGameId,
|
||||||
|
};
|
||||||
|
server.connections.forEach((c) => {
|
||||||
|
c.ws.send(redirectMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeclineRematch(server: GameServer): void {
|
||||||
|
server.rematchRequesterId = null;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelRematchRequest(server: GameServer): void {
|
||||||
|
server.rematchRequesterId = null;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { broadcastBoard } from '../view/board-renderer';
|
||||||
|
import { broadcastButtons } from '../view/button-renderer';
|
||||||
|
import { broadcastSoundToPlayer } from '../view/sound-renderer';
|
||||||
|
import { broadcastTitle } from '../view/title-renderer';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
import { Message } from './messages';
|
||||||
|
|
||||||
|
export interface ResignationMessage extends Message {}
|
||||||
|
|
||||||
|
export function handleResignation(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
message: ResignationMessage,
|
||||||
|
): void {
|
||||||
|
console.log(
|
||||||
|
`Handling resign message in game ${server.id} from player ${conn.id}: ${{ message }}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (server.gomoku.status !== 'playing') {
|
||||||
|
conn.sendMessage('error', 'You can only resign from an active game.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resigningPlayerColor = server.getPlayerColor(conn);
|
||||||
|
if (!resigningPlayerColor) {
|
||||||
|
conn.sendMessage('error', 'You are not a player in server game.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.gomoku.resign(resigningPlayerColor);
|
||||||
|
broadcastBoard(server);
|
||||||
|
broadcastTitle(server);
|
||||||
|
broadcastButtons(server);
|
||||||
|
broadcastSoundToPlayer(conn, 'defeat');
|
||||||
|
const otherPlayerId =
|
||||||
|
resigningPlayerColor === 'white'
|
||||||
|
? server.blackPlayerId
|
||||||
|
: server.whitePlayerId;
|
||||||
|
if (otherPlayerId) {
|
||||||
|
const otherPlayer = server.connections.get(otherPlayerId);
|
||||||
|
if (otherPlayer) {
|
||||||
|
broadcastSoundToPlayer(otherPlayer, 'victory');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Player ${conn.id} resigned from game ${server.id}`);
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { broadcastBoard } from '../view/board-renderer';
|
||||||
|
import { broadcastButtons } from '../view/button-renderer';
|
||||||
|
import { broadcastTitle } from '../view/title-renderer';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
import { ActionType, Message } from './messages';
|
||||||
|
|
||||||
|
export interface TakebackMessage extends Message {
|
||||||
|
action: ActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleTakebackMessage(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
message: TakebackMessage,
|
||||||
|
): void {
|
||||||
|
if (server.gomoku.status !== 'playing') {
|
||||||
|
conn.sendMessage(
|
||||||
|
'error',
|
||||||
|
'You can only perform this action in an active game.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (message.action) {
|
||||||
|
case 'request':
|
||||||
|
handleRequestTakeback(server, conn);
|
||||||
|
break;
|
||||||
|
case 'accept':
|
||||||
|
if (!server.takebackRequesterId) {
|
||||||
|
conn.sendMessage('error', 'No takeback has been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleAcceptTakeback(server);
|
||||||
|
break;
|
||||||
|
case 'decline':
|
||||||
|
if (!server.takebackRequesterId) {
|
||||||
|
conn.sendMessage('error', 'No takeback has been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleDeclineTakeback(server);
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
if (server.takebackRequesterId !== conn.id) {
|
||||||
|
conn.sendMessage(
|
||||||
|
'error',
|
||||||
|
'You are not the one who requested a takeback.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleCancelTakebackRequest(server);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestTakeback(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
): void {
|
||||||
|
if (server.gomoku.history.length === 0) {
|
||||||
|
conn.sendMessage('error', 'There are no moves to take back.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.drawRequesterId) {
|
||||||
|
conn.sendMessage('error', 'A draw has already been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
server.takebackRequesterId = conn.id;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAcceptTakeback(server: GameServer): void {
|
||||||
|
server.gomoku.undoMove();
|
||||||
|
server.takebackRequesterId = null;
|
||||||
|
broadcastBoard(server);
|
||||||
|
broadcastButtons(server);
|
||||||
|
broadcastTitle(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeclineTakeback(server: GameServer): void {
|
||||||
|
server.takebackRequesterId = null;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelTakebackRequest(server: GameServer): void {
|
||||||
|
server.takebackRequesterId = null;
|
||||||
|
broadcastButtons(server);
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { broadcastTitle } from '../view/title-renderer';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
import { Message } from './messages';
|
||||||
|
|
||||||
|
export interface UpdateDisplayNameMessage extends Message {
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleUpdateDisplayName(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
message: UpdateDisplayNameMessage,
|
||||||
|
): void {
|
||||||
|
const newDisplayName = message.displayName.trim();
|
||||||
|
|
||||||
|
if (!newDisplayName) {
|
||||||
|
conn.sendMessage('error', 'Display name cannot be empty.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDisplayName.length > 20) {
|
||||||
|
conn.sendMessage(
|
||||||
|
'error',
|
||||||
|
'Display name cannot be longer than 20 characters.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDisplayName === conn.name) {
|
||||||
|
return; // No change, do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.name = newDisplayName;
|
||||||
|
server.webSocketHandler.setPlayerName(conn.id, newDisplayName);
|
||||||
|
broadcastTitle(server);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { WS } from '.';
|
||||||
|
|
||||||
|
export type PlayerId = string;
|
||||||
|
|
||||||
|
export class PlayerConnection {
|
||||||
|
id: PlayerId;
|
||||||
|
name: string;
|
||||||
|
ws: WS;
|
||||||
|
|
||||||
|
constructor(id: string, name: string, ws: WS) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.ws = ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMessage(severity: 'info' | 'error', message: string) {
|
||||||
|
console.log(`Sending message ${message} to player ${this.id}`);
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,26 @@
|
||||||
import { Html } from '@elysiajs/html';
|
import { Html } from '@elysiajs/html';
|
||||||
import { GomokuGame } from '../game/game-instance';
|
import { GomokuGame } from '../game/game-instance';
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
|
||||||
export function renderGameBoardHtml(
|
export function broadcastBoard(server: GameServer) {
|
||||||
|
server.connections.forEach((conn: PlayerConnection) =>
|
||||||
|
broadcastBoardToPlayer(server, conn),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastBoardToPlayer(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
) {
|
||||||
|
const isToPlay =
|
||||||
|
server.gomoku.currentPlayerColor == server.getPlayerColor(conn);
|
||||||
|
const updatedBoardHtml = renderGameBoardHtml(server.gomoku, isToPlay);
|
||||||
|
conn.ws.send(updatedBoardHtml);
|
||||||
|
console.log(`Sent board for game ${server.id} to player ${conn.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGameBoardHtml(
|
||||||
game: GomokuGame,
|
game: GomokuGame,
|
||||||
isPlayerToPlay: boolean,
|
isPlayerToPlay: boolean,
|
||||||
): string {
|
): string {
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
import { Html } from '@elysiajs/html';
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
|
||||||
|
export function broadcastButtons(server: GameServer) {
|
||||||
|
server.connections.forEach((conn: PlayerConnection) =>
|
||||||
|
broadcastButtonsToPlayer(server, conn),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastButtonsToPlayer(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
) {
|
||||||
|
const buttons: JSX.Element[] = [];
|
||||||
|
let title: string | undefined;
|
||||||
|
|
||||||
|
if (server.gomoku.status == 'playing' && server.getPlayerColor(conn)) {
|
||||||
|
if (server.takebackRequesterId) {
|
||||||
|
if (server.takebackRequesterId === conn.id) {
|
||||||
|
title = 'You requested a takeback';
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
id="cancel-takeback-request-button"
|
||||||
|
class="decline-button"
|
||||||
|
ws-send="click"
|
||||||
|
>
|
||||||
|
<svg class="icon" alt="Cancel">
|
||||||
|
<use href="/icons/decline.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
title = 'Your opponent requests a takeback';
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
id="accept-takeback-button"
|
||||||
|
class="accept-button"
|
||||||
|
ws-send="click"
|
||||||
|
>
|
||||||
|
<svg class="icon" alt="Accept">
|
||||||
|
<use href="/icons/accept.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
id="decline-takeback-button"
|
||||||
|
class="decline-button"
|
||||||
|
ws-send="click"
|
||||||
|
>
|
||||||
|
<svg class="icon" alt="Decline">
|
||||||
|
<use href="/icons/decline.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (server.drawRequesterId) {
|
||||||
|
if (server.drawRequesterId === conn.id) {
|
||||||
|
title = 'You requested a draw';
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
id="cancel-draw-request-button"
|
||||||
|
class="decline-button"
|
||||||
|
ws-send="click"
|
||||||
|
>
|
||||||
|
<svg class="icon" alt="Cancel">
|
||||||
|
<use href="/icons/decline.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
title = 'Your opponent offers a draw';
|
||||||
|
buttons.push(
|
||||||
|
<button id="accept-draw-button" class="accept-button" ws-send="click">
|
||||||
|
<svg class="icon" alt="Accept">
|
||||||
|
<use href="/icons/accept.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
id="decline-draw-button"
|
||||||
|
class="decline-button"
|
||||||
|
ws-send="click"
|
||||||
|
>
|
||||||
|
<svg class="icon" alt="Decline">
|
||||||
|
<use href="/icons/decline.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buttons.push(
|
||||||
|
<button id="resign-button" ws-send="click">
|
||||||
|
<svg class="icon" alt="Resign">
|
||||||
|
<use href="/icons/resign.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
buttons.push(
|
||||||
|
<button id="takeback-button" ws-send="click">
|
||||||
|
<svg class="icon" alt="Takeback">
|
||||||
|
<use href="/icons/undo.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
buttons.push(
|
||||||
|
<button id="draw-button" ws-send="click">
|
||||||
|
<svg class="icon" alt="Draw">
|
||||||
|
<use href="/icons/draw.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (server.gomoku.status === 'finished') {
|
||||||
|
if (server.rematchRequesterId) {
|
||||||
|
if (server.rematchRequesterId === conn.id) {
|
||||||
|
title = 'You requested a rematch';
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
id="cancel-rematch-request-button"
|
||||||
|
class="decline-button"
|
||||||
|
ws-send="click"
|
||||||
|
>
|
||||||
|
<svg class="icon" alt="Cancel">
|
||||||
|
<use href="/icons/decline.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
title = 'Your opponent requests a rematch';
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
id="accept-rematch-button"
|
||||||
|
class="accept-button"
|
||||||
|
ws-send="click"
|
||||||
|
>
|
||||||
|
<svg class="icon" alt="Accept">
|
||||||
|
<use href="/icons/accept.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
id="decline-rematch-button"
|
||||||
|
class="decline-button"
|
||||||
|
ws-send="click"
|
||||||
|
>
|
||||||
|
<svg class="icon" alt="Decline">
|
||||||
|
<use href="/icons/decline.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buttons.push(
|
||||||
|
<button id="rematch-button" ws-send="click">
|
||||||
|
<svg class="icon" alt="Rematch">
|
||||||
|
<use href="/icons/rotate-right.svg"></use>
|
||||||
|
</svg>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (server.gomoku.status === 'waiting') {
|
||||||
|
buttons.push(
|
||||||
|
<button id="copy-link-button" onclick="copyGameLink()">
|
||||||
|
<svg class="icon" alt="Copy">
|
||||||
|
<use href="/icons/clipboard-copy.svg"></use>
|
||||||
|
</svg>
|
||||||
|
<span id="copy-link-text">Copy Link</span>
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.ws.send(
|
||||||
|
<div id="button-box">
|
||||||
|
<div id="button-box-title">{title}</div>
|
||||||
|
<div id="button-box-buttons">{buttons}</div>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
console.log(`Sent buttons for game ${server.id} to player ${conn.id}`);
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
|
||||||
|
export function broadcastSound(server: GameServer, sound: string) {
|
||||||
|
server.connections.forEach((conn: PlayerConnection) =>
|
||||||
|
broadcastSoundToPlayer(conn, sound),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastSoundToPlayer(conn: PlayerConnection, sound: string) {
|
||||||
|
conn.ws.send({
|
||||||
|
type: 'sound',
|
||||||
|
sound: sound,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Html } from '@elysiajs/html';
|
||||||
|
import { PlayerConnection } from '../player-connection';
|
||||||
|
import { GameServer } from '../game-server';
|
||||||
|
import { PlayerColor } from '../game/game-instance';
|
||||||
|
|
||||||
|
export function broadcastTitle(server: GameServer) {
|
||||||
|
server.connections.forEach((conn: PlayerConnection) =>
|
||||||
|
broadcastTitleToPlayer(server, conn),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastTitleToPlayer(
|
||||||
|
server: GameServer,
|
||||||
|
conn: PlayerConnection,
|
||||||
|
) {
|
||||||
|
let message = '';
|
||||||
|
switch (server.gomoku.status) {
|
||||||
|
case 'waiting': {
|
||||||
|
message = 'Waiting for players...';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'playing': {
|
||||||
|
const blackTag = playerTag(server, server.blackPlayerId!, 'black');
|
||||||
|
const whiteTag = playerTag(server, server.whitePlayerId!, 'white');
|
||||||
|
message = `${blackTag} vs ${whiteTag}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'finished': {
|
||||||
|
switch (server.gomoku.winnerColor) {
|
||||||
|
case 'draw': {
|
||||||
|
message = 'Game ended in draw.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'black': {
|
||||||
|
const name = server.connections.get(server.blackPlayerId!)?.name;
|
||||||
|
message = `${name} wins!`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'white': {
|
||||||
|
const name = server.connections.get(server.whitePlayerId!)?.name;
|
||||||
|
message = `${name} wins!`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.ws.send(<div id="title-box">{message}</div>);
|
||||||
|
console.log(`Sent title for game ${server.id} to player ${conn.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerTag(server: GameServer, playerId: string, color: PlayerColor) {
|
||||||
|
const connectionIcon = server.connections.has(playerId)
|
||||||
|
? ''
|
||||||
|
: `<img src="/icons/disconnected.svg" alt="Disconnected" class="icon" />`;
|
||||||
|
var turnClass =
|
||||||
|
server.gomoku.currentPlayerColor === color ? 'player-to-play' : '';
|
||||||
|
const classes = `player-name ${'player-' + color} ${turnClass}`.trim();
|
||||||
|
return (
|
||||||
|
<span class={classes}>
|
||||||
|
{server.connections.get(playerId)?.name}
|
||||||
|
{connectionIcon}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,775 +1,11 @@
|
||||||
import { ElysiaWS } from 'elysia/dist/ws';
|
|
||||||
import { Html } from '@elysiajs/html';
|
|
||||||
import { GomokuGame as GomokuGame, PlayerColor } from './game/game-instance';
|
|
||||||
import { renderGameBoardHtml } from './view/board-renderer';
|
|
||||||
import {
|
|
||||||
Message,
|
|
||||||
MakeMoveMessage,
|
|
||||||
ResignationMessage,
|
|
||||||
TakebackMessage,
|
|
||||||
DrawMessage,
|
|
||||||
RematchMessage,
|
|
||||||
RedirectToGameMessage,
|
|
||||||
UpdateDisplayNameMessage,
|
|
||||||
} from './messages';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { WS } from '.';
|
import { WS } from '.';
|
||||||
|
import { GameServer } from './game-server';
|
||||||
class PlayerConnection {
|
import { Message } from './messages/messages';
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
ws: WS;
|
|
||||||
|
|
||||||
constructor(id: string, name: string, ws: WS) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.ws = ws;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendMessage(severity: 'info' | 'error', message: string) {
|
|
||||||
console.log(`Sending message ${message} to player ${this.id}`);
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GameServer {
|
|
||||||
id: string;
|
|
||||||
gomoku: GomokuGame;
|
|
||||||
connections: Map<string, PlayerConnection>;
|
|
||||||
blackPlayerId?: string;
|
|
||||||
whitePlayerId?: string;
|
|
||||||
takebackRequesterId: string | null = null;
|
|
||||||
drawRequesterId: string | null = null;
|
|
||||||
rematchRequesterId: string | null = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
id: string,
|
|
||||||
private webSocketHandler: WebSocketHandler,
|
|
||||||
) {
|
|
||||||
this.id = id;
|
|
||||||
this.gomoku = new GomokuGame();
|
|
||||||
this.connections = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleConnection(ws: WS) {
|
|
||||||
const { playerId } = ws.data.query;
|
|
||||||
if (this.connections.has(playerId)) {
|
|
||||||
const existingConn = this.connections.get(playerId)!;
|
|
||||||
existingConn.ws = ws; // Update with new WebSocket
|
|
||||||
console.log(
|
|
||||||
`Updated connection for player ${playerId} in game ${this.id}, replacing old WS.`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const playerName = this.webSocketHandler.getPlayerName(playerId); // Retrieve name or use ID
|
|
||||||
const conn = new PlayerConnection(playerId, playerName, ws);
|
|
||||||
this.connections.set(playerId, conn);
|
|
||||||
console.log(
|
|
||||||
`Created new connection with player ${conn.id} in game ${this.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this.blackPlayerId) {
|
|
||||||
this.blackPlayerId = conn.id;
|
|
||||||
} else if (!this.whitePlayerId) {
|
|
||||||
this.whitePlayerId = conn.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.whitePlayerId && this.blackPlayerId) {
|
|
||||||
this.gomoku.status = 'playing';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.broadcastBoard();
|
|
||||||
this.broadcastButtons();
|
|
||||||
this.broadcastTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleDisconnect(ws: WS) {
|
|
||||||
const { playerId } = ws.data.query;
|
|
||||||
this.connections.delete(playerId);
|
|
||||||
this.broadcastTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
public broadcastBoard() {
|
|
||||||
this.connections.forEach((conn: PlayerConnection) =>
|
|
||||||
this.broadcastBoardToPlayer(conn),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public broadcastTitle() {
|
|
||||||
this.connections.forEach((conn: PlayerConnection) =>
|
|
||||||
this.broadcastTitleToPlayer(conn),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public broadcastButtons() {
|
|
||||||
this.connections.forEach((conn: PlayerConnection) =>
|
|
||||||
this.broadcastButtonsToPlayer(conn),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public broadcastSound(sound: string) {
|
|
||||||
this.connections.forEach((conn: PlayerConnection) =>
|
|
||||||
this.broadcastSoundToPlayer(conn, sound),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public broadcastSoundToPlayer(conn: PlayerConnection, sound: string) {
|
|
||||||
conn.ws.send({
|
|
||||||
type: 'sound',
|
|
||||||
sound: sound,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public broadcastBoardToPlayer(conn: PlayerConnection) {
|
|
||||||
const isToPlay =
|
|
||||||
this.gomoku.currentPlayerColor == this.getPlayerColor(conn);
|
|
||||||
const updatedBoardHtml = renderGameBoardHtml(this.gomoku, isToPlay);
|
|
||||||
conn.ws.send(updatedBoardHtml);
|
|
||||||
console.log(`Sent board for game ${this.id} to player ${conn.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public broadcastTitleToPlayer(conn: PlayerConnection) {
|
|
||||||
let message = '';
|
|
||||||
switch (this.gomoku.status) {
|
|
||||||
case 'waiting': {
|
|
||||||
message = 'Waiting for players...';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'playing': {
|
|
||||||
const blackTag = this.playerTag(this.blackPlayerId!, 'black');
|
|
||||||
const whiteTag = this.playerTag(this.whitePlayerId!, 'white');
|
|
||||||
message = `${blackTag} vs ${whiteTag}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'finished': {
|
|
||||||
switch (this.gomoku.winnerColor) {
|
|
||||||
case 'draw': {
|
|
||||||
message = 'Game ended in draw.';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'black': {
|
|
||||||
const name = this.connections.get(this.blackPlayerId!)?.name;
|
|
||||||
message = `${name} wins!`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'white': {
|
|
||||||
const name = this.connections.get(this.whitePlayerId!)?.name;
|
|
||||||
message = `${name} wins!`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conn.ws.send(<div id="title-box">{message}</div>);
|
|
||||||
console.log(`Sent title for game ${this.id} to player ${conn.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public broadcastButtonsToPlayer(conn: PlayerConnection) {
|
|
||||||
const buttons: JSX.Element[] = [];
|
|
||||||
let title: string | undefined;
|
|
||||||
|
|
||||||
if (this.gomoku.status == 'playing' && this.getPlayerColor(conn)) {
|
|
||||||
if (this.takebackRequesterId) {
|
|
||||||
if (this.takebackRequesterId === conn.id) {
|
|
||||||
title = 'You requested a takeback';
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="cancel-takeback-request-button"
|
|
||||||
class="decline-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Cancel">
|
|
||||||
<use href="/icons/decline.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
title = 'Your opponent requests a takeback';
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="accept-takeback-button"
|
|
||||||
class="accept-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Accept">
|
|
||||||
<use href="/icons/accept.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="decline-takeback-button"
|
|
||||||
class="decline-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Decline">
|
|
||||||
<use href="/icons/decline.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this.drawRequesterId) {
|
|
||||||
if (this.drawRequesterId === conn.id) {
|
|
||||||
title = 'You requested a draw';
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="cancel-draw-request-button"
|
|
||||||
class="decline-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Cancel">
|
|
||||||
<use href="/icons/decline.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
title = 'Your opponent offers a draw';
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="accept-draw-button"
|
|
||||||
class="accept-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Accept">
|
|
||||||
<use href="/icons/accept.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="decline-draw-button"
|
|
||||||
class="decline-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Decline">
|
|
||||||
<use href="/icons/decline.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buttons.push(
|
|
||||||
<button id="resign-button" ws-send="click">
|
|
||||||
<svg class="icon" alt="Resign">
|
|
||||||
<use href="/icons/resign.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
buttons.push(
|
|
||||||
<button id="takeback-button" ws-send="click">
|
|
||||||
<svg class="icon" alt="Takeback">
|
|
||||||
<use href="/icons/undo.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
buttons.push(
|
|
||||||
<button id="draw-button" ws-send="click">
|
|
||||||
<svg class="icon" alt="Draw">
|
|
||||||
<use href="/icons/draw.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this.gomoku.status === 'finished') {
|
|
||||||
if (this.rematchRequesterId) {
|
|
||||||
if (this.rematchRequesterId === conn.id) {
|
|
||||||
title = 'You requested a rematch';
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="cancel-rematch-request-button"
|
|
||||||
class="decline-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Cancel">
|
|
||||||
<use href="/icons/decline.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
title = 'Your opponent requests a rematch';
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="accept-rematch-button"
|
|
||||||
class="accept-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Accept">
|
|
||||||
<use href="/icons/accept.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
buttons.push(
|
|
||||||
<button
|
|
||||||
id="decline-rematch-button"
|
|
||||||
class="decline-button"
|
|
||||||
ws-send="click"
|
|
||||||
>
|
|
||||||
<svg class="icon" alt="Decline">
|
|
||||||
<use href="/icons/decline.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buttons.push(
|
|
||||||
<button id="rematch-button" ws-send="click">
|
|
||||||
<svg class="icon" alt="Rematch">
|
|
||||||
<use href="/icons/rotate-right.svg"></use>
|
|
||||||
</svg>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this.gomoku.status === 'waiting') {
|
|
||||||
buttons.push(
|
|
||||||
<button id="copy-link-button" onclick="copyGameLink()">
|
|
||||||
<svg class="icon" alt="Copy">
|
|
||||||
<use href="/icons/clipboard-copy.svg"></use>
|
|
||||||
</svg>
|
|
||||||
<span id="copy-link-text">Copy Link</span>
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.ws.send(
|
|
||||||
<div id="button-box">
|
|
||||||
<div id="button-box-title">{title}</div>
|
|
||||||
<div id="button-box-buttons">{buttons}</div>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
console.log(`Sent buttons for game ${this.id} to player ${conn.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private playerTag(playerId: string, color: PlayerColor) {
|
|
||||||
const connectionIcon = this.connections.has(playerId)
|
|
||||||
? ''
|
|
||||||
: `<img src="/icons/disconnected.svg" alt="Disconnected" class="icon" />`;
|
|
||||||
var turnClass =
|
|
||||||
this.gomoku.currentPlayerColor === color ? 'player-to-play' : '';
|
|
||||||
const classes = `player-name ${'player-' + color} ${turnClass}`.trim();
|
|
||||||
return (
|
|
||||||
<span class={classes}>
|
|
||||||
{this.connections.get(playerId)?.name}
|
|
||||||
{connectionIcon}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMessage(ws: WS, message: Message): void {
|
|
||||||
const conn = this.connections.get(ws.data.query.playerId);
|
|
||||||
if (!conn) {
|
|
||||||
console.error(
|
|
||||||
`Failed to handle message from player ${ws.data.query.playerId}, because they are not in the game ${this.id}, which it was routed to`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Handling ${message.type} message in game ${this.id} from player ${conn.id}: ${JSON.stringify(message)}`,
|
|
||||||
);
|
|
||||||
switch (message.type) {
|
|
||||||
case 'make_move': {
|
|
||||||
this.handleMakeMove(conn, message as MakeMoveMessage);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'resign': {
|
|
||||||
this.handleResignation(conn, message as ResignationMessage);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'takeback': {
|
|
||||||
this.handleTakebackMessage(conn, message as TakebackMessage);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'draw': {
|
|
||||||
this.handleDrawMessage(conn, message as DrawMessage);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'rematch': {
|
|
||||||
this.handleRematchMessage(conn, message as RematchMessage);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'update_display_name': {
|
|
||||||
this.handleUpdateDisplayName(conn, message as UpdateDisplayNameMessage);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPlayerColor(conn: PlayerConnection): PlayerColor | undefined {
|
|
||||||
if (this.blackPlayerId === conn.id) {
|
|
||||||
return 'black';
|
|
||||||
} else if (this.whitePlayerId === conn.id) {
|
|
||||||
return 'white';
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMakeMove(
|
|
||||||
conn: PlayerConnection,
|
|
||||||
message: MakeMoveMessage,
|
|
||||||
): void {
|
|
||||||
const { row, col } = message;
|
|
||||||
|
|
||||||
var playerColor;
|
|
||||||
if (this.blackPlayerId === conn.id) {
|
|
||||||
playerColor = 'black';
|
|
||||||
} else if (this.whitePlayerId == conn.id) {
|
|
||||||
playerColor = 'white';
|
|
||||||
} else {
|
|
||||||
conn.sendMessage(
|
|
||||||
'error',
|
|
||||||
'You are not a player in this game, you cannot make a move!',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.gomoku.currentPlayerColor !== playerColor) {
|
|
||||||
conn.sendMessage('error', "It's not your turn");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.takebackRequesterId || this.drawRequesterId) {
|
|
||||||
this.takebackRequesterId = null;
|
|
||||||
this.drawRequesterId = null;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateBeforeMove = this.gomoku.status;
|
|
||||||
const result = this.gomoku.makeMove(playerColor, row, col);
|
|
||||||
if (result.success) {
|
|
||||||
this.broadcastBoard();
|
|
||||||
this.broadcastTitle();
|
|
||||||
// We only need to re-send buttons when the game state changes
|
|
||||||
if (stateBeforeMove != this.gomoku.status) {
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast sounds
|
|
||||||
if (this.gomoku.status === 'playing') {
|
|
||||||
this.broadcastSound('move');
|
|
||||||
} else {
|
|
||||||
const whiteConn = this.whitePlayerId
|
|
||||||
? this.connections.get(this.whitePlayerId)
|
|
||||||
: null;
|
|
||||||
const blackConn = this.blackPlayerId
|
|
||||||
? this.connections.get(this.blackPlayerId)
|
|
||||||
: null;
|
|
||||||
switch (this.gomoku.winnerColor) {
|
|
||||||
case 'draw':
|
|
||||||
this.broadcastSound('draw');
|
|
||||||
case 'white':
|
|
||||||
if (whiteConn) {
|
|
||||||
this.broadcastSoundToPlayer(whiteConn, 'victory');
|
|
||||||
}
|
|
||||||
if (blackConn) {
|
|
||||||
this.broadcastSoundToPlayer(blackConn, 'defeat');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'black':
|
|
||||||
if (whiteConn) {
|
|
||||||
this.broadcastSoundToPlayer(whiteConn, 'defeat');
|
|
||||||
}
|
|
||||||
if (blackConn) {
|
|
||||||
this.broadcastSoundToPlayer(blackConn, 'victory');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Move made in game ${this.id} by ${conn.id}: (${row}, ${col})`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
conn.sendMessage('error', result.error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleResignation(
|
|
||||||
conn: PlayerConnection,
|
|
||||||
message: ResignationMessage,
|
|
||||||
): void {
|
|
||||||
console.log(
|
|
||||||
`Handling resign message in game ${this.id} from player ${conn.id}: ${{ message }}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.gomoku.status !== 'playing') {
|
|
||||||
conn.sendMessage('error', 'You can only resign from an active game.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resigningPlayerColor = this.getPlayerColor(conn);
|
|
||||||
if (!resigningPlayerColor) {
|
|
||||||
conn.sendMessage('error', 'You are not a player in this game.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.gomoku.resign(resigningPlayerColor);
|
|
||||||
this.broadcastBoard();
|
|
||||||
this.broadcastTitle();
|
|
||||||
this.broadcastButtons();
|
|
||||||
this.broadcastSoundToPlayer(conn, 'defeat');
|
|
||||||
const otherPlayerId =
|
|
||||||
resigningPlayerColor === 'white'
|
|
||||||
? this.blackPlayerId
|
|
||||||
: this.whitePlayerId;
|
|
||||||
if (otherPlayerId) {
|
|
||||||
const otherPlayer = this.connections.get(otherPlayerId);
|
|
||||||
if (otherPlayer) {
|
|
||||||
this.broadcastSoundToPlayer(otherPlayer, 'victory');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Player ${conn.id} resigned from game ${this.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleTakebackMessage(
|
|
||||||
conn: PlayerConnection,
|
|
||||||
message: TakebackMessage,
|
|
||||||
): void {
|
|
||||||
if (this.gomoku.status !== 'playing') {
|
|
||||||
conn.sendMessage(
|
|
||||||
'error',
|
|
||||||
'You can only perform this action in an active game.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (message.action) {
|
|
||||||
case 'request':
|
|
||||||
this.handleRequestTakeback(conn);
|
|
||||||
break;
|
|
||||||
case 'accept':
|
|
||||||
if (!this.takebackRequesterId) {
|
|
||||||
conn.sendMessage('error', 'No takeback has been requested.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleAcceptTakeback();
|
|
||||||
break;
|
|
||||||
case 'decline':
|
|
||||||
if (!this.takebackRequesterId) {
|
|
||||||
conn.sendMessage('error', 'No takeback has been requested.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleDeclineTakeback();
|
|
||||||
break;
|
|
||||||
case 'cancel':
|
|
||||||
if (this.takebackRequesterId !== conn.id) {
|
|
||||||
conn.sendMessage(
|
|
||||||
'error',
|
|
||||||
'You are not the one who requested a takeback.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleCancelTakebackRequest();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDrawMessage(
|
|
||||||
conn: PlayerConnection,
|
|
||||||
message: DrawMessage,
|
|
||||||
): void {
|
|
||||||
if (this.gomoku.status !== 'playing') {
|
|
||||||
conn.sendMessage(
|
|
||||||
'error',
|
|
||||||
'You can only perform this action in an active game.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (message.action) {
|
|
||||||
case 'request':
|
|
||||||
this.handleRequestDraw(conn);
|
|
||||||
break;
|
|
||||||
case 'accept':
|
|
||||||
if (!this.drawRequesterId) {
|
|
||||||
conn.sendMessage('error', 'No draw has been requested.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleAcceptDraw();
|
|
||||||
break;
|
|
||||||
case 'decline':
|
|
||||||
if (!this.drawRequesterId) {
|
|
||||||
conn.sendMessage('error', 'No draw has been requested.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleDeclineDraw();
|
|
||||||
break;
|
|
||||||
case 'cancel':
|
|
||||||
if (this.drawRequesterId !== conn.id) {
|
|
||||||
conn.sendMessage(
|
|
||||||
'error',
|
|
||||||
'You are not the one who requested a draw.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleCancelDrawRequest();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRematchMessage(
|
|
||||||
conn: PlayerConnection,
|
|
||||||
message: RematchMessage,
|
|
||||||
): void {
|
|
||||||
if (this.gomoku.status !== 'finished') {
|
|
||||||
conn.sendMessage(
|
|
||||||
'error',
|
|
||||||
'You can only perform this action in a finished game.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (message.action) {
|
|
||||||
case 'request':
|
|
||||||
this.handleRequestRematch(conn);
|
|
||||||
break;
|
|
||||||
case 'accept':
|
|
||||||
if (!this.rematchRequesterId) {
|
|
||||||
conn.sendMessage('error', 'No rematch has been requested.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleAcceptRematch();
|
|
||||||
break;
|
|
||||||
case 'decline':
|
|
||||||
if (!this.rematchRequesterId) {
|
|
||||||
conn.sendMessage('error', 'No rematch has been requested.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleDeclineRematch();
|
|
||||||
break;
|
|
||||||
case 'cancel':
|
|
||||||
if (this.rematchRequesterId !== conn.id) {
|
|
||||||
conn.sendMessage(
|
|
||||||
'error',
|
|
||||||
'You are not the one who requested a rematch.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleCancelRematchRequest();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRequestTakeback(conn: PlayerConnection): void {
|
|
||||||
if (this.gomoku.history.length === 0) {
|
|
||||||
conn.sendMessage('error', 'There are no moves to take back.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.drawRequesterId) {
|
|
||||||
conn.sendMessage('error', 'A draw has already been requested.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.takebackRequesterId = conn.id;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAcceptTakeback(): void {
|
|
||||||
this.gomoku.undoMove();
|
|
||||||
this.takebackRequesterId = null;
|
|
||||||
this.broadcastBoard();
|
|
||||||
this.broadcastButtons();
|
|
||||||
this.broadcastTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDeclineTakeback(): void {
|
|
||||||
this.takebackRequesterId = null;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRequestDraw(conn: PlayerConnection): void {
|
|
||||||
if (this.takebackRequesterId) {
|
|
||||||
conn.sendMessage('error', 'A takeback has already been requested.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.drawRequesterId = conn.id;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAcceptDraw(): void {
|
|
||||||
this.gomoku.declareDraw();
|
|
||||||
this.drawRequesterId = null;
|
|
||||||
this.broadcastBoard();
|
|
||||||
this.broadcastButtons();
|
|
||||||
this.broadcastTitle();
|
|
||||||
this.broadcastSound('draw');
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDeclineDraw(): void {
|
|
||||||
this.drawRequesterId = null;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRequestRematch(conn: PlayerConnection): void {
|
|
||||||
this.rematchRequesterId = conn.id;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAcceptRematch(): void {
|
|
||||||
const newGameId = this.webSocketHandler.createGame(
|
|
||||||
undefined,
|
|
||||||
this.whitePlayerId,
|
|
||||||
this.blackPlayerId,
|
|
||||||
);
|
|
||||||
const redirectMessage: RedirectToGameMessage = {
|
|
||||||
type: 'redirect_to_game',
|
|
||||||
gameId: newGameId,
|
|
||||||
};
|
|
||||||
this.connections.forEach((c) => {
|
|
||||||
c.ws.send(redirectMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDeclineRematch(): void {
|
|
||||||
this.rematchRequesterId = null;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCancelTakebackRequest(): void {
|
|
||||||
this.takebackRequesterId = null;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCancelDrawRequest(): void {
|
|
||||||
this.drawRequesterId = null;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCancelRematchRequest(): void {
|
|
||||||
this.rematchRequesterId = null;
|
|
||||||
this.broadcastButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleUpdateDisplayName(
|
|
||||||
conn: PlayerConnection,
|
|
||||||
message: UpdateDisplayNameMessage,
|
|
||||||
): void {
|
|
||||||
const newDisplayName = message.displayName.trim();
|
|
||||||
|
|
||||||
if (!newDisplayName) {
|
|
||||||
conn.sendMessage('error', 'Display name cannot be empty.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newDisplayName.length > 20) {
|
|
||||||
conn.sendMessage(
|
|
||||||
'error',
|
|
||||||
'Display name cannot be longer than 20 characters.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newDisplayName === conn.name) {
|
|
||||||
return; // No change, do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.name = newDisplayName;
|
|
||||||
this.webSocketHandler.setPlayerName(conn.id, newDisplayName);
|
|
||||||
this.broadcastTitle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WebSocketHandler {
|
export class WebSocketHandler {
|
||||||
private games: Map<String, GameServer>;
|
public games: Map<String, GameServer>;
|
||||||
private playerNames: Map<string, string>;
|
public playerNames: Map<string, string>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.games = new Map();
|
this.games = new Map();
|
||||||
|
@ -777,7 +13,7 @@ export class WebSocketHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleConnection(ws: WS): void {
|
public handleConnection(ws: WS): void {
|
||||||
const { gameId, playerId } = ws.data.query;
|
const { gameId } = ws.data.query;
|
||||||
if (this.games.has(gameId)) {
|
if (this.games.has(gameId)) {
|
||||||
const game = this.games.get(gameId)!;
|
const game = this.games.get(gameId)!;
|
||||||
game.handleConnection(ws);
|
game.handleConnection(ws);
|
||||||
|
|
Loading…
Reference in New Issue