2PlayerGame/chapter2/server.js

92 lines
4.1 KiB
JavaScript
Raw Normal View History

2026-07-23 17:03:26 +03:00
// 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Обидва гравці відкривають адресу мережі, вводять однаковий код сесії і обирають різні ролі.`);
});