- Aggiunto server Express + WebSocket (server.js) - Creata pagina Display (solo visualizzazione punteggio) - Creata pagina Controller (pannello comandi da mobile) - Aggiunto Vue Router con rotte / e /controller - Estratta logica di gioco condivisa in gameState.js
236 lines
8.3 KiB
JavaScript
236 lines
8.3 KiB
JavaScript
import { createServer } from 'http'
|
|
import express from 'express'
|
|
import { WebSocketServer } from 'ws'
|
|
import { fileURLToPath } from 'url'
|
|
import { dirname, join } from 'path'
|
|
|
|
// Import shared game logic
|
|
// We need to read it as a dynamic import since it uses ES modules
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = dirname(__filename)
|
|
|
|
// Inline the game state logic for the server (avoid complex ESM import from src/)
|
|
function createInitialState() {
|
|
return {
|
|
order: true,
|
|
visuForm: false,
|
|
visuStriscia: true,
|
|
modalitaPartita: "3/5",
|
|
sp: {
|
|
striscia: { home: [0], guest: [0] },
|
|
servHome: true,
|
|
punt: { home: 0, guest: 0 },
|
|
set: { home: 0, guest: 0 },
|
|
nomi: { home: "Antoniana", guest: "Guest" },
|
|
form: {
|
|
home: ["1", "2", "3", "4", "5", "6"],
|
|
guest: ["1", "2", "3", "4", "5", "6"],
|
|
},
|
|
storicoServizio: [],
|
|
},
|
|
}
|
|
}
|
|
|
|
function checkVittoria(state) {
|
|
const puntHome = state.sp.punt.home
|
|
const puntGuest = state.sp.punt.guest
|
|
const setHome = state.sp.set.home
|
|
const setGuest = state.sp.set.guest
|
|
const totSet = setHome + setGuest
|
|
let isSetDecisivo = false
|
|
if (state.modalitaPartita === "2/3") {
|
|
isSetDecisivo = totSet >= 2
|
|
} else {
|
|
isSetDecisivo = totSet >= 4
|
|
}
|
|
const punteggioVittoria = isSetDecisivo ? 15 : 25
|
|
if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) return true
|
|
if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) return true
|
|
return false
|
|
}
|
|
|
|
function applyAction(state, action) {
|
|
const s = JSON.parse(JSON.stringify(state))
|
|
switch (action.type) {
|
|
case "incPunt": {
|
|
const team = action.team
|
|
if (checkVittoria(s)) break
|
|
s.sp.storicoServizio.push({
|
|
servHome: s.sp.servHome,
|
|
cambioPalla: (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome),
|
|
})
|
|
s.sp.punt[team]++
|
|
if (team === "home") {
|
|
s.sp.striscia.home.push(s.sp.punt.home)
|
|
s.sp.striscia.guest.push(" ")
|
|
} else {
|
|
s.sp.striscia.guest.push(s.sp.punt.guest)
|
|
s.sp.striscia.home.push(" ")
|
|
}
|
|
const cambioPalla = (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome)
|
|
if (cambioPalla) {
|
|
s.sp.form[team].push(s.sp.form[team].shift())
|
|
}
|
|
s.sp.servHome = team === "home"
|
|
break
|
|
}
|
|
case "decPunt": {
|
|
if (s.sp.striscia.home.length > 1 && s.sp.storicoServizio.length > 0) {
|
|
const tmpHome = s.sp.striscia.home.pop()
|
|
s.sp.striscia.guest.pop()
|
|
const statoServizio = s.sp.storicoServizio.pop()
|
|
if (tmpHome === " ") {
|
|
s.sp.punt.guest--
|
|
if (statoServizio.cambioPalla) {
|
|
s.sp.form.guest.unshift(s.sp.form.guest.pop())
|
|
}
|
|
} else {
|
|
s.sp.punt.home--
|
|
if (statoServizio.cambioPalla) {
|
|
s.sp.form.home.unshift(s.sp.form.home.pop())
|
|
}
|
|
}
|
|
s.sp.servHome = statoServizio.servHome
|
|
}
|
|
break
|
|
}
|
|
case "incSet": {
|
|
const team = action.team
|
|
if (s.sp.set[team] === 2) { s.sp.set[team] = 0 } else { s.sp.set[team]++ }
|
|
break
|
|
}
|
|
case "cambiaPalla": {
|
|
if (s.sp.punt.home === 0 && s.sp.punt.guest === 0) {
|
|
s.sp.servHome = !s.sp.servHome
|
|
}
|
|
break
|
|
}
|
|
case "resetta": {
|
|
s.visuForm = false
|
|
s.sp.punt.home = 0
|
|
s.sp.punt.guest = 0
|
|
s.sp.form = {
|
|
home: ["1", "2", "3", "4", "5", "6"],
|
|
guest: ["1", "2", "3", "4", "5", "6"],
|
|
}
|
|
s.sp.striscia = { home: [0], guest: [0] }
|
|
s.sp.storicoServizio = []
|
|
break
|
|
}
|
|
case "toggleFormazione": { s.visuForm = !s.visuForm; break }
|
|
case "toggleStriscia": { s.visuStriscia = !s.visuStriscia; break }
|
|
case "toggleOrder": { s.order = !s.order; break }
|
|
case "setNomi": {
|
|
if (action.home !== undefined) s.sp.nomi.home = action.home
|
|
if (action.guest !== undefined) s.sp.nomi.guest = action.guest
|
|
break
|
|
}
|
|
case "setModalita": { s.modalitaPartita = action.modalita; break }
|
|
case "setFormazione": {
|
|
if (action.team && action.form) {
|
|
s.sp.form[action.team] = [...action.form]
|
|
}
|
|
break
|
|
}
|
|
case "confermaCambi": {
|
|
const team = action.team
|
|
const cambi = action.cambi || []
|
|
const form = s.sp.form[team].map((val) => String(val).trim())
|
|
const formAggiornata = [...form]
|
|
let valid = true
|
|
for (const cambio of cambi) {
|
|
const cin = (cambio.in || "").trim()
|
|
const cout = (cambio.out || "").trim()
|
|
if (!cin || !cout) continue
|
|
if (!/^\d+$/.test(cin) || !/^\d+$/.test(cout)) { valid = false; break }
|
|
if (cin === cout) { valid = false; break }
|
|
if (formAggiornata.includes(cin)) { valid = false; break }
|
|
if (!formAggiornata.includes(cout)) { valid = false; break }
|
|
const idx = formAggiornata.findIndex((val) => String(val).trim() === cout)
|
|
if (idx !== -1) { formAggiornata.splice(idx, 1, cin) }
|
|
}
|
|
if (valid) { s.sp.form[team] = formAggiornata }
|
|
break
|
|
}
|
|
default: break
|
|
}
|
|
return s
|
|
}
|
|
|
|
// ——— Server Setup ———
|
|
|
|
const app = express()
|
|
const PORT = process.env.PORT || 3000
|
|
|
|
// Serve the Vite build output
|
|
app.use(express.static(join(__dirname, 'dist')))
|
|
|
|
// SPA fallback: serve index.html for all non-file routes
|
|
app.get('/{*splat}', (req, res) => {
|
|
res.sendFile(join(__dirname, 'dist', 'index.html'))
|
|
})
|
|
|
|
const server = createServer(app)
|
|
|
|
// WebSocket server
|
|
const wss = new WebSocketServer({ server })
|
|
|
|
// Global game state
|
|
let gameState = createInitialState()
|
|
|
|
// Track client roles
|
|
const clients = new Map() // ws -> { role: 'display' | 'controller' }
|
|
|
|
wss.on('connection', (ws) => {
|
|
console.log('New WebSocket connection')
|
|
|
|
ws.on('message', (data) => {
|
|
try {
|
|
const msg = JSON.parse(data.toString())
|
|
|
|
if (msg.type === 'register') {
|
|
clients.set(ws, { role: msg.role || 'display' })
|
|
console.log(`Client registered as: ${msg.role || 'display'}`)
|
|
// Send current state immediately
|
|
ws.send(JSON.stringify({ type: 'state', state: gameState }))
|
|
return
|
|
}
|
|
|
|
if (msg.type === 'action') {
|
|
// Only controllers can send actions
|
|
const client = clients.get(ws)
|
|
if (!client || client.role !== 'controller') {
|
|
console.log('Action rejected: not a controller')
|
|
return
|
|
}
|
|
|
|
// Apply the action to game state
|
|
gameState = applyAction(gameState, msg.action)
|
|
|
|
// Broadcast new state to ALL connected clients
|
|
const stateMsg = JSON.stringify({ type: 'state', state: gameState })
|
|
wss.clients.forEach((c) => {
|
|
if (c.readyState === 1) { // WebSocket.OPEN
|
|
c.send(stateMsg)
|
|
}
|
|
})
|
|
}
|
|
} catch (err) {
|
|
console.error('Error processing message:', err)
|
|
}
|
|
})
|
|
|
|
ws.on('close', () => {
|
|
clients.delete(ws)
|
|
console.log('Client disconnected')
|
|
})
|
|
})
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`\n🏐 Segnapunti Server running on:`)
|
|
console.log(` Display: http://localhost:${PORT}/`)
|
|
console.log(` Controller: http://localhost:${PORT}/controller`)
|
|
console.log(`\n Per accedere da altri dispositivi sulla rete locale,`)
|
|
console.log(` usa l'IP di questo computer, es: http://192.168.1.x:${PORT}/controller\n`)
|
|
})
|