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

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