diff --git a/index.html b/index.html index e68ffce..e9405d8 100644 --- a/index.html +++ b/index.html @@ -35,5 +35,6 @@ + diff --git a/public/scripts/handle-redirects.js b/public/scripts/handle-redirects.js new file mode 100644 index 0000000..ba721d9 --- /dev/null +++ b/public/scripts/handle-redirects.js @@ -0,0 +1,6 @@ +document.addEventListener('htmx:wsAfterMessage', function (e) { + const message = JSON.parse(e.detail.message); + if (message.type === 'redirect_to_game') { + window.location.href = '/?gameId=' + message.gameId; + } +}); diff --git a/public/scripts/send-ws-messages.js b/public/scripts/send-ws-messages.js index f11f22a..e6e3f81 100644 --- a/public/scripts/send-ws-messages.js +++ b/public/scripts/send-ws-messages.js @@ -27,5 +27,29 @@ document.addEventListener('htmx:wsConfigSend', function (e) { e.detail.parameters = { type: 'decline_takeback', }; + } else if (e.target.id == 'draw-button') { + e.detail.parameters = { + type: 'request_draw', + }; + } else if (e.target.id == 'accept-draw-button') { + e.detail.parameters = { + type: 'accept_draw', + }; + } else if (e.target.id == 'decline-draw-button') { + e.detail.parameters = { + type: 'decline_draw', + }; + } else if (e.target.id == 'rematch-button') { + e.detail.parameters = { + type: 'request_rematch', + }; + } else if (e.target.id == 'accept-rematch-button') { + e.detail.parameters = { + type: 'accept_rematch', + }; + } else if (e.target.id == 'decline-rematch-button') { + e.detail.parameters = { + type: 'decline_rematch', + }; } }); diff --git a/src/game/game-instance.ts b/src/game/game-instance.ts index f0a64d9..0f11b86 100644 --- a/src/game/game-instance.ts +++ b/src/game/game-instance.ts @@ -100,6 +100,12 @@ export class GomokuGame { } } + public declareDraw() { + this.status = 'finished'; + this.winnerColor = 'draw'; + this.currentPlayerColor = null; + } + private checkWin(row: number, col: number, color: PlayerColor): boolean { const directions = [ [1, 0], // vertical diff --git a/src/messages.ts b/src/messages.ts index 6df0c7a..201a927 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -4,7 +4,14 @@ export interface Message { | 'resign' | 'request_takeback' | 'accept_takeback' - | 'decline_takeback'; + | 'decline_takeback' + | 'request_draw' + | 'accept_draw' + | 'decline_draw' + | 'request_rematch' + | 'accept_rematch' + | 'decline_rematch' + | 'redirect_to_game'; } export interface MakeMoveMessage extends Message { @@ -19,3 +26,19 @@ export interface RequestTakebackMessage extends Message {} export interface AcceptTakebackMessage extends Message {} export interface DeclineTakebackMessage extends Message {} + +export interface RequestDrawMessage extends Message {} + +export interface AcceptDrawMessage extends Message {} + +export interface DeclineDrawMessage extends Message {} + +export interface RequestRematchMessage extends Message {} + +export interface AcceptRematchMessage extends Message {} + +export interface DeclineRematchMessage extends Message {} + +export interface RedirectToGameMessage extends Message { + gameId: string; +} diff --git a/src/web-socket-handler.tsx b/src/web-socket-handler.tsx index 82563de..7aabbdb 100644 --- a/src/web-socket-handler.tsx +++ b/src/web-socket-handler.tsx @@ -9,6 +9,13 @@ import { RequestTakebackMessage, AcceptTakebackMessage, DeclineTakebackMessage, + RequestDrawMessage, + AcceptDrawMessage, + DeclineDrawMessage, + RequestRematchMessage, + AcceptRematchMessage, + DeclineRematchMessage, + RedirectToGameMessage, } from './messages'; import { v4 as uuidv4 } from 'uuid'; @@ -37,8 +44,13 @@ class GameServer { blackPlayerId?: string; whitePlayerId?: string; takebackRequesterId: string | null = null; + drawRequesterId: string | null = null; + rematchRequesterId: string | null = null; - constructor(id: string) { + constructor( + id: string, + private webSocketHandler: WebSocketHandler, + ) { this.id = id; this.gomoku = new GomokuGame(); this.connections = new Map(); @@ -146,10 +158,29 @@ class GameServer { buttonsHtml = (
+
+ ); + } + } else if (this.drawRequesterId) { + if (this.drawRequesterId === conn.id) { + buttonsHtml = ( + + ); + } else { + buttonsHtml = ( +
+ +
); @@ -163,9 +194,39 @@ class GameServer { + ); } + } else if (this.gomoku.status === 'finished') { + if (this.rematchRequesterId) { + if (this.rematchRequesterId === conn.id) { + buttonsHtml = ( + + ); + } else { + buttonsHtml = ( +
+ + +
+ ); + } + } else { + buttonsHtml = ( + + ); + } } conn.ws.send(
{buttonsHtml}
); console.log(`Sent buttons for game ${this.id} to player ${conn.id}`); @@ -219,6 +280,30 @@ class GameServer { this.handleDeclineTakeback(conn, message as DeclineTakebackMessage); break; } + case 'request_draw': { + this.handleRequestDraw(conn, message as RequestDrawMessage); + break; + } + case 'accept_draw': { + this.handleAcceptDraw(conn, message as AcceptDrawMessage); + break; + } + case 'decline_draw': { + this.handleDeclineDraw(conn, message as DeclineDrawMessage); + break; + } + case 'request_rematch': { + this.handleRequestRematch(conn, message as RequestRematchMessage); + break; + } + case 'accept_rematch': { + this.handleAcceptRematch(conn, message as AcceptRematchMessage); + break; + } + case 'decline_rematch': { + this.handleDeclineRematch(conn, message as DeclineRematchMessage); + break; + } } } @@ -255,8 +340,9 @@ class GameServer { return; } - if (this.takebackRequesterId) { + if (this.takebackRequesterId || this.drawRequesterId) { this.takebackRequesterId = null; + this.drawRequesterId = null; this.broadcastButtons(); } @@ -321,6 +407,10 @@ class GameServer { return; } + if (this.drawRequesterId) { + conn.sendMessage('error', 'A draw has already been requested.'); + return; + } this.takebackRequesterId = conn.id; this.broadcastButtons(); } @@ -369,6 +459,139 @@ class GameServer { this.takebackRequesterId = null; this.broadcastButtons(); } + + private handleRequestDraw( + conn: PlayerConnection, + message: RequestDrawMessage, + ): void { + if (this.gomoku.status !== 'playing') { + conn.sendMessage( + 'error', + 'You can only request a draw in an active game.', + ); + return; + } + if (this.takebackRequesterId) { + conn.sendMessage('error', 'A takeback has already been requested.'); + return; + } + + this.drawRequesterId = conn.id; + this.broadcastButtons(); + } + + private handleAcceptDraw( + conn: PlayerConnection, + message: AcceptDrawMessage, + ): void { + if (this.gomoku.status !== 'playing') { + conn.sendMessage( + 'error', + 'You can only accept a draw in an active game.', + ); + return; + } + + if (!this.drawRequesterId) { + conn.sendMessage('error', 'No draw has been requested.'); + return; + } + + this.gomoku.declareDraw(); + this.drawRequesterId = null; + this.broadcastBoard(); + this.broadcastButtons(); + this.broadcastTitle(); + } + + private handleDeclineDraw( + conn: PlayerConnection, + message: DeclineDrawMessage, + ): void { + if (this.gomoku.status !== 'playing') { + conn.sendMessage( + 'error', + 'You can only decline a draw in an active game.', + ); + return; + } + + if (!this.drawRequesterId) { + conn.sendMessage('error', 'No draw has been requested.'); + return; + } + + this.drawRequesterId = null; + this.broadcastButtons(); + } + + private handleRequestRematch( + conn: PlayerConnection, + message: RequestRematchMessage, + ): void { + if (this.gomoku.status !== 'finished') { + conn.sendMessage( + 'error', + 'You can only request a rematch in a finished game.', + ); + return; + } + + this.rematchRequesterId = conn.id; + this.broadcastButtons(); + } + + private handleAcceptRematch( + conn: PlayerConnection, + message: AcceptRematchMessage, + ): void { + if (this.gomoku.status !== 'finished') { + conn.sendMessage( + 'error', + 'You can only accept a rematch in a finished game.', + ); + return; + } + + if (!this.rematchRequesterId) { + conn.sendMessage('error', 'No rematch has been requested.'); + return; + } + + 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( + conn: PlayerConnection, + message: DeclineRematchMessage, + ): void { + if (this.gomoku.status !== 'finished') { + conn.sendMessage( + 'error', + 'You can only decline a rematch in a finished game.', + ); + return; + } + + if (!this.rematchRequesterId) { + conn.sendMessage('error', 'No rematch has been requested.'); + return; + } + + this.rematchRequesterId = null; + this.broadcastButtons(); + } } export class WebSocketHandler { @@ -430,9 +653,16 @@ export class WebSocketHandler { gameServer.handleMessage(ws, message); } - public createGame(id?: string): string { + public createGame( + id?: string, + blackPlayerId?: string, + whitePlayerId?: string, + ): string { const realId = id ? id : uuidv4(); - this.games.set(realId, new GameServer(realId)); + this.games.set(realId, new GameServer(realId, this)); + const game = this.games.get(realId)!; + game.blackPlayerId = blackPlayerId; + game.whitePlayerId = whitePlayerId; return realId; }