92 lines
4.1 KiB
JavaScript
92 lines
4.1 KiB
JavaScript
|
|
// Zero-dependency sync server (розділ 2): static + SSE broadcast стану сесії.
|
|||
|
|
// ponytail: сесії в пам'яті — рестарт сервера скидає гру. Досить для вечора.
|
|||
|
|
const http = require('http');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const os = require('os');
|
|||
|
|
|
|||
|
|
const PORT = process.env.PORT || 3001;
|
|||
|
|
const sessions = Object.create(null);
|
|||
|
|
|
|||
|
|
function getSession(code) {
|
|||
|
|
return sessions[code] ??= {
|
|||
|
|
state: { found: {}, pinned: [], asked: {}, notes: [], crossed: {}, stars: {}, act: 1,
|
|||
|
|
tries: 0, lastAccusation: null, solved: false, online: {} },
|
|||
|
|
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 '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 'note': if (typeof a.txt === 'string' && a.txt.trim()) st.notes.push({ id: Date.now() + Math.random(), txt: a.txt.slice(0, 500), by: a.by }); break;
|
|||
|
|
case 'note_del':st.notes = st.notes.filter(n => n.id !== a.id); break;
|
|||
|
|
case 'cross': st.crossed[a.s] = !st.crossed[a.s]; break;
|
|||
|
|
case 'star': st.stars[a.id] = !st.stars[a.id]; break;
|
|||
|
|
case 'advance': st.act = Math.max(st.act, a.act | 0); 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);
|
|||
|
|
if (role) s.state.online[role] = (s.state.online[role] || 0) + 1;
|
|||
|
|
broadcast(s);
|
|||
|
|
req.on('close', () => {
|
|||
|
|
s.clients.delete(res);
|
|||
|
|
if (role) { s.state.online[role] = Math.max(0, (s.state.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', '/case2.js': 'case2.js', '/minigames.js': 'minigames.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(`Тихий аукціон (розділ 2) — сервер запущено.\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Обидва гравці відкривають адресу мережі, вводять однаковий код сесії і обирають різні ролі.`);
|
|||
|
|
});
|