Get the client to a point where it at least renders the board
This commit is contained in:
parent
3be0c40b64
commit
e8e982c3d6
|
@ -0,0 +1,307 @@
|
||||||
|
// src/game-client/WebSocketClient.ts
|
||||||
|
class WebSocketClient {
|
||||||
|
ws = null;
|
||||||
|
url;
|
||||||
|
messageQueue = [];
|
||||||
|
isConnected = false;
|
||||||
|
reconnectCount = 0;
|
||||||
|
options;
|
||||||
|
manualClose = false;
|
||||||
|
onMessageHandler = () => {};
|
||||||
|
onOpenHandler = () => {};
|
||||||
|
onCloseHandler = () => {};
|
||||||
|
onErrorHandler = () => {};
|
||||||
|
constructor(url, options) {
|
||||||
|
this.url = url;
|
||||||
|
this.options = {
|
||||||
|
reconnectAttempts: 5,
|
||||||
|
reconnectInterval: 3000,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
this.manualClose = false;
|
||||||
|
this.ws = new WebSocket(this.url);
|
||||||
|
this.ws.onopen = this.handleOpen.bind(this);
|
||||||
|
this.ws.onmessage = this.handleMessage.bind(this);
|
||||||
|
this.ws.onclose = this.handleClose.bind(this);
|
||||||
|
this.ws.onerror = this.handleError.bind(this);
|
||||||
|
}
|
||||||
|
send(message) {
|
||||||
|
if (this.isConnected && this.ws) {
|
||||||
|
this.ws.send(message);
|
||||||
|
} else {
|
||||||
|
this.messageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
this.manualClose = true;
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMessage(handler) {
|
||||||
|
this.onMessageHandler = handler;
|
||||||
|
}
|
||||||
|
onOpen(handler) {
|
||||||
|
this.onOpenHandler = handler;
|
||||||
|
}
|
||||||
|
onClose(handler) {
|
||||||
|
this.onCloseHandler = handler;
|
||||||
|
}
|
||||||
|
onError(handler) {
|
||||||
|
this.onErrorHandler = handler;
|
||||||
|
}
|
||||||
|
handleOpen() {
|
||||||
|
this.isConnected = true;
|
||||||
|
this.reconnectCount = 0;
|
||||||
|
this.onOpenHandler();
|
||||||
|
this.flushMessageQueue();
|
||||||
|
}
|
||||||
|
handleMessage(event) {
|
||||||
|
this.onMessageHandler(event.data);
|
||||||
|
}
|
||||||
|
handleClose(event) {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.onCloseHandler(event.code, event.reason);
|
||||||
|
if (
|
||||||
|
!this.manualClose &&
|
||||||
|
this.reconnectCount < this.options.reconnectAttempts
|
||||||
|
) {
|
||||||
|
this.reconnectCount++;
|
||||||
|
setTimeout(() => this.connect(), this.options.reconnectInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleError(event) {
|
||||||
|
this.onErrorHandler(event);
|
||||||
|
}
|
||||||
|
flushMessageQueue() {
|
||||||
|
while (this.messageQueue.length > 0 && this.isConnected && this.ws) {
|
||||||
|
const message = this.messageQueue.shift();
|
||||||
|
if (message) {
|
||||||
|
this.ws.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/game-client/GameStateManager.ts
|
||||||
|
class GameStateManager {
|
||||||
|
gameState;
|
||||||
|
stateHistory;
|
||||||
|
constructor() {
|
||||||
|
this.gameState = this.getDefaultGameState();
|
||||||
|
this.stateHistory = [];
|
||||||
|
}
|
||||||
|
getDefaultGameState() {
|
||||||
|
const emptyBoard = Array(15)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => Array(15).fill(null));
|
||||||
|
return {
|
||||||
|
id: '',
|
||||||
|
board: emptyBoard,
|
||||||
|
currentPlayer: 'black',
|
||||||
|
status: 'waiting',
|
||||||
|
winner: null,
|
||||||
|
players: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
getGameState() {
|
||||||
|
return this.gameState;
|
||||||
|
}
|
||||||
|
updateGameState(newState) {
|
||||||
|
this.stateHistory.push(JSON.parse(JSON.stringify(this.gameState)));
|
||||||
|
this.gameState = newState;
|
||||||
|
}
|
||||||
|
rollbackGameState() {
|
||||||
|
if (this.stateHistory.length > 0) {
|
||||||
|
this.gameState = this.stateHistory.pop();
|
||||||
|
} else {
|
||||||
|
console.warn('No previous state to rollback to.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/game-client/GameBoardUI.ts
|
||||||
|
class GameBoardUI {
|
||||||
|
boardElement;
|
||||||
|
cells = [];
|
||||||
|
onCellClickCallback = null;
|
||||||
|
isInteractionEnabled = true;
|
||||||
|
constructor(boardElement) {
|
||||||
|
this.boardElement = boardElement;
|
||||||
|
this.initializeBoard();
|
||||||
|
}
|
||||||
|
initializeBoard() {
|
||||||
|
this.boardElement.innerHTML = '';
|
||||||
|
this.boardElement.style.display = 'grid';
|
||||||
|
this.boardElement.style.gridTemplateColumns = 'repeat(15, 1fr)';
|
||||||
|
this.boardElement.style.width = '450px';
|
||||||
|
this.boardElement.style.height = '450px';
|
||||||
|
this.boardElement.style.border = '1px solid black';
|
||||||
|
for (let row = 0; row < 15; row++) {
|
||||||
|
this.cells[row] = [];
|
||||||
|
for (let col = 0; col < 15; col++) {
|
||||||
|
const cell = document.createElement('div');
|
||||||
|
cell.classList.add('board-cell');
|
||||||
|
cell.style.width = '30px';
|
||||||
|
cell.style.height = '30px';
|
||||||
|
cell.style.border = '1px solid #ccc';
|
||||||
|
cell.style.boxSizing = 'border-box';
|
||||||
|
cell.style.display = 'flex';
|
||||||
|
cell.style.justifyContent = 'center';
|
||||||
|
cell.style.alignItems = 'center';
|
||||||
|
cell.dataset.row = row.toString();
|
||||||
|
cell.dataset.col = col.toString();
|
||||||
|
cell.addEventListener('click', () => this.handleCellClick(row, col));
|
||||||
|
this.boardElement.appendChild(cell);
|
||||||
|
this.cells[row][col] = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateBoard(gameState) {
|
||||||
|
const board = gameState.board;
|
||||||
|
const lastMove = { row: -1, col: -1 };
|
||||||
|
for (let row = 0; row < 15; row++) {
|
||||||
|
for (let col = 0; col < 15; col++) {
|
||||||
|
const cell = this.cells[row][col];
|
||||||
|
cell.innerHTML = '';
|
||||||
|
const stone = board[row][col];
|
||||||
|
if (stone) {
|
||||||
|
const stoneElement = document.createElement('div');
|
||||||
|
stoneElement.style.width = '24px';
|
||||||
|
stoneElement.style.height = '24px';
|
||||||
|
stoneElement.style.borderRadius = '50%';
|
||||||
|
stoneElement.style.backgroundColor =
|
||||||
|
stone === 'black' ? 'black' : 'white';
|
||||||
|
stoneElement.style.border = '1px solid #333';
|
||||||
|
cell.appendChild(stoneElement);
|
||||||
|
}
|
||||||
|
cell.classList.remove('last-move');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isInteractionEnabled =
|
||||||
|
gameState.status === 'playing' && gameState.currentPlayer === 'black';
|
||||||
|
this.boardElement.style.pointerEvents = this.isInteractionEnabled
|
||||||
|
? 'auto'
|
||||||
|
: 'none';
|
||||||
|
this.boardElement.style.opacity = this.isInteractionEnabled ? '1' : '0.7';
|
||||||
|
console.log(
|
||||||
|
`Current Player: ${gameState.currentPlayer}, Status: ${gameState.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setOnCellClick(callback) {
|
||||||
|
this.onCellClickCallback = callback;
|
||||||
|
}
|
||||||
|
handleCellClick(row, col) {
|
||||||
|
if (this.isInteractionEnabled && this.onCellClickCallback) {
|
||||||
|
this.onCellClickCallback(row, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/client-entry.ts
|
||||||
|
console.log('Gomoku client entry point loaded.');
|
||||||
|
var WS_URL = 'ws://localhost:3000/ws';
|
||||||
|
var gameStateManager = new GameStateManager();
|
||||||
|
var wsClient = new WebSocketClient(WS_URL);
|
||||||
|
var gameBoardElement = document.getElementById('game-board');
|
||||||
|
console.log('gameBoardElement: ', gameBoardElement);
|
||||||
|
var messagesElement = document.getElementById('messages');
|
||||||
|
var playerInfoElement = document.getElementById('player-info');
|
||||||
|
if (!gameBoardElement || !messagesElement || !playerInfoElement) {
|
||||||
|
console.error(
|
||||||
|
'Missing essential DOM elements (game-board, messages, or player-info)',
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
'Missing essential DOM elements (game-board, messages, or player-info)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var gameBoardUI = new GameBoardUI(gameBoardElement);
|
||||||
|
console.log('GameBoardUI initialized.', gameBoardUI);
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(message);
|
||||||
|
console.log('Parsed message:', msg);
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'game_state':
|
||||||
|
gameStateManager.updateGameState(msg.state);
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
console.log('Game state updated: ', gameStateManager.getGameState());
|
||||||
|
break;
|
||||||
|
case 'move_result':
|
||||||
|
if (msg.success) {
|
||||||
|
console.log('Move successful!');
|
||||||
|
} else {
|
||||||
|
console.error(`Move failed: ${msg.error}`);
|
||||||
|
gameStateManager.rollbackGameState();
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'player_joined':
|
||||||
|
console.log(`${msg.playerId} joined the game.`);
|
||||||
|
break;
|
||||||
|
case 'player_disconnected':
|
||||||
|
console.log(`${msg.playerId} disconnected.`);
|
||||||
|
break;
|
||||||
|
case 'ping':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`Unknown message type: ${msg.type}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
'Error parsing WebSocket message:',
|
||||||
|
e,
|
||||||
|
'Message was:',
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
gameBoardUI.setOnCellClick((row, col) => {
|
||||||
|
const moveMessage = {
|
||||||
|
type: 'make_move',
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
};
|
||||||
|
console.log('Sending move:', moveMessage);
|
||||||
|
wsClient.send(JSON.stringify(moveMessage));
|
||||||
|
const currentGameState = gameStateManager.getGameState();
|
||||||
|
const nextPlayer =
|
||||||
|
currentGameState.currentPlayer === 'black' ? 'white' : 'black';
|
||||||
|
const newBoard = currentGameState.board.map((rowArr) => [...rowArr]);
|
||||||
|
newBoard[row][col] = currentGameState.currentPlayer;
|
||||||
|
const optimisticState = {
|
||||||
|
...currentGameState,
|
||||||
|
board: newBoard,
|
||||||
|
currentPlayer: nextPlayer,
|
||||||
|
};
|
||||||
|
gameStateManager.updateGameState(optimisticState);
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
});
|
||||||
|
wsClient.onOpen(() => {
|
||||||
|
console.log('Connected to game server.');
|
||||||
|
const playerId = `player-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
const joinMessage = {
|
||||||
|
type: 'join_game',
|
||||||
|
gameId: 'some-game-id',
|
||||||
|
playerId,
|
||||||
|
};
|
||||||
|
wsClient.send(JSON.stringify(joinMessage));
|
||||||
|
if (playerInfoElement) {
|
||||||
|
playerInfoElement.textContent = `You are: ${playerId} (Waiting for game state...)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wsClient.onClose(() => {
|
||||||
|
console.log('Disconnected from game server. Attempting to reconnect...');
|
||||||
|
});
|
||||||
|
wsClient.onError((error) => {
|
||||||
|
console.error(
|
||||||
|
`WebSocket error: ${error instanceof ErrorEvent ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wsClient.connect();
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
if (playerInfoElement) {
|
||||||
|
playerInfoElement.textContent = `You are: (Connecting...)`;
|
||||||
|
}
|
|
@ -0,0 +1,305 @@
|
||||||
|
// src/game-client/WebSocketClient.ts
|
||||||
|
class WebSocketClient {
|
||||||
|
ws = null;
|
||||||
|
url;
|
||||||
|
messageQueue = [];
|
||||||
|
isConnected = false;
|
||||||
|
reconnectCount = 0;
|
||||||
|
options;
|
||||||
|
manualClose = false;
|
||||||
|
onMessageHandler = () => {};
|
||||||
|
onOpenHandler = () => {};
|
||||||
|
onCloseHandler = () => {};
|
||||||
|
onErrorHandler = () => {};
|
||||||
|
constructor(url, options) {
|
||||||
|
this.url = url;
|
||||||
|
this.options = {
|
||||||
|
reconnectAttempts: 5,
|
||||||
|
reconnectInterval: 3000,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
this.manualClose = false;
|
||||||
|
this.ws = new WebSocket(this.url);
|
||||||
|
this.ws.onopen = this.handleOpen.bind(this);
|
||||||
|
this.ws.onmessage = this.handleMessage.bind(this);
|
||||||
|
this.ws.onclose = this.handleClose.bind(this);
|
||||||
|
this.ws.onerror = this.handleError.bind(this);
|
||||||
|
}
|
||||||
|
send(message) {
|
||||||
|
if (this.isConnected && this.ws) {
|
||||||
|
this.ws.send(message);
|
||||||
|
} else {
|
||||||
|
this.messageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
this.manualClose = true;
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMessage(handler) {
|
||||||
|
this.onMessageHandler = handler;
|
||||||
|
}
|
||||||
|
onOpen(handler) {
|
||||||
|
this.onOpenHandler = handler;
|
||||||
|
}
|
||||||
|
onClose(handler) {
|
||||||
|
this.onCloseHandler = handler;
|
||||||
|
}
|
||||||
|
onError(handler) {
|
||||||
|
this.onErrorHandler = handler;
|
||||||
|
}
|
||||||
|
handleOpen() {
|
||||||
|
this.isConnected = true;
|
||||||
|
this.reconnectCount = 0;
|
||||||
|
this.onOpenHandler();
|
||||||
|
this.flushMessageQueue();
|
||||||
|
}
|
||||||
|
handleMessage(event) {
|
||||||
|
this.onMessageHandler(event.data);
|
||||||
|
}
|
||||||
|
handleClose(event) {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.onCloseHandler(event.code, event.reason);
|
||||||
|
if (
|
||||||
|
!this.manualClose &&
|
||||||
|
this.reconnectCount < this.options.reconnectAttempts
|
||||||
|
) {
|
||||||
|
this.reconnectCount++;
|
||||||
|
setTimeout(() => this.connect(), this.options.reconnectInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleError(event) {
|
||||||
|
this.onErrorHandler(event);
|
||||||
|
}
|
||||||
|
flushMessageQueue() {
|
||||||
|
while (this.messageQueue.length > 0 && this.isConnected && this.ws) {
|
||||||
|
const message = this.messageQueue.shift();
|
||||||
|
if (message) {
|
||||||
|
this.ws.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/game-client/GameStateManager.ts
|
||||||
|
class GameStateManager {
|
||||||
|
gameState;
|
||||||
|
stateHistory;
|
||||||
|
constructor() {
|
||||||
|
this.gameState = this.getDefaultGameState();
|
||||||
|
this.stateHistory = [];
|
||||||
|
}
|
||||||
|
getDefaultGameState() {
|
||||||
|
const emptyBoard = Array(15)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => Array(15).fill(null));
|
||||||
|
return {
|
||||||
|
id: '',
|
||||||
|
board: emptyBoard,
|
||||||
|
currentPlayer: 'black',
|
||||||
|
status: 'waiting',
|
||||||
|
winner: null,
|
||||||
|
players: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
getGameState() {
|
||||||
|
return this.gameState;
|
||||||
|
}
|
||||||
|
updateGameState(newState) {
|
||||||
|
this.stateHistory.push(JSON.parse(JSON.stringify(this.gameState)));
|
||||||
|
this.gameState = newState;
|
||||||
|
}
|
||||||
|
rollbackGameState() {
|
||||||
|
if (this.stateHistory.length > 0) {
|
||||||
|
this.gameState = this.stateHistory.pop();
|
||||||
|
} else {
|
||||||
|
console.warn('No previous state to rollback to.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/game-client/GameBoardUI.ts
|
||||||
|
class GameBoardUI {
|
||||||
|
boardElement;
|
||||||
|
cells = [];
|
||||||
|
onCellClickCallback = null;
|
||||||
|
isInteractionEnabled = true;
|
||||||
|
constructor(boardElement) {
|
||||||
|
this.boardElement = boardElement;
|
||||||
|
this.initializeBoard();
|
||||||
|
}
|
||||||
|
initializeBoard() {
|
||||||
|
this.boardElement.innerHTML = '';
|
||||||
|
this.boardElement.style.display = 'grid';
|
||||||
|
this.boardElement.style.gridTemplateColumns = 'repeat(15, 1fr)';
|
||||||
|
this.boardElement.style.width = '450px';
|
||||||
|
this.boardElement.style.height = '450px';
|
||||||
|
this.boardElement.style.border = '1px solid black';
|
||||||
|
for (let row = 0; row < 15; row++) {
|
||||||
|
this.cells[row] = [];
|
||||||
|
for (let col = 0; col < 15; col++) {
|
||||||
|
const cell = document.createElement('div');
|
||||||
|
cell.classList.add('board-cell');
|
||||||
|
cell.style.width = '30px';
|
||||||
|
cell.style.height = '30px';
|
||||||
|
cell.style.border = '1px solid #ccc';
|
||||||
|
cell.style.boxSizing = 'border-box';
|
||||||
|
cell.style.display = 'flex';
|
||||||
|
cell.style.justifyContent = 'center';
|
||||||
|
cell.style.alignItems = 'center';
|
||||||
|
cell.dataset.row = row.toString();
|
||||||
|
cell.dataset.col = col.toString();
|
||||||
|
cell.addEventListener('click', () => this.handleCellClick(row, col));
|
||||||
|
this.boardElement.appendChild(cell);
|
||||||
|
this.cells[row][col] = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateBoard(gameState) {
|
||||||
|
const board = gameState.board;
|
||||||
|
const lastMove = { row: -1, col: -1 };
|
||||||
|
for (let row = 0; row < 15; row++) {
|
||||||
|
for (let col = 0; col < 15; col++) {
|
||||||
|
const cell = this.cells[row][col];
|
||||||
|
cell.innerHTML = '';
|
||||||
|
const stone = board[row][col];
|
||||||
|
if (stone) {
|
||||||
|
const stoneElement = document.createElement('div');
|
||||||
|
stoneElement.style.width = '24px';
|
||||||
|
stoneElement.style.height = '24px';
|
||||||
|
stoneElement.style.borderRadius = '50%';
|
||||||
|
stoneElement.style.backgroundColor =
|
||||||
|
stone === 'black' ? 'black' : 'white';
|
||||||
|
stoneElement.style.border = '1px solid #333';
|
||||||
|
cell.appendChild(stoneElement);
|
||||||
|
}
|
||||||
|
cell.classList.remove('last-move');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isInteractionEnabled =
|
||||||
|
gameState.status === 'playing' && gameState.currentPlayer === 'black';
|
||||||
|
this.boardElement.style.pointerEvents = this.isInteractionEnabled
|
||||||
|
? 'auto'
|
||||||
|
: 'none';
|
||||||
|
this.boardElement.style.opacity = this.isInteractionEnabled ? '1' : '0.7';
|
||||||
|
console.log(
|
||||||
|
`Current Player: ${gameState.currentPlayer}, Status: ${gameState.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setOnCellClick(callback) {
|
||||||
|
this.onCellClickCallback = callback;
|
||||||
|
}
|
||||||
|
handleCellClick(row, col) {
|
||||||
|
if (this.isInteractionEnabled && this.onCellClickCallback) {
|
||||||
|
this.onCellClickCallback(row, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/client-entry.ts
|
||||||
|
console.log('Gomoku client entry point loaded.');
|
||||||
|
var WS_URL = process.env.WS_URL || 'ws://localhost:3000/ws';
|
||||||
|
var gameStateManager = new GameStateManager();
|
||||||
|
var wsClient = new WebSocketClient(WS_URL);
|
||||||
|
var gameBoardElement = document.getElementById('game-board');
|
||||||
|
console.log('gameBoardElement: ', gameBoardElement);
|
||||||
|
var messagesElement = document.getElementById('messages');
|
||||||
|
var playerInfoElement = document.getElementById('player-info');
|
||||||
|
if (!gameBoardElement || !messagesElement || !playerInfoElement) {
|
||||||
|
console.error(
|
||||||
|
'Missing essential DOM elements (game-board, messages, or player-info)',
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
'Missing essential DOM elements (game-board, messages, or player-info)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var gameBoardUI = new GameBoardUI(gameBoardElement);
|
||||||
|
console.log('GameBoardUI initialized.', gameBoardUI);
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(message);
|
||||||
|
console.log('Parsed message:', msg);
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'game_state':
|
||||||
|
gameStateManager.updateGameState(msg.state);
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
console.log('Game state updated: ', gameStateManager.getGameState());
|
||||||
|
break;
|
||||||
|
case 'move_result':
|
||||||
|
if (msg.success) {
|
||||||
|
console.log('Move successful!');
|
||||||
|
} else {
|
||||||
|
console.error(`Move failed: ${msg.error}`);
|
||||||
|
gameStateManager.rollbackGameState();
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'player_joined':
|
||||||
|
console.log(`${msg.playerId} joined the game.`);
|
||||||
|
break;
|
||||||
|
case 'player_disconnected':
|
||||||
|
console.log(`${msg.playerId} disconnected.`);
|
||||||
|
break;
|
||||||
|
case 'ping':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`Unknown message type: ${msg.type}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
'Error parsing WebSocket message:',
|
||||||
|
e,
|
||||||
|
'Message was:',
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
gameBoardUI.setOnCellClick((row, col) => {
|
||||||
|
const moveMessage = {
|
||||||
|
type: 'make_move',
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
};
|
||||||
|
console.log('Sending move:', moveMessage);
|
||||||
|
wsClient.send(JSON.stringify(moveMessage));
|
||||||
|
const currentGameState = gameStateManager.getGameState();
|
||||||
|
const nextPlayer =
|
||||||
|
currentGameState.currentPlayer === 'black' ? 'white' : 'black';
|
||||||
|
const newBoard = currentGameState.board.map((rowArr) => [...rowArr]);
|
||||||
|
newBoard[row][col] = currentGameState.currentPlayer;
|
||||||
|
const optimisticState = {
|
||||||
|
...currentGameState,
|
||||||
|
board: newBoard,
|
||||||
|
currentPlayer: nextPlayer,
|
||||||
|
};
|
||||||
|
gameStateManager.updateGameState(optimisticState);
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
});
|
||||||
|
wsClient.onOpen(() => {
|
||||||
|
console.log('Connected to game server.');
|
||||||
|
const playerId = `player-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
const joinMessage = {
|
||||||
|
type: 'join_game',
|
||||||
|
gameId: 'some-game-id',
|
||||||
|
playerId,
|
||||||
|
};
|
||||||
|
wsClient.send(JSON.stringify(joinMessage));
|
||||||
|
if (playerInfoElement) {
|
||||||
|
playerInfoElement.textContent = `You are: ${playerId} (Waiting for game state...)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wsClient.onClose(() => {
|
||||||
|
console.log('Disconnected from game server. Attempting to reconnect...');
|
||||||
|
});
|
||||||
|
wsClient.onError((error) => {
|
||||||
|
console.error(`WebSocket error: ${error.message}`);
|
||||||
|
});
|
||||||
|
wsClient.connect();
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
if (playerInfoElement) {
|
||||||
|
playerInfoElement.textContent = `You are: (Connecting...)`;
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Gomoku Game</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
#game-container {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#game-board {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: grid;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
.board-cell {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.board-cell:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.board-cell > div {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
.last-move {
|
||||||
|
box-shadow: 0 0 5px 3px rgba(255, 255, 0, 0.7); /* Yellow glow */
|
||||||
|
}
|
||||||
|
#messages {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
#player-info {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game-container">
|
||||||
|
<h1>Gomoku</h1>
|
||||||
|
<div id="player-info"></div>
|
||||||
|
<div id="game-board"></div>
|
||||||
|
<div id="messages"></div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="./dist/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
2
justfile
2
justfile
|
@ -10,7 +10,7 @@ dev:
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
build:
|
build:
|
||||||
bun run build
|
bun build src/client-entry.ts --outfile dist/bundle.js --define "process.env.WS_URL='ws://localhost:3000/ws'"
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
test:
|
test:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "gomoku",
|
"name": "gomoku",
|
||||||
"version": "1.0.50",
|
"version": "1.0.50",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@elysiajs/static": "^1.3.0",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
// src/client-entry.ts
|
||||||
|
|
||||||
|
import { WebSocketClient } from './game-client/WebSocketClient';
|
||||||
|
import {
|
||||||
|
GameStateManager,
|
||||||
|
GameStateType,
|
||||||
|
} from './game-client/GameStateManager';
|
||||||
|
import { GameBoardUI } from './game-client/GameBoardUI';
|
||||||
|
|
||||||
|
console.log('Gomoku client entry point loaded.');
|
||||||
|
|
||||||
|
const WS_URL = process.env.WS_URL || 'ws://localhost:3000/ws';
|
||||||
|
|
||||||
|
// Initialize components
|
||||||
|
const gameStateManager = new GameStateManager();
|
||||||
|
const wsClient = new WebSocketClient(WS_URL);
|
||||||
|
|
||||||
|
const gameBoardElement = document.getElementById('game-board');
|
||||||
|
console.log('gameBoardElement: ', gameBoardElement); // Log to check if element is found
|
||||||
|
|
||||||
|
const messagesElement = document.getElementById('messages');
|
||||||
|
const playerInfoElement = document.getElementById('player-info');
|
||||||
|
|
||||||
|
if (!gameBoardElement || !messagesElement || !playerInfoElement) {
|
||||||
|
console.error(
|
||||||
|
'Missing essential DOM elements (game-board, messages, or player-info)',
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
'Missing essential DOM elements (game-board, messages, or player-info)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameBoardUI = new GameBoardUI(gameBoardElement);
|
||||||
|
console.log('GameBoardUI initialized.', gameBoardUI); // Log to confirm GameBoardUI construction
|
||||||
|
|
||||||
|
// --- Event Handlers and Wiring ---
|
||||||
|
|
||||||
|
// WebSocketClient -> GameStateManager -> GameBoardUI
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(message);
|
||||||
|
console.log('Parsed message:', msg);
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'game_state':
|
||||||
|
gameStateManager.updateGameState(msg.state as GameStateType);
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
console.log('Game state updated: ', gameStateManager.getGameState());
|
||||||
|
break;
|
||||||
|
case 'move_result':
|
||||||
|
if (msg.success) {
|
||||||
|
console.log('Move successful!');
|
||||||
|
} else {
|
||||||
|
console.error(`Move failed: ${msg.error}`);
|
||||||
|
gameStateManager.rollbackGameState();
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState()); // Re-render after rollback
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'player_joined':
|
||||||
|
console.log(`${msg.playerId} joined the game.`);
|
||||||
|
break;
|
||||||
|
case 'player_disconnected':
|
||||||
|
console.log(`${msg.playerId} disconnected.`);
|
||||||
|
break;
|
||||||
|
case 'ping':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`Unknown message type: ${msg.type}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
'Error parsing WebSocket message:',
|
||||||
|
e,
|
||||||
|
'Message was:',
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GameBoardUI -> WebSocketClient (for making moves)
|
||||||
|
gameBoardUI.setOnCellClick((row, col) => {
|
||||||
|
const moveMessage = {
|
||||||
|
type: 'make_move',
|
||||||
|
row: row,
|
||||||
|
col: col,
|
||||||
|
};
|
||||||
|
console.log('Sending move:', moveMessage);
|
||||||
|
wsClient.send(JSON.stringify(moveMessage));
|
||||||
|
|
||||||
|
// Optimistic Update: Apply the move to local state immediately
|
||||||
|
const currentGameState = gameStateManager.getGameState();
|
||||||
|
const nextPlayer =
|
||||||
|
currentGameState.currentPlayer === 'black' ? 'white' : 'black';
|
||||||
|
const newBoard = currentGameState.board.map((rowArr) => [...rowArr]); // Deep copy board
|
||||||
|
newBoard[row][col] = currentGameState.currentPlayer; // Place stone optimistically
|
||||||
|
|
||||||
|
const optimisticState: GameStateType = {
|
||||||
|
...currentGameState,
|
||||||
|
board: newBoard,
|
||||||
|
currentPlayer: nextPlayer, // Optimistically switch turn
|
||||||
|
};
|
||||||
|
gameStateManager.updateGameState(optimisticState);
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocketClient connection status messages
|
||||||
|
wsClient.onOpen(() => {
|
||||||
|
console.log('Connected to game server.');
|
||||||
|
const playerId = `player-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
const joinMessage = {
|
||||||
|
type: 'join_game',
|
||||||
|
gameId: 'some-game-id',
|
||||||
|
playerId: playerId,
|
||||||
|
};
|
||||||
|
wsClient.send(JSON.stringify(joinMessage));
|
||||||
|
if (playerInfoElement) {
|
||||||
|
playerInfoElement.textContent = `You are: ${playerId} (Waiting for game state...)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.onClose(() => {
|
||||||
|
console.log('Disconnected from game server. Attempting to reconnect...');
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.onError((error: Event) => {
|
||||||
|
console.error(
|
||||||
|
`WebSocket error: ${error instanceof ErrorEvent ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Start Connection ---
|
||||||
|
wsClient.connect();
|
||||||
|
|
||||||
|
// Initial board render (empty board until server sends state)
|
||||||
|
gameBoardUI.updateBoard(gameStateManager.getGameState());
|
||||||
|
// Initial setup for player info
|
||||||
|
if (playerInfoElement) {
|
||||||
|
playerInfoElement.textContent = `You are: (Connecting...)`;
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
// src/game-client/GameBoardUI.ts
|
||||||
|
|
||||||
|
import { GameStateType } from './GameStateManager';
|
||||||
|
|
||||||
|
export class GameBoardUI {
|
||||||
|
private boardElement: HTMLElement;
|
||||||
|
private cells: HTMLElement[][] = [];
|
||||||
|
private onCellClickCallback: ((row: number, col: number) => void) | null =
|
||||||
|
null;
|
||||||
|
private isInteractionEnabled: boolean = true;
|
||||||
|
|
||||||
|
constructor(boardElement: HTMLElement) {
|
||||||
|
this.boardElement = boardElement;
|
||||||
|
this.initializeBoard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeBoard(): void {
|
||||||
|
this.boardElement.innerHTML = ''; // Clear existing content
|
||||||
|
this.boardElement.style.display = 'grid';
|
||||||
|
this.boardElement.style.gridTemplateColumns = 'repeat(15, 1fr)';
|
||||||
|
this.boardElement.style.width = '450px'; // 15*30px, assuming cell size
|
||||||
|
this.boardElement.style.height = '450px';
|
||||||
|
this.boardElement.style.border = '1px solid black';
|
||||||
|
|
||||||
|
for (let row = 0; row < 15; row++) {
|
||||||
|
this.cells[row] = [];
|
||||||
|
for (let col = 0; col < 15; col++) {
|
||||||
|
const cell = document.createElement('div');
|
||||||
|
cell.classList.add('board-cell');
|
||||||
|
cell.style.width = '30px';
|
||||||
|
cell.style.height = '30px';
|
||||||
|
cell.style.border = '1px solid #ccc';
|
||||||
|
cell.style.boxSizing = 'border-box';
|
||||||
|
cell.style.display = 'flex';
|
||||||
|
cell.style.justifyContent = 'center';
|
||||||
|
cell.style.alignItems = 'center';
|
||||||
|
cell.dataset.row = row.toString();
|
||||||
|
cell.dataset.col = col.toString();
|
||||||
|
cell.addEventListener('click', () => this.handleCellClick(row, col));
|
||||||
|
this.boardElement.appendChild(cell);
|
||||||
|
this.cells[row][col] = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateBoard(gameState: GameStateType): void {
|
||||||
|
const board = gameState.board;
|
||||||
|
const lastMove = { row: -1, col: -1 }; // Placeholder for last move highlighting (needs actual last move from state)
|
||||||
|
|
||||||
|
for (let row = 0; row < 15; row++) {
|
||||||
|
for (let col = 0; col < 15; col++) {
|
||||||
|
const cell = this.cells[row][col];
|
||||||
|
cell.innerHTML = ''; // Clear previous stone
|
||||||
|
|
||||||
|
const stone = board[row][col];
|
||||||
|
if (stone) {
|
||||||
|
const stoneElement = document.createElement('div');
|
||||||
|
stoneElement.style.width = '24px';
|
||||||
|
stoneElement.style.height = '24px';
|
||||||
|
stoneElement.style.borderRadius = '50%';
|
||||||
|
stoneElement.style.backgroundColor =
|
||||||
|
stone === 'black' ? 'black' : 'white';
|
||||||
|
stoneElement.style.border = '1px solid #333';
|
||||||
|
cell.appendChild(stoneElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove highlight from previous last moves
|
||||||
|
cell.classList.remove('last-move');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply highlight to the last move (this would require 'lastMove' to be part of GameStateType)
|
||||||
|
// if (lastMove.row !== -1) {
|
||||||
|
// this.cells[lastMove.row][lastMove.col].classList.add('last-move');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Disable interaction if it's not our turn or game is over
|
||||||
|
// This logic needs to know which player 'we' are, and the current player from gameState
|
||||||
|
// For simplicity, let's assume 'black' is the client for now, and enable/disable
|
||||||
|
// based on if it's black's turn. This will need refinement for multi-player.
|
||||||
|
this.isInteractionEnabled =
|
||||||
|
gameState.status === 'playing' && gameState.currentPlayer === 'black'; // Simplified for now
|
||||||
|
this.boardElement.style.pointerEvents = this.isInteractionEnabled
|
||||||
|
? 'auto'
|
||||||
|
: 'none';
|
||||||
|
this.boardElement.style.opacity = this.isInteractionEnabled ? '1' : '0.7';
|
||||||
|
|
||||||
|
// Update turn indicator and status (these elements would need to be passed in or managed by a parent UI component)
|
||||||
|
console.log(
|
||||||
|
`Current Player: ${gameState.currentPlayer}, Status: ${gameState.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setOnCellClick(callback: (row: number, col: number) => void): void {
|
||||||
|
this.onCellClickCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCellClick(row: number, col: number): void {
|
||||||
|
if (this.isInteractionEnabled && this.onCellClickCallback) {
|
||||||
|
this.onCellClickCallback(row, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
import { expect, test, describe, beforeEach, afterEach, mock } from 'bun:test';
|
import { expect, test, describe, beforeEach, afterEach, mock } from 'bun:test';
|
||||||
import { GameStateManager } from './GameStateManager';
|
import { GameStateManager, GameStateType } from './GameStateManager';
|
||||||
|
|
||||||
describe('GameStateManager', () => {
|
describe('GameStateManager', () => {
|
||||||
let gameStateManager: GameStateManager;
|
let gameStateManager: GameStateManager;
|
||||||
|
@ -25,9 +24,9 @@ describe('GameStateManager', () => {
|
||||||
test('should update game state from server updates', () => {
|
test('should update game state from server updates', () => {
|
||||||
const serverState = {
|
const serverState = {
|
||||||
id: 'game123',
|
id: 'game123',
|
||||||
board: Array(15).fill(Array(15).fill(null)),
|
board: Array.from({ length: 15 }, () => Array(15).fill(null)),
|
||||||
currentPlayer: 'white',
|
currentPlayer: 'white' as 'black' | 'white',
|
||||||
status: 'playing',
|
status: 'playing' as 'waiting' | 'playing' | 'finished',
|
||||||
winner: null,
|
winner: null,
|
||||||
players: { black: 'playerA', white: 'playerB' },
|
players: { black: 'playerA', white: 'playerB' },
|
||||||
};
|
};
|
||||||
|
@ -36,14 +35,14 @@ describe('GameStateManager', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle optimistic updates for making a move', () => {
|
test('should handle optimistic updates for making a move', () => {
|
||||||
const initialBoard = Array(15).fill(Array(15).fill(null));
|
const initialBoard = Array.from({ length: 15 }, () => Array(15).fill(null));
|
||||||
initialBoard[7][7] = 'black'; // Simulate an optimistic move
|
initialBoard[7][7] = 'black'; // Simulate an optimistic move
|
||||||
|
|
||||||
const optimisticState = {
|
const optimisticState = {
|
||||||
id: 'game123',
|
id: 'game123',
|
||||||
board: initialBoard,
|
board: initialBoard,
|
||||||
currentPlayer: 'white', // Turn changes optimistically
|
currentPlayer: 'white' as 'black' | 'white', // Turn changes optimistically
|
||||||
status: 'playing',
|
status: 'playing' as 'waiting' | 'playing' | 'finished',
|
||||||
winner: null,
|
winner: null,
|
||||||
players: { black: 'playerA', white: 'playerB' },
|
players: { black: 'playerA', white: 'playerB' },
|
||||||
};
|
};
|
||||||
|
@ -56,22 +55,24 @@ describe('GameStateManager', () => {
|
||||||
test('should rollback optimistic updates if server rejects move', () => {
|
test('should rollback optimistic updates if server rejects move', () => {
|
||||||
const initialServerState = {
|
const initialServerState = {
|
||||||
id: 'game123',
|
id: 'game123',
|
||||||
board: Array(15).fill(Array(15).fill(null)),
|
board: Array.from({ length: 15 }, () => Array(15).fill(null)),
|
||||||
currentPlayer: 'black',
|
currentPlayer: 'black' as 'black' | 'white',
|
||||||
status: 'playing',
|
status: 'playing' as 'waiting' | 'playing' | 'finished',
|
||||||
winner: null,
|
winner: null,
|
||||||
players: { black: 'playerA', white: 'playerB' },
|
players: { black: 'playerA', white: 'playerB' },
|
||||||
};
|
};
|
||||||
gameStateManager.updateGameState(initialServerState);
|
gameStateManager.updateGameState(initialServerState);
|
||||||
|
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
const optimisticBoard = Array(15).fill(Array(15).fill(null));
|
const optimisticBoard = Array.from({ length: 15 }, () =>
|
||||||
|
Array(15).fill(null),
|
||||||
|
);
|
||||||
optimisticBoard[7][7] = 'black';
|
optimisticBoard[7][7] = 'black';
|
||||||
const optimisticState = {
|
const optimisticState = {
|
||||||
id: 'game123',
|
id: 'game123',
|
||||||
board: optimisticBoard,
|
board: optimisticBoard,
|
||||||
currentPlayer: 'white',
|
currentPlayer: 'white' as 'black' | 'white',
|
||||||
status: 'playing',
|
status: 'playing' as 'waiting' | 'playing' | 'finished',
|
||||||
winner: null,
|
winner: null,
|
||||||
players: { black: 'playerA', white: 'playerB' },
|
players: { black: 'playerA', white: 'playerB' },
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ export interface GameStateType {
|
||||||
currentPlayer: 'black' | 'white';
|
currentPlayer: 'black' | 'white';
|
||||||
status: 'waiting' | 'playing' | 'finished';
|
status: 'waiting' | 'playing' | 'finished';
|
||||||
winner: null | 'black' | 'white' | 'draw';
|
winner: null | 'black' | 'white' | 'draw';
|
||||||
players: { black?: string, white?: string };
|
players: { black?: string; white?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GameStateManager {
|
export class GameStateManager {
|
||||||
|
@ -17,7 +17,9 @@ export class GameStateManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultGameState(): GameStateType {
|
private getDefaultGameState(): GameStateType {
|
||||||
const emptyBoard: (null | 'black' | 'white')[][] = Array(15).fill(null).map(() => Array(15).fill(null));
|
const emptyBoard: (null | 'black' | 'white')[][] = Array(15)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => Array(15).fill(null));
|
||||||
return {
|
return {
|
||||||
id: '',
|
id: '',
|
||||||
board: emptyBoard,
|
board: emptyBoard,
|
||||||
|
@ -43,7 +45,7 @@ export class GameStateManager {
|
||||||
if (this.stateHistory.length > 0) {
|
if (this.stateHistory.length > 0) {
|
||||||
this.gameState = this.stateHistory.pop()!;
|
this.gameState = this.stateHistory.pop()!;
|
||||||
} else {
|
} else {
|
||||||
console.warn("No previous state to rollback to.");
|
console.warn('No previous state to rollback to.');
|
||||||
// Optionally, throw an error or reset to default state here
|
// Optionally, throw an error or reset to default state here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,20 @@ import { WebSocketClient } from './WebSocketClient';
|
||||||
|
|
||||||
// Define MockWebSocket as a regular class
|
// Define MockWebSocket as a regular class
|
||||||
class MockWebSocket {
|
class MockWebSocket {
|
||||||
onopen: ((this: WebSocket, ev: Event) => any) | null = null;
|
static CONNECTING = 0;
|
||||||
onmessage: ((this: WebSocket, ev: MessageEvent<any>) => any) | null = null;
|
static OPEN = 1;
|
||||||
onclose: ((this: WebSocket, ev: CloseEvent) => any) | null = null;
|
static CLOSING = 2;
|
||||||
onerror: ((this: WebSocket, ev: Event) => any) | null = null;
|
static CLOSED = 3;
|
||||||
|
|
||||||
|
constructor(url?: string) {
|
||||||
|
// In a real scenario, you might do something with the URL
|
||||||
|
// For this mock, we just need to accept it.
|
||||||
|
}
|
||||||
|
|
||||||
|
onopen: ((this: any, ev: Event) => any) | null = null;
|
||||||
|
onmessage: ((this: any, ev: MessageEvent<any>) => any) | null = null;
|
||||||
|
onclose: ((this: any, ev: CloseEvent) => any) | null = null;
|
||||||
|
onerror: ((this: any, ev: Event) => any) | null = null;
|
||||||
readyState: number = 0; // 0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED
|
readyState: number = 0; // 0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED
|
||||||
|
|
||||||
// Using a plain function for send/close to simplify.
|
// Using a plain function for send/close to simplify.
|
||||||
|
@ -54,7 +64,7 @@ describe('WebSocketClient', () => {
|
||||||
const url = 'ws://localhost:8080';
|
const url = 'ws://localhost:8080';
|
||||||
|
|
||||||
// Using a mock function to wrap the actual global WebSocket constructor
|
// Using a mock function to wrap the actual global WebSocket constructor
|
||||||
let globalWebSocketConstructorMock: ReturnType<typeof mock>;
|
let globalWebSocketConstructorMock: typeof WebSocket;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Clear instances and reset mocks before each test
|
// Clear instances and reset mocks before each test
|
||||||
|
@ -67,18 +77,13 @@ describe('WebSocketClient', () => {
|
||||||
const instance = new MockWebSocket(url);
|
const instance = new MockWebSocket(url);
|
||||||
createdMockWebSockets.push(instance);
|
createdMockWebSockets.push(instance);
|
||||||
|
|
||||||
// Wrap send and close methods in mock functions for call tracking
|
|
||||||
// We do this directly on the instance for each new WebSocket.
|
|
||||||
// Using mock() directly on the method reference, and binding 'this'
|
|
||||||
// to ensure it operates in the context of the instance.
|
|
||||||
(instance as any).send = mock(instance.send.bind(instance));
|
(instance as any).send = mock(instance.send.bind(instance));
|
||||||
(instance as any).close = mock(instance.close.bind(instance));
|
(instance as any).close = mock(instance.close.bind(instance));
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}) as any; // Cast as any to satisfy TypeScript for global assignment
|
}) as unknown as typeof WebSocket;
|
||||||
|
|
||||||
global.WebSocket = globalWebSocketConstructorMock;
|
global.WebSocket = globalWebSocketConstructorMock;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -165,21 +170,28 @@ describe('WebSocketClient', () => {
|
||||||
client.send('queued message 2');
|
client.send('queued message 2');
|
||||||
|
|
||||||
// Simulate reconnection after a short delay
|
// Simulate reconnection after a short delay
|
||||||
await new Promise(resolve => setTimeout(resolve, 20)); // Allow for reconnectInterval
|
await new Promise((resolve) => setTimeout(resolve, 20)); // Allow for reconnectInterval
|
||||||
expect(createdMockWebSockets.length).toBe(2); // New instance created for reconnection
|
expect(createdMockWebSockets.length).toBe(2); // New instance created for reconnection
|
||||||
createdMockWebSockets[1]._simulateOpen(); // Simulate new connection opening
|
createdMockWebSockets[1]._simulateOpen(); // Simulate new connection opening
|
||||||
|
|
||||||
// Wait for messages to be flushed
|
// Wait for messages to be flushed
|
||||||
await new Promise(resolve => setTimeout(resolve, 5));
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
|
||||||
expect(createdMockWebSockets[1].send).toHaveBeenCalledWith('queued message 1');
|
expect(createdMockWebSockets[1].send).toHaveBeenCalledWith(
|
||||||
expect(createdMockWebSockets[1].send).toHaveBeenCalledWith('queued message 2');
|
'queued message 1',
|
||||||
|
);
|
||||||
|
expect(createdMockWebSockets[1].send).toHaveBeenCalledWith(
|
||||||
|
'queued message 2',
|
||||||
|
);
|
||||||
|
|
||||||
expect(onOpenMock).toHaveBeenCalledTimes(1); // onOpen should be called on successful reconnection
|
expect(onOpenMock).toHaveBeenCalledTimes(1); // onOpen should be called on successful reconnection
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not attempt to reconnect if explicitly closed', async () => {
|
it('should not attempt to reconnect if explicitly closed', async () => {
|
||||||
client = new WebSocketClient(url, { reconnectAttempts: 3, reconnectInterval: 10 });
|
client = new WebSocketClient(url, {
|
||||||
|
reconnectAttempts: 3,
|
||||||
|
reconnectInterval: 10,
|
||||||
|
});
|
||||||
const onCloseMock = mock(() => {});
|
const onCloseMock = mock(() => {});
|
||||||
client.onClose(onCloseMock);
|
client.onClose(onCloseMock);
|
||||||
|
|
||||||
|
@ -188,10 +200,9 @@ describe('WebSocketClient', () => {
|
||||||
client.close(); // Explicitly close
|
client.close(); // Explicitly close
|
||||||
|
|
||||||
// Allow some time for potential reconnect attempts. If no new WebSocket is created after the attempts would have happened, then we know it's not reconnecting.
|
// Allow some time for potential reconnect attempts. If no new WebSocket is created after the attempts would have happened, then we know it's not reconnecting.
|
||||||
await new Promise(resolve => setTimeout(resolve, 50)); // (reconnectAttempts * reconnectInterval) + buffer
|
await new Promise((resolve) => setTimeout(resolve, 50)); // (reconnectAttempts * reconnectInterval) + buffer
|
||||||
|
|
||||||
expect(createdMockWebSockets.length).toBe(1); // Only the initial one
|
expect(createdMockWebSockets.length).toBe(1); // Only the initial one
|
||||||
expect(onCloseMock).toHaveBeenCalledTimes(1); // onClose should be called
|
expect(onCloseMock).toHaveBeenCalledTimes(1); // onClose should be called
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,7 +85,10 @@ export class WebSocketClient {
|
||||||
private handleClose(event: CloseEvent): void {
|
private handleClose(event: CloseEvent): void {
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
this.onCloseHandler(event.code, event.reason);
|
this.onCloseHandler(event.code, event.reason);
|
||||||
if (!this.manualClose && this.reconnectCount < this.options.reconnectAttempts!) {
|
if (
|
||||||
|
!this.manualClose &&
|
||||||
|
this.reconnectCount < this.options.reconnectAttempts!
|
||||||
|
) {
|
||||||
this.reconnectCount++;
|
this.reconnectCount++;
|
||||||
setTimeout(() => this.connect(), this.options.reconnectInterval);
|
setTimeout(() => this.connect(), this.options.reconnectInterval);
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,7 +184,9 @@ describe('WebSocketHandler', () => {
|
||||||
const webSocketHandler = new WebSocketHandler(gameManager);
|
const webSocketHandler = new WebSocketHandler(gameManager);
|
||||||
|
|
||||||
// Player 1
|
// Player 1
|
||||||
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = { request: {} };
|
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = {
|
||||||
|
request: {},
|
||||||
|
};
|
||||||
const mockWs1: any = {
|
const mockWs1: any = {
|
||||||
send: mock(() => {}),
|
send: mock(() => {}),
|
||||||
on: mock((event: string, callback: Function) => {
|
on: mock((event: string, callback: Function) => {
|
||||||
|
@ -197,7 +199,9 @@ describe('WebSocketHandler', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Player 2
|
// Player 2
|
||||||
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = { request: {} };
|
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = {
|
||||||
|
request: {},
|
||||||
|
};
|
||||||
const mockWs2: any = {
|
const mockWs2: any = {
|
||||||
send: mock(() => {}),
|
send: mock(() => {}),
|
||||||
on: mock((event: string, callback: Function) => {
|
on: mock((event: string, callback: Function) => {
|
||||||
|
@ -223,13 +227,23 @@ describe('WebSocketHandler', () => {
|
||||||
|
|
||||||
// Player 1 joins, creates game
|
// Player 1 joins, creates game
|
||||||
webSocketHandler.handleConnection(mockWs1, mockWs1.data.request);
|
webSocketHandler.handleConnection(mockWs1, mockWs1.data.request);
|
||||||
triggerMessageForWs(mockWs1, JSON.stringify({ type: 'join_game', playerId: 'player1' }));
|
triggerMessageForWs(
|
||||||
|
mockWs1,
|
||||||
|
JSON.stringify({ type: 'join_game', playerId: 'player1' }),
|
||||||
|
);
|
||||||
mockWs1.data.gameId = mockWsData1.gameId;
|
mockWs1.data.gameId = mockWsData1.gameId;
|
||||||
mockWs1.data.playerId = 'player1';
|
mockWs1.data.playerId = 'player1';
|
||||||
|
|
||||||
// Player 2 joins same game
|
// Player 2 joins same game
|
||||||
webSocketHandler.handleConnection(mockWs2, mockWs2.data.request);
|
webSocketHandler.handleConnection(mockWs2, mockWs2.data.request);
|
||||||
triggerMessageForWs(mockWs2, JSON.stringify({ type: 'join_game', gameId: mockWsData1.gameId, playerId: 'player2' }));
|
triggerMessageForWs(
|
||||||
|
mockWs2,
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'join_game',
|
||||||
|
gameId: mockWsData1.gameId,
|
||||||
|
playerId: 'player2',
|
||||||
|
}),
|
||||||
|
);
|
||||||
mockWs2.data.gameId = mockWsData1.gameId;
|
mockWs2.data.gameId = mockWsData1.gameId;
|
||||||
mockWs2.data.playerId = 'player2';
|
mockWs2.data.playerId = 'player2';
|
||||||
|
|
||||||
|
@ -246,15 +260,21 @@ describe('WebSocketHandler', () => {
|
||||||
|
|
||||||
// Verify connections map is updated (Player 2 removed)
|
// Verify connections map is updated (Player 2 removed)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(webSocketHandler.connections.get(mockWsData1.gameId)).toContain(mockWs1);
|
expect(webSocketHandler.connections.get(mockWsData1.gameId)).toContain(
|
||||||
|
mockWs1,
|
||||||
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(webSocketHandler.connections.get(mockWsData1.gameId)).not.toContain(mockWs2);
|
expect(webSocketHandler.connections.get(mockWsData1.gameId)).not.toContain(
|
||||||
|
mockWs2,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
it('should broadcast game state to other players when a new player joins', () => {
|
it('should broadcast game state to other players when a new player joins', () => {
|
||||||
const gameManager = new GameManager();
|
const gameManager = new GameManager();
|
||||||
const webSocketHandler = new WebSocketHandler(gameManager);
|
const webSocketHandler = new WebSocketHandler(gameManager);
|
||||||
|
|
||||||
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = { request: {} };
|
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = {
|
||||||
|
request: {},
|
||||||
|
};
|
||||||
const mockWs1: any = {
|
const mockWs1: any = {
|
||||||
send: mock(() => {}),
|
send: mock(() => {}),
|
||||||
on: mock((event: string, callback: Function) => {
|
on: mock((event: string, callback: Function) => {
|
||||||
|
@ -264,7 +284,9 @@ describe('WebSocketHandler', () => {
|
||||||
data: mockWsData1,
|
data: mockWsData1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = { request: {} };
|
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = {
|
||||||
|
request: {},
|
||||||
|
};
|
||||||
const mockWs2: any = {
|
const mockWs2: any = {
|
||||||
send: mock(() => {}),
|
send: mock(() => {}),
|
||||||
on: mock((event: string, callback: Function) => {
|
on: mock((event: string, callback: Function) => {
|
||||||
|
@ -313,7 +335,9 @@ describe('WebSocketHandler', () => {
|
||||||
const gameManager = new GameManager();
|
const gameManager = new GameManager();
|
||||||
const webSocketHandler = new WebSocketHandler(gameManager);
|
const webSocketHandler = new WebSocketHandler(gameManager);
|
||||||
|
|
||||||
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = { request: {} };
|
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = {
|
||||||
|
request: {},
|
||||||
|
};
|
||||||
const mockWs1: any = {
|
const mockWs1: any = {
|
||||||
send: mock(() => {}),
|
send: mock(() => {}),
|
||||||
on: mock((event: string, callback: Function) => {
|
on: mock((event: string, callback: Function) => {
|
||||||
|
@ -323,7 +347,9 @@ describe('WebSocketHandler', () => {
|
||||||
data: mockWsData1,
|
data: mockWsData1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = { request: {} };
|
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = {
|
||||||
|
request: {},
|
||||||
|
};
|
||||||
const mockWs2: any = {
|
const mockWs2: any = {
|
||||||
send: mock(() => {}),
|
send: mock(() => {}),
|
||||||
on: mock((event: string, callback: Function) => {
|
on: mock((event: string, callback: Function) => {
|
||||||
|
|
|
@ -23,22 +23,19 @@ export class WebSocketHandler {
|
||||||
|
|
||||||
public handleConnection(ws: any, req: any): void {
|
public handleConnection(ws: any, req: any): void {
|
||||||
console.log('WebSocket connected');
|
console.log('WebSocket connected');
|
||||||
|
|
||||||
ws.on('message', (message: string) => {
|
|
||||||
this.handleMessage(ws, message);
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on('close', () => {
|
|
||||||
console.log('WebSocket disconnected');
|
|
||||||
this.handleDisconnect(ws);
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on('error', (error: Error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleMessage(ws: any, message: string): void {
|
public handleError(ws: any, error: Error): void {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
// Optionally send an error message to the client
|
||||||
|
if (ws) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({ type: 'error', error: 'Server-side WebSocket error' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleMessage(ws: any, message: string): void {
|
||||||
try {
|
try {
|
||||||
const parsedMessage: WebSocketMessage = JSON.parse(message);
|
const parsedMessage: WebSocketMessage = JSON.parse(message);
|
||||||
console.log('Received message:', parsedMessage);
|
console.log('Received message:', parsedMessage);
|
||||||
|
@ -111,7 +108,8 @@ export class WebSocketHandler {
|
||||||
ws.send(gameStateMessage);
|
ws.send(gameStateMessage);
|
||||||
// Notify other players if any
|
// Notify other players if any
|
||||||
this.connections.get(game.id)?.forEach((playerWs: any) => {
|
this.connections.get(game.id)?.forEach((playerWs: any) => {
|
||||||
if (playerWs !== ws) { // Don't send back to the player who just joined
|
if (playerWs !== ws) {
|
||||||
|
// Don't send back to the player who just joined
|
||||||
playerWs.send(gameStateMessage);
|
playerWs.send(gameStateMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -187,7 +185,7 @@ export class WebSocketHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDisconnect(ws: any): void {
|
public handleDisconnect(ws: any): void {
|
||||||
const gameId = ws.data.gameId;
|
const gameId = ws.data.gameId;
|
||||||
const playerId = ws.data.playerId;
|
const playerId = ws.data.playerId;
|
||||||
|
|
||||||
|
@ -195,7 +193,10 @@ export class WebSocketHandler {
|
||||||
// Remove disconnected player's websocket from connections
|
// Remove disconnected player's websocket from connections
|
||||||
const connectionsInGame = this.connections.get(gameId);
|
const connectionsInGame = this.connections.get(gameId);
|
||||||
if (connectionsInGame) {
|
if (connectionsInGame) {
|
||||||
this.connections.set(gameId, connectionsInGame.filter((conn: any) => conn !== ws));
|
this.connections.set(
|
||||||
|
gameId,
|
||||||
|
connectionsInGame.filter((conn: any) => conn !== ws),
|
||||||
|
);
|
||||||
if (this.connections.get(gameId)?.length === 0) {
|
if (this.connections.get(gameId)?.length === 0) {
|
||||||
this.connections.delete(gameId); // Clean up if no players left
|
this.connections.delete(gameId); // Clean up if no players left
|
||||||
}
|
}
|
||||||
|
|
51
src/index.ts
51
src/index.ts
|
@ -1,28 +1,55 @@
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
import { GameManager } from './game/GameManager';
|
import { staticPlugin } from '@elysiajs/static';
|
||||||
import { WebSocketHandler } from './game/WebSocketHandler';
|
import { WebSocketHandler } from './game/WebSocketHandler';
|
||||||
|
import { GameManager } from './game/GameManager';
|
||||||
|
import { GameInstance } from './game/GameInstance'; // Make sure GameInstance is accessible if GameInstance.addPlayer is used directly
|
||||||
|
|
||||||
|
// Initialize GameManager (server-side)
|
||||||
const gameManager = new GameManager();
|
const gameManager = new GameManager();
|
||||||
const webSocketHandler = new WebSocketHandler(gameManager);
|
|
||||||
|
// Initialize WebSocketHandler with the gameManager
|
||||||
|
const wsHandler = new WebSocketHandler(gameManager);
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
staticPlugin({
|
||||||
|
assets: 'dist', // Serve static files from the dist directory
|
||||||
|
prefix: '/dist', // Serve them under the /dist path
|
||||||
|
}),
|
||||||
|
)
|
||||||
.ws('/ws', {
|
.ws('/ws', {
|
||||||
open(ws: any) {
|
open(ws) {
|
||||||
webSocketHandler.handleConnection(ws, ws.data.request);
|
// Call the handler's connection logic
|
||||||
|
// Elysia's ws context directly provides the ws object
|
||||||
|
wsHandler.handleConnection(ws as any, {});
|
||||||
},
|
},
|
||||||
message(ws: any, message: any) {
|
message(ws, message) {
|
||||||
// This is handled inside WebSocketHandler.handleMessage
|
let msgString: string;
|
||||||
|
if (message instanceof Buffer) {
|
||||||
|
msgString = message.toString();
|
||||||
|
} else if (typeof message === 'object') {
|
||||||
|
// If Elysia already parsed it to an object, stringify it
|
||||||
|
msgString = JSON.stringify(message);
|
||||||
|
} else {
|
||||||
|
msgString = message as string;
|
||||||
|
}
|
||||||
|
wsHandler.handleMessage(ws as any, msgString);
|
||||||
},
|
},
|
||||||
close(ws: any) {
|
close(ws) {
|
||||||
// This is handled inside WebSocketHandler.handleDisconnect
|
// Call the handler's disconnection logic
|
||||||
|
wsHandler.handleDisconnect(ws as any);
|
||||||
},
|
},
|
||||||
err(ws: any, error: any, code: number, message: string) {
|
error(context: any) {
|
||||||
// This is handled inside WebSocketHandler.handleConnection
|
// Call the handler's error logic
|
||||||
|
wsHandler.handleError(context.ws as any, context.error);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.get('/', () => 'Hello Elysia')
|
.get('/', () => Bun.file('index.html'));
|
||||||
.listen(3000);
|
|
||||||
|
|
||||||
|
app.listen(3000, () => {
|
||||||
console.log(
|
console.log(
|
||||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
|
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Elysia server started!');
|
||||||
|
|
Loading…
Reference in New Issue