Add draw requests and rematches

This commit is contained in:
sepia 2025-07-22 20:44:06 -05:00
parent 1a221bf680
commit 2f46d86947
6 changed files with 297 additions and 7 deletions

View File

@ -35,5 +35,6 @@
<script src="scripts/display-ws-connection.js"></script>
<script src="scripts/send-ws-messages.js"></script>
<script src="scripts/copy-game-link.js"></script>
<script src="scripts/handle-redirects.js"></script>
</body>
</html>

View File

@ -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;
}
});

View File

@ -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',
};
}
});

View File

@ -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

View File

@ -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;
}

View File

@ -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 = (
<div>
<button id="accept-takeback-button" ws-send="click">
Accept
Accept Takeback
</button>
<button id="decline-takeback-button" ws-send="click">
Decline
Decline Takeback
</button>
</div>
);
}
} else if (this.drawRequesterId) {
if (this.drawRequesterId === conn.id) {
buttonsHtml = (
<button id="draw-button" disabled>
Draw Requested
</button>
);
} else {
buttonsHtml = (
<div>
<button id="accept-draw-button" ws-send="click">
Accept Draw
</button>
<button id="decline-draw-button" ws-send="click">
Decline Draw
</button>
</div>
);
@ -163,9 +194,39 @@ class GameServer {
<button id="takeback-button" ws-send="click">
Takeback
</button>
<button id="draw-button" ws-send="click">
Draw
</button>
</div>
);
}
} else if (this.gomoku.status === 'finished') {
if (this.rematchRequesterId) {
if (this.rematchRequesterId === conn.id) {
buttonsHtml = (
<button id="rematch-button" disabled>
Rematch Requested
</button>
);
} else {
buttonsHtml = (
<div>
<button id="accept-rematch-button" ws-send="click">
Accept Rematch
</button>
<button id="decline-rematch-button" ws-send="click">
Decline Rematch
</button>
</div>
);
}
} else {
buttonsHtml = (
<button id="rematch-button" ws-send="click">
Rematch
</button>
);
}
}
conn.ws.send(<div id="button-box">{buttonsHtml}</div>);
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;
}