Files
stream-overlay/server/index.js
T
2026-06-24 13:00:51 +02:00

349 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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/ ║
╚═══════════════════════════════════════╝
`);
});