Add draw requests and rematches
This commit is contained in:
parent
1a221bf680
commit
2f46d86947
|
@ -35,5 +35,6 @@
|
||||||
<script src="scripts/display-ws-connection.js"></script>
|
<script src="scripts/display-ws-connection.js"></script>
|
||||||
<script src="scripts/send-ws-messages.js"></script>
|
<script src="scripts/send-ws-messages.js"></script>
|
||||||
<script src="scripts/copy-game-link.js"></script>
|
<script src="scripts/copy-game-link.js"></script>
|
||||||
|
<script src="scripts/handle-redirects.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
|
@ -27,5 +27,29 @@ document.addEventListener('htmx:wsConfigSend', function (e) {
|
||||||
e.detail.parameters = {
|
e.detail.parameters = {
|
||||||
type: 'decline_takeback',
|
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',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
private checkWin(row: number, col: number, color: PlayerColor): boolean {
|
||||||
const directions = [
|
const directions = [
|
||||||
[1, 0], // vertical
|
[1, 0], // vertical
|
||||||
|
|
|
@ -4,7 +4,14 @@ export interface Message {
|
||||||
| 'resign'
|
| 'resign'
|
||||||
| 'request_takeback'
|
| 'request_takeback'
|
||||||
| 'accept_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 {
|
export interface MakeMoveMessage extends Message {
|
||||||
|
@ -19,3 +26,19 @@ export interface RequestTakebackMessage extends Message {}
|
||||||
export interface AcceptTakebackMessage extends Message {}
|
export interface AcceptTakebackMessage extends Message {}
|
||||||
|
|
||||||
export interface DeclineTakebackMessage 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;
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,13 @@ import {
|
||||||
RequestTakebackMessage,
|
RequestTakebackMessage,
|
||||||
AcceptTakebackMessage,
|
AcceptTakebackMessage,
|
||||||
DeclineTakebackMessage,
|
DeclineTakebackMessage,
|
||||||
|
RequestDrawMessage,
|
||||||
|
AcceptDrawMessage,
|
||||||
|
DeclineDrawMessage,
|
||||||
|
RequestRematchMessage,
|
||||||
|
AcceptRematchMessage,
|
||||||
|
DeclineRematchMessage,
|
||||||
|
RedirectToGameMessage,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
@ -37,8 +44,13 @@ class GameServer {
|
||||||
blackPlayerId?: string;
|
blackPlayerId?: string;
|
||||||
whitePlayerId?: string;
|
whitePlayerId?: string;
|
||||||
takebackRequesterId: string | null = null;
|
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.id = id;
|
||||||
this.gomoku = new GomokuGame();
|
this.gomoku = new GomokuGame();
|
||||||
this.connections = new Map();
|
this.connections = new Map();
|
||||||
|
@ -146,10 +158,29 @@ class GameServer {
|
||||||
buttonsHtml = (
|
buttonsHtml = (
|
||||||
<div>
|
<div>
|
||||||
<button id="accept-takeback-button" ws-send="click">
|
<button id="accept-takeback-button" ws-send="click">
|
||||||
Accept
|
Accept Takeback
|
||||||
</button>
|
</button>
|
||||||
<button id="decline-takeback-button" ws-send="click">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -163,9 +194,39 @@ class GameServer {
|
||||||
<button id="takeback-button" ws-send="click">
|
<button id="takeback-button" ws-send="click">
|
||||||
Takeback
|
Takeback
|
||||||
</button>
|
</button>
|
||||||
|
<button id="draw-button" ws-send="click">
|
||||||
|
Draw
|
||||||
|
</button>
|
||||||
</div>
|
</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>);
|
conn.ws.send(<div id="button-box">{buttonsHtml}</div>);
|
||||||
console.log(`Sent buttons for game ${this.id} to player ${conn.id}`);
|
console.log(`Sent buttons for game ${this.id} to player ${conn.id}`);
|
||||||
|
@ -219,6 +280,30 @@ class GameServer {
|
||||||
this.handleDeclineTakeback(conn, message as DeclineTakebackMessage);
|
this.handleDeclineTakeback(conn, message as DeclineTakebackMessage);
|
||||||
break;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.takebackRequesterId) {
|
if (this.takebackRequesterId || this.drawRequesterId) {
|
||||||
this.takebackRequesterId = null;
|
this.takebackRequesterId = null;
|
||||||
|
this.drawRequesterId = null;
|
||||||
this.broadcastButtons();
|
this.broadcastButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,6 +407,10 @@ class GameServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.drawRequesterId) {
|
||||||
|
conn.sendMessage('error', 'A draw has already been requested.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.takebackRequesterId = conn.id;
|
this.takebackRequesterId = conn.id;
|
||||||
this.broadcastButtons();
|
this.broadcastButtons();
|
||||||
}
|
}
|
||||||
|
@ -369,6 +459,139 @@ class GameServer {
|
||||||
this.takebackRequesterId = null;
|
this.takebackRequesterId = null;
|
||||||
this.broadcastButtons();
|
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 {
|
export class WebSocketHandler {
|
||||||
|
@ -430,9 +653,16 @@ export class WebSocketHandler {
|
||||||
gameServer.handleMessage(ws, message);
|
gameServer.handleMessage(ws, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public createGame(id?: string): string {
|
public createGame(
|
||||||
|
id?: string,
|
||||||
|
blackPlayerId?: string,
|
||||||
|
whitePlayerId?: string,
|
||||||
|
): string {
|
||||||
const realId = id ? id : uuidv4();
|
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;
|
return realId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue