2PlayerGame/chapter2/server.js
2026-07-23 17:03:26 +03:00

91 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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