349 lines
10 KiB
JavaScript
349 lines
10 KiB
JavaScript
/**
|
||
* Tennis Roots – Server Express + WebSocket
|
||
*
|
||
* Funge da hub centrale:
|
||
* - Serve gli overlay (statici)
|
||
* - Serve il pannello di controllo (statico)
|
||
* - Gestisce le connessioni WebSocket per lo stato in tempo reale
|
||
*/
|
||
|
||
import express from 'express';
|
||
import { createServer } from 'http';
|
||
import { WebSocketServer } from 'ws';
|
||
import { resolve, dirname } from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const PORT = process.env.PORT || 3000;
|
||
|
||
// ---- Stato di default ----
|
||
const defaultState = {
|
||
player1: 'J. Sinner',
|
||
player2: 'C. Alcaraz',
|
||
flag1: 'it',
|
||
flag2: 'es',
|
||
sets: [
|
||
[0, 0],
|
||
[0, 0],
|
||
[0, 0],
|
||
],
|
||
games: [0, 0],
|
||
points: [0, 0],
|
||
server: 1,
|
||
currentSet: 0,
|
||
matchOver: false,
|
||
deuce: false,
|
||
advantage: 0,
|
||
tiebreak: false,
|
||
tiebreakPoints: [0, 0],
|
||
tiebreakFirstServer: 1,
|
||
tournament: 'Wimbledon',
|
||
round: 'Finale · Uomini · Singolare',
|
||
matchStatus: '🎾 In corso · Set 1',
|
||
};
|
||
|
||
let currentState = { ...defaultState };
|
||
|
||
// ---- Utility punteggio tennis ----
|
||
function computeMatchStatus(state) {
|
||
if (state.matchOver) {
|
||
const p1Sets = state.sets.reduce((a, s) => a + (s[0] > s[1] ? 1 : 0), 0);
|
||
const winner = p1Sets >= 2 ? state.player1 : state.player2;
|
||
return `🏆 ${winner} vince!`;
|
||
}
|
||
const set = state.currentSet + 1;
|
||
if (state.tiebreak) {
|
||
return `🎾 Tiebreak · Set ${set} · ${state.tiebreakPoints[0]}-${state.tiebreakPoints[1]}`;
|
||
}
|
||
return `🎾 In corso · Set ${set}`;
|
||
}
|
||
|
||
function pointLabel(state) {
|
||
// Tiebreak: mostra punti numerici
|
||
if (state.tiebreak) {
|
||
return [String(state.tiebreakPoints[0]), String(state.tiebreakPoints[1])];
|
||
}
|
||
const p1 = state.points[0];
|
||
const p2 = state.points[1];
|
||
if (state.deuce) {
|
||
if (state.advantage === 1) return ['AD', '40'];
|
||
if (state.advantage === 2) return ['40', 'AD'];
|
||
return ['40', '40'];
|
||
}
|
||
const map = ['0', '15', '30', '40'];
|
||
return [map[p1] || '40', map[p2] || '40'];
|
||
}
|
||
|
||
// ---- Express ----
|
||
const app = express();
|
||
const server = createServer(app);
|
||
|
||
// Static files: overlay
|
||
app.use('/overlay', express.static(resolve(__dirname, '..', 'overlay')));
|
||
|
||
// Static files: controller
|
||
app.use('/controller', express.static(resolve(__dirname, '..', 'controller')));
|
||
|
||
// Redirect root → controller
|
||
app.get('/', (req, res) => {
|
||
res.redirect('/controller/');
|
||
});
|
||
|
||
// API: GET stato corrente
|
||
app.get('/api/state', (req, res) => {
|
||
res.json(currentState);
|
||
});
|
||
|
||
// API: POST aggiornamento stato (da controller via HTTP)
|
||
app.post('/api/state', express.json(), (req, res) => {
|
||
const partial = req.body;
|
||
currentState = { ...currentState, ...partial, matchStatus: undefined };
|
||
currentState.matchStatus = computeMatchStatus(currentState);
|
||
currentState.timestamp = Date.now();
|
||
broadcast({ type: 'STATE_UPDATE', state: currentState });
|
||
res.json({ ok: true, state: currentState });
|
||
});
|
||
|
||
// API: POST comando (ADD_POINT, RESET_GAME, RESET_MATCH)
|
||
app.post('/api/command', express.json(), (req, res) => {
|
||
const { command, player } = req.body;
|
||
handleCommand(command, player);
|
||
res.json({ ok: true, state: currentState });
|
||
});
|
||
|
||
// ---- WebSocket ----
|
||
const wss = new WebSocketServer({ server, path: '/ws' });
|
||
|
||
wss.on('connection', (ws) => {
|
||
console.log('🟢 WebSocket client connected');
|
||
|
||
// Invia stato corrente al nuovo client
|
||
ws.send(JSON.stringify({ type: 'STATE_UPDATE', state: currentState }));
|
||
|
||
ws.on('message', (data) => {
|
||
try {
|
||
const msg = JSON.parse(data.toString());
|
||
|
||
if (msg.type === 'COMMAND') {
|
||
handleCommand(msg.command, msg.player);
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'STATE_UPDATE') {
|
||
currentState = { ...currentState, ...msg.state, matchStatus: undefined };
|
||
currentState.matchStatus = computeMatchStatus(currentState);
|
||
currentState.timestamp = Date.now();
|
||
broadcast({ type: 'STATE_UPDATE', state: currentState }, ws);
|
||
return;
|
||
}
|
||
} catch (err) {
|
||
console.error('❌ Invalid message:', err.message);
|
||
}
|
||
});
|
||
|
||
ws.on('close', () => {
|
||
console.log('🔴 WebSocket client disconnected');
|
||
});
|
||
});
|
||
|
||
function broadcast(message, exclude = null) {
|
||
const payload = JSON.stringify(message);
|
||
for (const client of wss.clients) {
|
||
if (client !== exclude && client.readyState === 1) {
|
||
client.send(payload);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Comandi ----
|
||
function handleCommand(command, player) {
|
||
switch (command) {
|
||
case 'ADD_POINT':
|
||
addPoint(player);
|
||
break;
|
||
case 'RESET_GAME':
|
||
currentState.points = [0, 0];
|
||
currentState.deuce = false;
|
||
currentState.advantage = 0;
|
||
currentState.tiebreak = false;
|
||
currentState.tiebreakPoints = [0, 0];
|
||
currentState.tiebreakFirstServer = 1;
|
||
break;
|
||
case 'RESET_MATCH':
|
||
currentState = { ...defaultState, timestamp: Date.now() };
|
||
break;
|
||
case 'SET_DEUCE':
|
||
currentState.deuce = true;
|
||
currentState.advantage = 0;
|
||
currentState.points = [3, 3];
|
||
break;
|
||
case 'SET_SERVER':
|
||
currentState.server = player; // player = 1 o 2
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
currentState.matchStatus = computeMatchStatus(currentState);
|
||
currentState.timestamp = Date.now();
|
||
broadcast({ type: 'STATE_UPDATE', state: currentState });
|
||
}
|
||
|
||
function addPoint(player) {
|
||
const idx = player - 1;
|
||
|
||
if (currentState.matchOver) return;
|
||
|
||
// ---- TIEBREAK ----
|
||
if (currentState.tiebreak) {
|
||
currentState.tiebreakPoints[idx]++;
|
||
const tp1 = currentState.tiebreakPoints[0];
|
||
const tp2 = currentState.tiebreakPoints[1];
|
||
|
||
// Aggiorna server per il punto successivo nel tiebreak
|
||
updateTiebreakServer(currentState);
|
||
|
||
// Vinta se ≥7 punti e 2 di scarto
|
||
if ((tp1 >= 7 || tp2 >= 7) && Math.abs(tp1 - tp2) >= 2) {
|
||
// Ha vinto il tiebreak → set 7-6
|
||
const won = tp1 > tp2 ? 0 : 1;
|
||
currentState.sets[currentState.currentSet][won] = 7;
|
||
currentState.sets[currentState.currentSet][1 - won] = 6;
|
||
currentState.tiebreak = false;
|
||
currentState.tiebreakPoints = [0, 0];
|
||
currentState.games = [0, 0];
|
||
|
||
// Controlla match vinto
|
||
let p1Sets = 0, p2Sets = 0;
|
||
for (let i = 0; i < 3; i++) {
|
||
if (currentState.sets[i][0] > currentState.sets[i][1]) p1Sets++;
|
||
else if (currentState.sets[i][1] > currentState.sets[i][0]) p2Sets++;
|
||
}
|
||
if (p1Sets >= 2 || p2Sets >= 2) {
|
||
currentState.matchOver = true;
|
||
} else {
|
||
currentState.currentSet++;
|
||
}
|
||
|
||
currentState.server = currentState.server === 1 ? 2 : 1;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// ---- GIOCO NORMALE ----
|
||
if (currentState.deuce) {
|
||
if (currentState.advantage === player) {
|
||
currentState.points = [0, 0];
|
||
currentState.deuce = false;
|
||
currentState.advantage = 0;
|
||
addGame(player);
|
||
} else if (currentState.advantage === 0) {
|
||
currentState.advantage = player;
|
||
} else {
|
||
currentState.advantage = 0;
|
||
}
|
||
return;
|
||
}
|
||
|
||
currentState.points[idx]++;
|
||
const p1 = currentState.points[0];
|
||
const p2 = currentState.points[1];
|
||
|
||
if (p1 >= 3 && p2 >= 3 && p1 === p2) {
|
||
currentState.deuce = true;
|
||
currentState.advantage = 0;
|
||
return;
|
||
}
|
||
|
||
if ((p1 >= 4 || p2 >= 4) && Math.abs(p1 - p2) >= 2) {
|
||
currentState.points = [0, 0];
|
||
addGame(player);
|
||
return;
|
||
}
|
||
}
|
||
|
||
function addGame(player) {
|
||
const idx = player - 1;
|
||
currentState.games[idx]++;
|
||
const [g1, g2] = currentState.games;
|
||
const set = currentState.currentSet;
|
||
|
||
// 6-6 → si entra in tiebreak
|
||
if (g1 === 6 && g2 === 6) {
|
||
currentState.tiebreak = true;
|
||
currentState.tiebreakPoints = [0, 0];
|
||
currentState.points = [0, 0];
|
||
currentState.deuce = false;
|
||
currentState.advantage = 0;
|
||
// Il primo a servire nel tiebreak è il giocatore che NON ha servito l'ultimo game
|
||
currentState.tiebreakFirstServer = currentState.server === 1 ? 2 : 1;
|
||
currentState.server = currentState.tiebreakFirstServer;
|
||
return;
|
||
}
|
||
|
||
if ((g1 >= 6 || g2 >= 6) && Math.abs(g1 - g2) >= 2) {
|
||
currentState.sets[set][0] = g1;
|
||
currentState.sets[set][1] = g2;
|
||
currentState.games = [0, 0];
|
||
|
||
let p1Sets = 0, p2Sets = 0;
|
||
for (let i = 0; i < 3; i++) {
|
||
if (currentState.sets[i][0] > currentState.sets[i][1]) p1Sets++;
|
||
else if (currentState.sets[i][1] > currentState.sets[i][0]) p2Sets++;
|
||
}
|
||
if (p1Sets >= 2 || p2Sets >= 2) {
|
||
currentState.matchOver = true;
|
||
} else {
|
||
currentState.currentSet++;
|
||
}
|
||
} else {
|
||
currentState.sets[set][0] = g1;
|
||
currentState.sets[set][1] = g2;
|
||
}
|
||
|
||
currentState.server = currentState.server === 1 ? 2 : 1;
|
||
}
|
||
|
||
/**
|
||
* Determina il servitore nel tiebreak.
|
||
* Nel tiebreak:
|
||
* - Punto 1: servito dal giocatore designato (tiebreakFirstServer)
|
||
* - Punto 2: servito dall'altro giocatore
|
||
* - Dal punto 3 in poi: cambio servizio ogni 2 punti
|
||
*/
|
||
function updateTiebreakServer(state) {
|
||
const total = state.tiebreakPoints[0] + state.tiebreakPoints[1];
|
||
const first = state.tiebreakFirstServer;
|
||
|
||
if (total === 0) {
|
||
state.server = first;
|
||
} else if (total === 1) {
|
||
// Dopo il primo punto, serve l'altro giocatore
|
||
state.server = first === 1 ? 2 : 1;
|
||
} else {
|
||
// Gruppi da 2: alternati
|
||
const groupIndex = Math.floor((total - 1) / 2);
|
||
if (groupIndex % 2 === 0) {
|
||
// Gruppi pari (0, 2, 4...): serve l'altro giocatore
|
||
state.server = first === 1 ? 2 : 1;
|
||
} else {
|
||
// Gruppi dispari (1, 3, 5...): serve il primo giocatore
|
||
state.server = first;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Avvio ----
|
||
server.listen(PORT, () => {
|
||
console.log(`
|
||
╔═══════════════════════════════════════╗
|
||
║ 🎾 Tennis Roots Server ║
|
||
║ ────────────────────────── ║
|
||
║ HTTP : http://localhost:${PORT} ║
|
||
║ WS : ws://localhost:${PORT}/ws ║
|
||
║ ║
|
||
║ Controller : http://localhost:${PORT}/ ║
|
||
║ Overlay : http://localhost:${PORT}/overlay/ ║
|
||
╚═══════════════════════════════════════╝
|
||
`);
|
||
});
|