forked from Ryukaki/ThressGame
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
209 lines (172 loc) · 7.67 KB
/
server.js
File metadata and controls
209 lines (172 loc) · 7.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const path = require('path');
const { GameManager } = require('./gameManager');
const { serializeBoardForClient, getPublicPlayer } = require('./gameController');
const { handleCreateRoom, handleJoinRoom, handleJoinBot, handleListRooms } = require('./handlers/joinHandler');
const { handleMove } = require('./handlers/moveHandler');
const { handleDisconnect, handleResign, handleQuietResign, handleResume } = require('./handlers/playerHandlers');
const { handleSpectateRoom, handleDisableSpectating, handleSpectatorDisconnect } = require('./handlers/spectatorHandler');
const { createMutatorHandlers } = require('./handlers/mutatorHandler');
const { addBotToRoom, scheduleBotMove, generateBotTarget } = require('./botManager');
const { formatBasePath, buildSocketPath, registerConfigRoute } = require('./utils/config');
const turnClock = require('./utils/turnClock');
const { autoResignOnTimeout } = require('./utils/gameLifecycle');
// Routes
const { setupApiRoutes } = require('./routes/apiRoutes');
// --- Configuration -----------------------------------------------------------
const PORT = process.env.PORT || 3000;
const BASE_PATH = formatBasePath(process.env.BASE_PATH);
const SOCKET_PATH = buildSocketPath(BASE_PATH);
// --- Express Setup -----------------------------------------------------------
const app = express();
// cPanel/Passenger subfolder support: strip BASE_PATH prefix from incoming URLs
if (BASE_PATH !== '/') {
app.use((req, _res, next) => {
if (req.url.startsWith(BASE_PATH)) {
req.url = req.url.slice(BASE_PATH.length) || '/';
}
next();
});
}
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Config route (exposes BASE_PATH and SOCKET_PATH to the client)
registerConfigRoute(app, BASE_PATH, SOCKET_PATH);
// API routes (health check, etc.) -- before static files
app.use('/api', setupApiRoutes());
// --- HTTP Server & Socket.IO -------------------------------------------------
const httpServer = createServer(app);
const io = new Server(httpServer, {
path: SOCKET_PATH,
cors: {
origin: true,
credentials: true,
},
transports: ['polling', 'websocket'],
pingTimeout: 120000,
pingInterval: 25000,
});
// --- Game Infrastructure -----------------------------------------------------
const gameManager = new GameManager();
// When a turn timer expires, auto-resign the staller
turnClock.setTimeoutResignHandler((room, io, stallingColor) => {
autoResignOnTimeout(room, io, gameManager, stallingColor);
});
// Static files
// URL rewriting middleware above strips BASE_PATH, so serve at '/'
const STATIC_DIR = path.join(__dirname, 'public');
// Serve index.html with the current package version injected as a cache-buster
// on the main.js script tag and the footer version label, so browsers always
// fetch the latest module bundle after a deploy without a manual hard-refresh.
const fs = require('fs');
const APP_VERSION = require('./package.json').version;
const INDEX_PATH = path.join(STATIC_DIR, 'index.html');
function serveIndex(_req, res) {
fs.readFile(INDEX_PATH, 'utf8', (err, html) => {
if (err) return res.status(500).send('index unavailable');
const stamped = html.replace(/main\.js\?v=[^"']+/g, `main.js?v=${APP_VERSION}`);
res.set('Cache-Control', 'no-store');
res.type('html').send(stamped);
});
}
app.get('/', serveIndex);
app.get('/index.html', serveIndex);
app.use(express.static(STATIC_DIR, { index: ['index.html'] }));
/**
* Start a game when both players are in the room.
*/
function startGame(room) {
room.startGame();
const boardState = serializeBoardForClient(room.chess);
io.to(room.roomCode).emit('gameStarted', {
board: boardState,
white: getPublicPlayer(room.white),
black: getPublicPlayer(room.black),
});
// Start turn clock for the first player (skipped for bot games)
turnClock.startClock(room, io);
// If bot goes first (white), schedule its move
scheduleBotMove(room, io, gameManager, handleMove, botAutoMutatorResponse);
}
/**
* Add a bot opponent to a room.
*/
function addBot(room, color) {
const bot = addBotToRoom(room, color);
gameManager.setSocketRoom(bot.socketId, room.roomCode);
}
/**
* Broadcast the updated public waiting rooms list to all connected clients.
*/
function broadcastRoomUpdate() {
io.to('lobby').emit('roomsList', {
waiting: gameManager.getPublicWaitingRooms(),
active: gameManager.getSpectatableRooms(),
});
}
// Initialize mutator handlers with dependencies
const { botAutoMutatorResponse, registerSocketHandlers: registerMutatorHandlers } =
createMutatorHandlers({ handleMove, scheduleBotMove, generateBotTarget });
// --- Socket.IO Rate Limiting -------------------------------------------------
function createRateLimiter(maxPerWindow, windowMs) {
const counts = new Map();
setInterval(() => counts.clear(), windowMs);
return function rateLimit(socket, next) {
const count = (counts.get(socket.id) || 0) + 1;
counts.set(socket.id, count);
if (count > maxPerWindow) return;
next();
};
}
const socketRateLimit = createRateLimiter(60, 10_000); // 60 events per 10s
// --- Socket.IO Connection Handler --------------------------------------------
io.on('connection', (socket) => {
// New connections start in the lobby for scoped broadcasts
socket.join('lobby');
// Rate-limit all incoming events
socket.use((event, next) => socketRateLimit(socket, next));
// Room management
socket.on('createRoom', (data) => handleCreateRoom(io, socket, gameManager, data, broadcastRoomUpdate));
socket.on('joinRoom', (data) => handleJoinRoom(io, socket, gameManager, data, startGame, broadcastRoomUpdate));
socket.on('joinBot', (data) => handleJoinBot(io, socket, gameManager, data, startGame, addBot));
socket.on('listRooms', () => handleListRooms(socket, gameManager));
socket.on('joinLobby', () => socket.join('lobby'));
// Spectating
socket.on('spectateRoom', (data) => handleSpectateRoom(io, socket, gameManager, data));
socket.on('disableSpectating', () => handleDisableSpectating(io, socket, gameManager));
// Game actions
socket.on('move', (data) => {
handleMove(io, socket, gameManager, data);
// After human move, schedule bot response if opponent is bot
const room = gameManager.getRoomForSocket(socket.id);
if (room && room.status === 'active') {
scheduleBotMove(room, io, gameManager, handleMove, botAutoMutatorResponse);
}
// Bot auto-responds to mutator prompts (with humanizing delay)
if (room && room.mutatorState) {
setTimeout(() => botAutoMutatorResponse(room, io, gameManager), 1200 + Math.random() * 600);
}
});
socket.on('resign', () => handleResign(io, socket, gameManager, broadcastRoomUpdate));
socket.on('quietResign', () => handleQuietResign(io, socket, gameManager, broadcastRoomUpdate));
// Mutator events (selectMutator, mutatorActionResponse, rpsChoice, coinFlipChoice, coinFlipStart)
registerMutatorHandlers(socket, io, gameManager);
// Session
socket.on('resumeSession', (data) => handleResume(io, socket, gameManager, data));
// Disconnect
socket.on('disconnect', () => {
handleSpectatorDisconnect(io, socket.id, gameManager);
handleDisconnect(io, socket, gameManager, broadcastRoomUpdate);
});
});
// --- Periodic Cleanup --------------------------------------------------------
setInterval(() => gameManager.cleanupOldRooms(), 5 * 60 * 1000);
// --- Initialization ----------------------------------------------------------
// Start HTTP server
httpServer.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Socket.IO path: ${SOCKET_PATH}`);
console.log(`Base path: ${BASE_PATH}`);
});