238 lines
8.7 KiB
JavaScript
238 lines
8.7 KiB
JavaScript
const express = require('express');
|
|
const http = require('http');
|
|
const { Server } = require('socket.io');
|
|
const bodyParser = require('body-parser');
|
|
const cors = require('cors');
|
|
|
|
const app = express();
|
|
app.use(cors());
|
|
app.use(bodyParser.json());
|
|
|
|
const server = http.createServer(app);
|
|
const io = new Server(server, { cors: { origin: '*' } });
|
|
|
|
const PORT = process.env.PORT || 4000;
|
|
|
|
// simple in-memory storage (not persisted)
|
|
const games = [];
|
|
// map socket.id -> { gameId, playerId }
|
|
const socketMap = new Map();
|
|
|
|
app.post('/api/create', (req, res) => {
|
|
console.log('[backend] POST /api/create received', req.body && { name: req.body.name, owner: req.body.ownerNickname });
|
|
const id = 'game-' + Math.random().toString(36).slice(2,9);
|
|
const { name, pass, ownerNickname, ownerColorChoice } = req.body || {};
|
|
const g = {
|
|
id,
|
|
name: name || id,
|
|
pass: pass || null,
|
|
created: Date.now(),
|
|
players: [],
|
|
started: false,
|
|
options: { startWhenTwo: true }
|
|
};
|
|
// add owner as first player if provided
|
|
let ownerPlayer = null;
|
|
if(ownerNickname){
|
|
ownerPlayer = { id: 'p-' + Math.random().toString(36).slice(2,6), nickname: ownerNickname, colorChoice: ownerColorChoice || 'random', colorAssigned: null };
|
|
g.players.push(ownerPlayer);
|
|
// explicit owner id for robust ownership checks
|
|
g.ownerId = ownerPlayer.id;
|
|
}
|
|
games.push(g);
|
|
io.emit('games-list', games);
|
|
// return game and owner player if present so frontend can persist player id
|
|
if(ownerPlayer) return res.json({ ok: true, game: g, player: ownerPlayer });
|
|
res.json({ ok: true, game: g });
|
|
});
|
|
|
|
app.get('/api/list', (req, res) => {
|
|
// remove any orphan games (no players)
|
|
for (let i = games.length - 1; i >= 0; i--) {
|
|
const g = games[i];
|
|
if(!g.players || g.players.length === 0){
|
|
games.splice(i, 1);
|
|
}
|
|
}
|
|
res.json(games);
|
|
});
|
|
|
|
// remove previously created test games (names used during local testing)
|
|
app.post('/api/cleanup-tests', (req, res) => {
|
|
const patterns = [/^test$/i, /^curl-test$/i, /^start-test$/i];
|
|
const before = games.length;
|
|
for (let i = games.length - 1; i >= 0; i--) {
|
|
const g = games[i];
|
|
if (patterns.some(p => p.test(g.name))) {
|
|
games.splice(i, 1);
|
|
}
|
|
}
|
|
const removed = before - games.length;
|
|
io.emit('games-list', games);
|
|
res.json({ ok: true, removed });
|
|
});
|
|
|
|
function pruneOrphans(){
|
|
let removed = 0;
|
|
for (let i = games.length - 1; i >= 0; i--) {
|
|
const g = games[i];
|
|
if(!g.players || g.players.length === 0){ games.splice(i,1); removed++; }
|
|
}
|
|
if(removed) io.emit('games-list', games);
|
|
return removed;
|
|
}
|
|
|
|
// allow manual prune via api for dev
|
|
app.post('/api/prune-orphans', (req, res) => {
|
|
const removed = pruneOrphans();
|
|
res.json({ ok: true, removed });
|
|
});
|
|
|
|
// dev helper: delete game(s) by exact name
|
|
app.post('/api/delete-by-name', (req, res) => {
|
|
const { name } = req.body || {};
|
|
if(!name) return res.status(400).json({ error: 'name required' });
|
|
const before = games.length;
|
|
for (let i = games.length - 1; i >= 0; i--) {
|
|
if (games[i].name === name) games.splice(i,1);
|
|
}
|
|
const removed = before - games.length;
|
|
if(removed) io.emit('games-list', games);
|
|
res.json({ ok: true, removed });
|
|
});
|
|
|
|
// dev helper: delete a game by id
|
|
app.post('/api/delete-by-id', (req, res) => {
|
|
const { id } = req.body || {};
|
|
if(!id) return res.status(400).json({ error: 'id required' });
|
|
const idx = games.findIndex(g => g.id === id);
|
|
if(idx === -1) return res.status(404).json({ error: 'not found' });
|
|
games.splice(idx, 1);
|
|
io.emit('games-list', games);
|
|
res.json({ ok: true, removed: 1 });
|
|
});
|
|
|
|
// periodic prune every minute
|
|
setInterval(() => { pruneOrphans(); }, 60 * 1000);
|
|
|
|
app.post('/api/join', (req, res) => {
|
|
const { id, pass, nickname, colorChoice } = req.body || {};
|
|
const g = games.find(x => x.id === id);
|
|
if(!g) return res.status(404).json({ error: 'not found' });
|
|
if(g.pass && g.pass !== pass) return res.status(403).json({ error: 'bad password' });
|
|
if(g.players.length >= 2) return res.status(403).json({ error: 'full' });
|
|
const player = { id: 'p-' + Math.random().toString(36).slice(2,6), nickname: nickname || 'Anon', colorChoice: colorChoice || 'random', colorAssigned: null };
|
|
g.players.push(player);
|
|
io.to(g.id).emit('player-update', g);
|
|
io.emit('games-list', games);
|
|
res.json({ ok: true, game: g, player });
|
|
});
|
|
|
|
// leave a game: payload { gameId, playerId }
|
|
app.post('/api/leave', (req, res) => {
|
|
const { gameId, playerId } = req.body || {};
|
|
const gIndex = games.findIndex(x => x.id === gameId);
|
|
if(gIndex === -1) return res.status(404).json({ error: 'not found' });
|
|
const g = games[gIndex];
|
|
const pIndex = g.players.findIndex(p => p.id === playerId);
|
|
if(pIndex === -1) return res.status(404).json({ error: 'player not in game' });
|
|
// remove player
|
|
const leaving = g.players.splice(pIndex, 1)[0];
|
|
// if the leaving player was the owner, remove the entire game
|
|
if(g.ownerId && leaving.id === g.ownerId){
|
|
games.splice(gIndex, 1);
|
|
io.emit('games-list', games);
|
|
io.to(gameId).emit('game-deleted', { gameId });
|
|
return res.json({ ok: true, deleted: true });
|
|
}
|
|
io.to(gameId).emit('player-update', g);
|
|
io.emit('games-list', games);
|
|
res.json({ ok: true, game: g });
|
|
});
|
|
|
|
app.get('/api/game/:id', (req, res) => {
|
|
const id = req.params.id;
|
|
const g = games.find(x => x.id === id);
|
|
if(!g) return res.status(404).json({ error: 'not found' });
|
|
// if game exists but has no players, treat as not found and remove it
|
|
if(!g.players || g.players.length === 0){
|
|
const idx = games.findIndex(x => x.id === id);
|
|
if(idx !== -1) games.splice(idx, 1);
|
|
return res.status(404).json({ error: 'not found' });
|
|
}
|
|
res.json(g);
|
|
});
|
|
|
|
app.post('/api/start', (req, res) => {
|
|
const { id } = req.body || {};
|
|
const g = games.find(x => x.id === id);
|
|
if(!g) return res.status(404).json({ error: 'not found' });
|
|
if(g.players.length < 2) return res.status(400).json({ error: 'need 2 players' });
|
|
// assign colors
|
|
const p0 = g.players[0];
|
|
const p1 = g.players[1];
|
|
function assign(){
|
|
// if one chose white and other chose black, honor
|
|
if(p0.colorChoice === 'white' && p1.colorChoice === 'black'){ p0.colorAssigned = 'white'; p1.colorAssigned = 'black'; return; }
|
|
if(p0.colorChoice === 'black' && p1.colorChoice === 'white'){ p0.colorAssigned = 'black'; p1.colorAssigned = 'white'; return; }
|
|
// if one fixed and other random, honor fixed
|
|
if(p0.colorChoice === 'white' || p0.colorChoice === 'black'){ p0.colorAssigned = p0.colorChoice; p1.colorAssigned = (p0.colorChoice === 'white' ? 'black' : 'white'); return; }
|
|
if(p1.colorChoice === 'white' || p1.colorChoice === 'black'){ p1.colorAssigned = p1.colorChoice; p0.colorAssigned = (p1.colorChoice === 'white' ? 'black' : 'white'); return; }
|
|
// otherwise random
|
|
if(Math.random() < 0.5){ p0.colorAssigned = 'white'; p1.colorAssigned = 'black'; } else { p0.colorAssigned = 'black'; p1.colorAssigned = 'white'; }
|
|
}
|
|
assign();
|
|
g.started = true;
|
|
io.to(g.id).emit('game-start', g);
|
|
io.emit('games-list', games);
|
|
res.json({ ok: true, game: g });
|
|
});
|
|
|
|
io.on('connection', (socket) => {
|
|
console.log('socket connected', socket.id);
|
|
socket.on('join-game', (gameId) => {
|
|
const room = gameId;
|
|
socket.join(room);
|
|
console.log(`${socket.id} joined room ${room}`);
|
|
});
|
|
// client can identify which player it is in a game so server can map socket -> player
|
|
socket.on('identify', (payload) => {
|
|
try {
|
|
const { gameId, playerId } = payload || {};
|
|
if(gameId && playerId){
|
|
socketMap.set(socket.id, { gameId, playerId });
|
|
console.log(`socket ${socket.id} identified as player ${playerId} in ${gameId}`);
|
|
}
|
|
}catch(e){ }
|
|
});
|
|
socket.on('move', (payload) => {
|
|
const { gameId } = payload;
|
|
socket.to(gameId).emit('move', payload);
|
|
});
|
|
|
|
socket.on('disconnect', (reason) => {
|
|
// if we know which player this socket belonged to, remove them from the game
|
|
const meta = socketMap.get(socket.id);
|
|
if(!meta) return;
|
|
const { gameId, playerId } = meta;
|
|
socketMap.delete(socket.id);
|
|
const gIndex = games.findIndex(x => x.id === gameId);
|
|
if(gIndex === -1) return;
|
|
const g = games[gIndex];
|
|
const pIndex = g.players.findIndex(p => p.id === playerId);
|
|
if(pIndex === -1) return;
|
|
const leaving = g.players.splice(pIndex, 1)[0];
|
|
console.log(`socket ${socket.id} disconnected (${reason}), removed player ${leaving.id} from ${gameId}`);
|
|
// if owner left, delete the game
|
|
if(g.ownerId && leaving.id === g.ownerId){
|
|
games.splice(gIndex, 1);
|
|
io.emit('games-list', games);
|
|
io.to(gameId).emit('game-deleted', { gameId });
|
|
return;
|
|
}
|
|
io.to(gameId).emit('player-update', g);
|
|
io.emit('games-list', games);
|
|
});
|
|
});
|
|
|
|
server.listen(PORT, () => console.log(`backend listening ${PORT}`));
|