initial commit

This commit is contained in:
Nicola
2026-06-24 13:00:51 +02:00
commit 2d4c3864ef
11 changed files with 2555 additions and 0 deletions
+348
View File
@@ -0,0 +1,348 @@
/**
* 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/ ║
╚═══════════════════════════════════════╝
`);
});