Nuovo script cli.js che si connette al server via WebSocket come controller e permette di gestire la partita da terminale con comandi testuali, colori ANSI, tab-completion e history dei comandi. Aggiunge script npm "cli" / "cli:dev" e documenta tutti i comandi nel README
303 lines
8.7 KiB
JavaScript
303 lines
8.7 KiB
JavaScript
import { WebSocket } from 'ws';
|
|
import readline from 'readline';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ANSI helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const c = {
|
|
reset: '\x1b[0m',
|
|
bold: '\x1b[1m',
|
|
dim: '\x1b[2m',
|
|
red: '\x1b[31m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
cyan: '\x1b[36m',
|
|
brightWhite: '\x1b[97m',
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const port = process.argv[2] || process.env.PORT || 3000;
|
|
const url = `ws://localhost:${port}/ws`;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// State
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let currentState = null;
|
|
let connected = false;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Startup banner
|
|
// ---------------------------------------------------------------------------
|
|
|
|
console.log(`\n${c.bold}${c.cyan} Segnapunti Anto — Terminal Controller${c.reset}`);
|
|
console.log(`${c.dim} Connessione a ${url}...${c.reset}\n`);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WebSocket
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const ws = new WebSocket(url);
|
|
|
|
ws.on('error', (err) => {
|
|
console.error(`${c.red}Errore di connessione: ${err.message}${c.reset}`);
|
|
console.error(`${c.dim}Assicurati che il server sia avviato su ${url}${c.reset}`);
|
|
process.exit(1);
|
|
});
|
|
|
|
ws.on('open', () => {
|
|
connected = true;
|
|
console.log(` ${c.green}Connesso.${c.reset} Digita ${c.bold}help${c.reset} per i comandi disponibili.\n`);
|
|
ws.send(JSON.stringify({ type: 'register', role: 'controller' }));
|
|
});
|
|
|
|
ws.on('message', (data) => {
|
|
try {
|
|
const msg = JSON.parse(data.toString());
|
|
if (msg.type === 'state') {
|
|
currentState = msg.state;
|
|
printState(currentState);
|
|
} else if (msg.type === 'error') {
|
|
clearLine();
|
|
console.error(` ${c.red}Errore server: ${msg.message}${c.reset}`);
|
|
}
|
|
} catch {
|
|
// ignora messaggi malformati
|
|
}
|
|
rl.prompt(true);
|
|
});
|
|
|
|
ws.on('close', () => {
|
|
console.log(`\n${c.dim} Connessione chiusa.${c.reset}\n`);
|
|
process.exit(0);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Output helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Cancella la riga corrente del terminale (evita di sovrascrivere il prompt). */
|
|
function clearLine() {
|
|
process.stdout.write('\r\x1b[K');
|
|
}
|
|
|
|
function printState(s) {
|
|
if (!s) return;
|
|
const { nomi, punt, set, servHome } = s.sp;
|
|
|
|
const homeServ = servHome ? `${c.yellow}▶${c.reset}` : ' ';
|
|
const guestServ = !servHome ? `${c.yellow}◀${c.reset}` : ' ';
|
|
|
|
const homeName = nomi.home.padEnd(15);
|
|
const guestName = nomi.guest.padEnd(15);
|
|
const homeScore = String(punt.home).padStart(3);
|
|
const guestScore = String(punt.guest).padStart(3);
|
|
|
|
clearLine();
|
|
console.log(
|
|
` ${homeServ} ${c.bold}${homeName}${c.reset}` +
|
|
`${c.brightWhite}${homeScore}${c.reset} ${c.dim}(set ${set.home})${c.reset}` +
|
|
` ${c.dim}│${c.reset} ` +
|
|
`${c.brightWhite}${guestScore}${c.reset} ${c.dim}(set ${set.guest})${c.reset}` +
|
|
` ${c.bold}${guestName}${c.reset} ${guestServ}` +
|
|
` ${c.dim}[${s.modalitaPartita}]${c.reset}`
|
|
);
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log(`
|
|
${c.bold} Punteggio${c.reset}
|
|
${c.cyan}+${c.reset} / ${c.cyan}pc${c.reset} Punto casa (shortcut)
|
|
${c.cyan}-${c.reset} / ${c.cyan}po${c.reset} Punto ospite (shortcut)
|
|
${c.cyan}punto casa${c.reset} Punto casa
|
|
${c.cyan}punto ospite${c.reset} Punto ospite
|
|
${c.cyan}undo${c.reset} / ${c.cyan}u${c.reset} Annulla ultimo punto
|
|
|
|
${c.bold} Set${c.reset}
|
|
${c.cyan}set casa${c.reset} Incrementa set casa
|
|
${c.cyan}set ospite${c.reset} Incrementa set ospite
|
|
|
|
${c.bold} Partita${c.reset}
|
|
${c.cyan}serv${c.reset} Cambia servizio (solo se 0-0)
|
|
${c.cyan}reset${c.reset} Resetta la partita (chiede conferma)
|
|
${c.cyan}nomi <casa> <ospite>${c.reset} Imposta nomi squadre
|
|
${c.cyan}modalita 2/3${c.reset} Imposta modalità best-of-3
|
|
${c.cyan}modalita 3/5${c.reset} Imposta modalità best-of-5
|
|
|
|
${c.bold} Informazioni${c.reset}
|
|
${c.cyan}stato${c.reset} Mostra punteggio attuale
|
|
${c.cyan}help${c.reset} Mostra questo aiuto
|
|
${c.cyan}exit${c.reset} / ${c.cyan}q${c.reset} Esci
|
|
|
|
${c.dim}Suggerimento: usa Tab per il completamento automatico dei comandi.${c.reset}
|
|
`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Command dispatch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function sendAction(action) {
|
|
if (!connected || ws.readyState !== WebSocket.OPEN) {
|
|
console.error(` ${c.red}Non connesso al server.${c.reset}`);
|
|
rl.prompt();
|
|
return;
|
|
}
|
|
ws.send(JSON.stringify({ type: 'action', action }));
|
|
}
|
|
|
|
function parseCommand(line) {
|
|
const parts = line.trim().split(/\s+/);
|
|
const cmd = parts[0].toLowerCase();
|
|
|
|
switch (cmd) {
|
|
|
|
case '+':
|
|
case 'pc':
|
|
sendAction({ type: 'incPunt', team: 'home' });
|
|
break;
|
|
|
|
case '-':
|
|
case 'po':
|
|
sendAction({ type: 'incPunt', team: 'guest' });
|
|
break;
|
|
|
|
case 'punto': {
|
|
const team = parts[1]?.toLowerCase();
|
|
if (team === 'casa' || team === 'home') {
|
|
sendAction({ type: 'incPunt', team: 'home' });
|
|
} else if (team === 'ospite' || team === 'guest') {
|
|
sendAction({ type: 'incPunt', team: 'guest' });
|
|
} else {
|
|
console.error(` ${c.red}Uso: punto casa | punto ospite${c.reset}`);
|
|
rl.prompt();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'undo':
|
|
case 'u':
|
|
sendAction({ type: 'decPunt' });
|
|
break;
|
|
|
|
case 'set': {
|
|
const team = parts[1]?.toLowerCase();
|
|
if (team === 'casa' || team === 'home') {
|
|
sendAction({ type: 'incSet', team: 'home' });
|
|
} else if (team === 'ospite' || team === 'guest') {
|
|
sendAction({ type: 'incSet', team: 'guest' });
|
|
} else {
|
|
console.error(` ${c.red}Uso: set casa | set ospite${c.reset}`);
|
|
rl.prompt();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'serv':
|
|
sendAction({ type: 'cambiaPalla' });
|
|
break;
|
|
|
|
case 'reset':
|
|
rl.question(` ${c.yellow}Confermi reset partita? (s/N) ${c.reset}`, (answer) => {
|
|
if (answer.trim().toLowerCase() === 's') {
|
|
sendAction({ type: 'resetta' });
|
|
} else {
|
|
console.log(` ${c.dim}Reset annullato.${c.reset}`);
|
|
rl.prompt();
|
|
}
|
|
});
|
|
return;
|
|
|
|
case 'nomi': {
|
|
const home = parts[1];
|
|
const guest = parts[2];
|
|
if (!home) {
|
|
console.error(` ${c.red}Uso: nomi <casa> <ospite>${c.reset}`);
|
|
rl.prompt();
|
|
break;
|
|
}
|
|
const payload = { type: 'setNomi', home };
|
|
if (guest) payload.guest = guest;
|
|
sendAction(payload);
|
|
break;
|
|
}
|
|
|
|
case 'modalita': {
|
|
const m = parts[1];
|
|
if (m !== '2/3' && m !== '3/5') {
|
|
console.error(` ${c.red}Uso: modalita 2/3 | modalita 3/5${c.reset}`);
|
|
rl.prompt();
|
|
break;
|
|
}
|
|
sendAction({ type: 'setModalita', modalita: m });
|
|
break;
|
|
}
|
|
|
|
case 'stato':
|
|
printState(currentState);
|
|
rl.prompt();
|
|
break;
|
|
|
|
case 'help':
|
|
printHelp();
|
|
rl.prompt();
|
|
break;
|
|
|
|
case 'exit':
|
|
case 'q':
|
|
ws.close();
|
|
break;
|
|
|
|
default:
|
|
console.error(
|
|
` ${c.red}Comando non riconosciuto: "${cmd}"${c.reset}` +
|
|
` — digita ${c.bold}help${c.reset} per la lista`
|
|
);
|
|
rl.prompt();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// REPL
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const TAB_COMPLETIONS = [
|
|
'+', '-', 'pc', 'po',
|
|
'punto casa', 'punto ospite',
|
|
'undo', 'u',
|
|
'set casa', 'set ospite',
|
|
'serv',
|
|
'reset',
|
|
'nomi',
|
|
'modalita 2/3', 'modalita 3/5',
|
|
'stato', 'help',
|
|
'exit', 'q',
|
|
];
|
|
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
prompt: '> ',
|
|
historySize: 100,
|
|
completer(line) {
|
|
const hits = TAB_COMPLETIONS.filter((entry) => entry.startsWith(line));
|
|
return [hits.length ? hits : TAB_COMPLETIONS, line];
|
|
},
|
|
});
|
|
|
|
rl.on('line', (line) => {
|
|
if (!line.trim()) {
|
|
rl.prompt();
|
|
return;
|
|
}
|
|
parseCommand(line.trim());
|
|
});
|
|
|
|
rl.on('close', () => {
|
|
ws.close();
|
|
});
|