87 lines
3.6 KiB
JavaScript
87 lines
3.6 KiB
JavaScript
// Zero-dependency sync server: static files + SSE broadcast of session state.
|
||
// ponytail: sessions live in memory — рестарт сервера скидає гру. Досить для вечора.
|
||
const http = require('http');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const os = require('os');
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
const sessions = Object.create(null);
|
||
|
||
function getSession(code) {
|
||
return sessions[code] ??= {
|
||
state: { found: {}, pinned: [], asked: {}, tries: 0, lastAccusation: null, solved: false, players: {} },
|
||
clients: new Set()
|
||
};
|
||
}
|
||
|
||
function broadcast(s) {
|
||
const msg = `data: ${JSON.stringify(s.state)}\n\n`;
|
||
for (const res of s.clients) res.write(msg);
|
||
}
|
||
|
||
function apply(st, a) {
|
||
switch (a.t) {
|
||
case 'join': st.players[a.role] = (st.players[a.role] || 0) + 1; break;
|
||
case 'find': st.found[a.id] = a.by || true; break;
|
||
case 'pin': if (!st.pinned.some(p => p.id === a.id)) st.pinned.push({ id: a.id, s: a.s, by: a.by }); break;
|
||
case 'unpin': st.pinned = st.pinned.filter(p => p.id !== a.id); break;
|
||
case 'repin': { const p = st.pinned.find(p => p.id === a.id); if (p) p.s = a.s; } break;
|
||
case 'ask': { const arr = st.asked[a.s] ??= []; if (!arr.includes(a.q)) arr.push(a.q); } break;
|
||
case 'accuse': st.tries++; st.lastAccusation = { suspect: a.suspect, method: a.method, evidence: a.evidence, result: a.result, n: st.tries };
|
||
if (a.result.ok) st.solved = true; break;
|
||
}
|
||
}
|
||
|
||
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8' };
|
||
|
||
const server = http.createServer((req, res) => {
|
||
const u = new URL(req.url, 'http://x');
|
||
|
||
if (u.pathname === '/events') {
|
||
const s = getSession(u.searchParams.get('s') || 'DEFAULT');
|
||
const role = u.searchParams.get('role');
|
||
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
||
s.clients.add(res);
|
||
const online = s.state.online ??= {};
|
||
if (role) { online[role] = (online[role] || 0) + 1; }
|
||
broadcast(s);
|
||
req.on('close', () => {
|
||
s.clients.delete(res);
|
||
if (role) { online[role] = Math.max(0, (online[role] || 1) - 1); broadcast(s); }
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (u.pathname === '/act' && req.method === 'POST') {
|
||
let body = '';
|
||
req.on('data', c => { body += c; if (body.length > 1e6) req.destroy(); });
|
||
req.on('end', () => {
|
||
try {
|
||
const a = JSON.parse(body);
|
||
const s = getSession(a.session || 'DEFAULT');
|
||
apply(s.state, a);
|
||
broadcast(s);
|
||
res.writeHead(200).end('ok');
|
||
} catch { res.writeHead(400).end('bad request'); }
|
||
});
|
||
return;
|
||
}
|
||
|
||
const file = { '/': 'index.html', '/index.html': 'index.html', '/case.js': 'case.js' }[u.pathname];
|
||
if (!file) return void res.writeHead(404).end('not found');
|
||
fs.readFile(path.join(__dirname, file), (err, data) => {
|
||
if (err) return void res.writeHead(500).end('error');
|
||
res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] }).end(data);
|
||
});
|
||
});
|
||
|
||
server.listen(PORT, () => {
|
||
console.log(`Nightshade Protocol — сервер запущено.\n`);
|
||
console.log(`На цьому пристрої: http://localhost:${PORT}`);
|
||
for (const ifs of Object.values(os.networkInterfaces()))
|
||
for (const i of ifs)
|
||
if (i.family === 'IPv4' && !i.internal)
|
||
console.log(`У локальній мережі: http://${i.address}:${PORT}`);
|
||
console.log(`\nОбидва гравці відкривають адресу мережі, вводять однаковий код сесії і обирають різні ролі.`);
|
||
});
|