Загрузка данных
/*
* ============================================================
* ДУРАК — LAN EDITION
* ============================================================
*
* Один файл.
* Node.js >= 18
*
* Запуск:
* node durak.js
*
* Открыть на компьютере:
* http://localhost:3000
*
* Другие устройства в LAN:
* http://IP-ХОСТА:3000
*
* npm install НЕ НУЖЕН.
*
* ============================================================
*/
'use strict';
const http = require('http');
const os = require('os');
const crypto = require('crypto');
const fs = require('fs');
const PORT = Number(process.env.PORT || 3000);
const HOST = '0.0.0.0';
const sessions = new Map();
const rooms = new Map();
const COOKIE_NAME = 'DURAK_SESSION';
function uid(prefix = '') {
return prefix + crypto.randomBytes(8).toString('hex');
}
function roomCode() {
let code;
do {
code = String(Math.floor(100000 + Math.random() * 900000));
} while (rooms.has(code));
return code;
}
function json(res, data, status = 200) {
const body = JSON.stringify(data);
res.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
});
res.end(body);
}
function html(res, body) {
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
});
res.end(body);
}
function readBody(req) {
return new Promise((resolve, reject) => {
let data = '';
req.on('data', chunk => {
data += chunk;
if (data.length > 2_000_000) {
req.destroy();
reject(new Error('Request too large'));
}
});
req.on('end', () => {
if (!data) {
resolve({});
return;
}
try {
resolve(JSON.parse(data));
} catch {
reject(new Error('Invalid JSON'));
}
});
req.on('error', reject);
});
}
function getCookies(req) {
const result = {};
const raw = req.headers.cookie || '';
for (const part of raw.split(';')) {
const index = part.indexOf('=');
if (index === -1) continue;
const key = part.slice(0, index).trim();
const value = part.slice(index + 1).trim();
result[key] = decodeURIComponent(value);
}
return result;
}
function getSession(req, res) {
const cookies = getCookies(req);
let sid = cookies[COOKIE_NAME];
if (!sid || !sessions.has(sid)) {
sid = uid('s_');
sessions.set(sid, {
id: sid,
name: '',
room: null,
playerId: null,
pollers: [],
created: Date.now(),
lastSeen: Date.now(),
});
res.setHeader(
'Set-Cookie',
`${COOKIE_NAME}=${encodeURIComponent(sid)}; Path=/; SameSite=Lax`
);
}
const session = sessions.get(sid);
session.lastSeen = Date.now();
return session;
}
function sendEvent(session, event) {
if (!session) return;
const list = session.pollers.splice(0);
for (const poller of list) {
try {
poller.end(JSON.stringify(event));
} catch {}
}
}
function broadcast(room, event) {
for (const player of room.players) {
if (player.sessionId) {
sendEvent(sessions.get(player.sessionId), event);
}
}
}
function broadcastState(room) {
room.version++;
for (const player of room.players) {
const session = sessions.get(player.sessionId);
if (!session) continue;
sendEvent(session, {
type: 'state',
state: publicState(room, player.id),
});
}
}
function createDefaultConfig() {
return {
mode: 'throwin',
players: 4,
deck: 36,
throwMode: 'all',
maxThrow: 'defender',
finish: 'last',
autoDraw: true,
allowTrumpAce: true,
animations: true,
sounds: true,
firstAttacker: 'lowestTrump',
translateLimit: 1,
allowTranslate: true,
};
}
function createRoom(hostSession, name, config) {
const code = roomCode();
const room = {
id: code,
hostSessionId: hostSession.id,
config: {
...createDefaultConfig(),
...(config || {}),
},
players: [],
deck: [],
trump: null,
table: [],
attackerId: null,
defenderId: null,
phase: 'lobby',
winnerId: null,
loserId: null,
turnNumber: 0,
version: 0,
log: [],
created: Date.now(),
startedAt: null,
finishedAt: null,
lastActivity: Date.now(),
};
rooms.set(code, room);
return room;
}
function addPlayer(room, session, name) {
const cleanName =
String(name || '')
.trim()
.replace(/\s+/g, ' ')
.slice(0, 24) || 'Игрок';
const player = {
id: uid('p_'),
sessionId: session.id,
name: cleanName,
cards: [],
connected: true,
ready: false,
joinedAt: Date.now(),
};
room.players.push(player);
session.room = room.id;
session.playerId = player.id;
session.name = cleanName;
return player;
}
function getPlayer(room, id) {
return room.players.find(p => p.id === id);
}
function connectedPlayers(room) {
return room.players.filter(p => p.connected);
}
function ranksForDeck(deckSize) {
if (deckSize === 52) {
return [
'2', '3', '4', '5', '6', '7',
'8', '9', '10', 'J', 'Q', 'K', 'A'
];
}
return [
'6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'
];
}
const suits = ['♠', '♥', '♦', '♣'];
function createDeck(size) {
const deck = [];
for (const suit of suits) {
for (const rank of ranksForDeck(size)) {
deck.push({
id: uid('c_'),
suit,
rank,
});
}
}
return deck;
}
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function rankValue(rank) {
const values = {
'6': 6,
'7': 7,
'8': 8,
'9': 9,
'10': 10,
'J': 11,
'Q': 12,
'K': 13,
'A': 14,
};
return values[rank];
}
function isTrump(room, card) {
return !!card && card.suit === room.trump;
}
function canBeat(room, attack, defense) {
if (!attack || !defense) return false;
if (attack.suit === defense.suit) {
return rankValue(defense.rank) > rankValue(attack.rank);
}
return isTrump(room, defense) && !isTrump(room, attack);
}
function cardKey(card) {
return card.rank + card.suit;
}
function allTableRanks(room) {
const set = new Set();
for (const pair of room.table) {
if (pair.attack) set.add(cardKey(pair.attack));
if (pair.defense) set.add(cardKey(pair.defense));
}
return set;
}
function hasRankOnTable(room, rank) {
for (const pair of room.table) {
if (pair.attack && pair.attack.rank === rank) return true;
if (pair.defense && pair.defense.rank === rank) return true;
}
return false;
}
function getCurrentPair(room) {
return room.table.find(pair => pair.attack && !pair.defense);
}
function countUncovered(room) {
return room.table.filter(p => p.attack && !p.defense).length;
}
function canThrow(room, player, card) {
if (!player || !card) return false;
if (room.phase !== 'attack') return false;
if (room.attackerId !== player.id) {
if (room.config.throwMode === 'attacker') return false;
if (room.config.throwMode === 'neighbors') {
if (!isNeighborOfAttacker(room, player.id)) return false;
}
if (room.config.throwMode === 'participants') {
const participantIds = new Set();
for (const p of room.table) {
if (p.attack) participantIds.add(room.attackerId);
if (p.defense) participantIds.add(room.defenderId);
}
if (!participantIds.has(player.id)) return false;
}
}
if (!hasRankOnTable(room, card.rank)) return false;
const maxCards = getMaxThrow(room);
if (room.table.length >= maxCards) return false;
return true;
}
function getMaxThrow(room) {
if (room.config.maxThrow === 'defender') {
const defender = getPlayer(room, room.defenderId);
return Math.max(1, defender ? defender.cards.length : 6);
}
if (room.config.maxThrow === '6') return 6;
if (room.config.maxThrow === '5') return 5;
if (room.config.maxThrow === '4') return 4;
if (room.config.maxThrow === '3') return 3;
return 99;
}
function isNeighborOfAttacker(room, playerId) {
const index = room.players.findIndex(p => p.id === room.attackerId);
if (index === -1) return false;
const n = room.players.length;
const left = room.players[(index - 1 + n) % n];
const right = room.players[(index + 1) % n];
return playerId === left.id || playerId === right.id;
}
function isPlayerTurn(room, player) {
if (!player) return false;
if (room.phase === 'defense') {
return player.id === room.defenderId;
}
if (room.phase === 'attack') {
if (room.table.length === 0) {
return player.id === room.attackerId;
}
return true;
}
return false;
}
function removeCard(player, cardId) {
const index = player.cards.findIndex(c => c.id === cardId);
if (index === -1) return null;
return player.cards.splice(index, 1)[0];
}
function getCard(player, cardId) {
return player.cards.find(c => c.id === cardId);
}
function dealInitial(room) {
room.deck = shuffle(createDeck(room.config.deck));
room.trump = null;
const trumpCard = room.deck[room.deck.length - 1];
room.trump = trumpCard.suit;
for (const player of room.players) {
player.cards = [];
}
for (let i = 0; i < 6; i++) {
for (const player of room.players) {
if (room.deck.length > 0) {
player.cards.push(room.deck.shift());
}
}
}
chooseFirstAttacker(room);
room.table = [];
room.phase = 'attack';
room.turnNumber = 1;
room.log = [];
addLog(room, 'Игра началась.');
const attacker = getPlayer(room, room.attackerId);
if (attacker) {
addLog(room, `Первым ходит ${attacker.name}.`);
}
}
function chooseFirstAttacker(room) {
if (room.config.firstAttacker === 'random') {
const p =
room.players[Math.floor(Math.random() * room.players.length)];
room.attackerId = p.id;
room.defenderId = nextPlayer(room, p.id).id;
return;
}
if (room.config.firstAttacker === 'host') {
const hostPlayer =
room.players.find(p => p.sessionId === room.hostSessionId);
if (hostPlayer) {
room.attackerId = hostPlayer.id;
room.defenderId = nextPlayer(room, hostPlayer.id).id;
return;
}
}
let best = null;
for (const player of room.players) {
for (const card of player.cards) {
if (card.suit !== room.trump) continue;
if (!best || rankValue(card.rank) < rankValue(best.card.rank)) {
best = {
player,
card,
};
}
}
}
if (best) {
room.attackerId = best.player.id;
room.defenderId = nextPlayer(room, best.player.id).id;
} else {
const p = room.players[0];
room.attackerId = p.id;
room.defenderId = nextPlayer(room, p.id).id;
}
}
function nextPlayer(room, playerId) {
const active = room.players.filter(p => p.connected);
const index = active.findIndex(p => p.id === playerId);
if (index === -1) return active[0];
return active[(index + 1) % active.length];
}
function playerAfter(room, playerId) {
return nextPlayer(room, playerId);
}
function drawCards(room) {
if (!room.config.autoDraw) return;
const order = [];
const attacker = getPlayer(room, room.attackerId);
if (attacker) order.push(attacker);
const defender = getPlayer(room, room.defenderId);
if (defender && !order.includes(defender)) {
order.push(defender);
}
for (const p of room.players) {
if (!order.includes(p)) order.push(p);
}
for (const player of order) {
while (player.cards.length < 6 && room.deck.length > 0) {
player.cards.push(room.deck.shift());
}
}
}
function clearTable(room) {
room.table = [];
}
function finishRoundSuccess(room) {
const defender = getPlayer(room, room.defenderId);
if (!defender) return;
addLog(room, `${defender.name} отбился.`);
clearTable(room);
drawCards(room);
if (checkGameEnd(room)) return;
const oldDefender = defender;
room.attackerId = oldDefender.id;
room.defenderId = nextPlayer(room, oldDefender.id).id;
room.phase = 'attack';
room.turnNumber++;
addLog(
room,
`Теперь атакует ${oldDefender.name}.`
);
}
function finishRoundTake(room) {
const defender = getPlayer(room, room.defenderId);
if (!defender) return;
const cards = [];
for (const pair of room.table) {
if (pair.attack) cards.push(pair.attack);
if (pair.defense) cards.push(pair.defense);
}
defender.cards.push(...cards);
addLog(
room,
`${defender.name} взял ${cards.length} карт.`
);
clearTable(room);
drawCards(room);
if (checkGameEnd(room)) return;
const oldAttacker = getPlayer(room, room.attackerId);
room.attackerId = oldAttacker.id;
room.defenderId = nextPlayer(room, oldAttacker.id).id;
room.phase = 'attack';
room.turnNumber++;
addLog(
room,
`${oldAttacker.name} продолжает атаковать.`
);
}
function checkGameEnd(room) {
if (room.phase === 'lobby') return false;
const active = connectedPlayers(room);
if (active.length <= 1) {
if (active.length === 1) {
room.loserId = active[0].id;
room.winnerId = null;
}
room.phase = 'finished';
room.finishedAt = Date.now();
return true;
}
const noCards = active.filter(p => p.cards.length === 0);
if (noCards.length === 0) return false;
if (room.config.finish === 'last') {
const stillPlaying = active.filter(p => p.cards.length > 0);
if (stillPlaying.length === 1) {
room.loserId = stillPlaying[0].id;
room.winnerId = null;
room.phase = 'finished';
room.finishedAt = Date.now();
addLog(
room,
`${stillPlaying[0].name} остался последним с картами.`
);
return true;
}
}
if (room.config.finish === 'first') {
const winner = noCards[0];
room.winnerId = winner.id;
room.loserId = null;
room.phase = 'finished';
room.finishedAt = Date.now();
addLog(
room,
`${winner.name} первым остался без карт.`
);
return true;
}
if (room.config.finish === 'single') {
if (noCards.length === 1) {
room.winnerId = noCards[0].id;
room.phase = 'finished';
room.finishedAt = Date.now();
return true;
}
}
if (room.config.finish === 'draw') {
if (noCards.length >= 2) {
room.phase = 'finished';
room.winnerId = null;
room.loserId = null;
addLog(room, 'Игра закончилась ничьей.');
return true;
}
}
return false;
}
function addLog(room, text) {
room.log.push({
id: uid('l_'),
time: Date.now(),
text,
});
if (room.log.length > 100) {
room.log.shift();
}
}
function publicCard(card) {
return {
id: card.id,
suit: card.suit,
rank: card.rank,
};
}
function publicState(room, viewerId) {
const viewer = getPlayer(room, viewerId);
const players = room.players.map(p => ({
id: p.id,
name: p.name,
cards: p.id === viewerId
? p.cards.map(publicCard)
: p.cards.map(() => ({ hidden: true })),
cardCount: p.cards.length,
connected: p.connected,
ready: p.ready,
}));
return {
room: room.id,
viewerId,
host: room.players.find(
p => p.sessionId === room.hostSessionId
)?.id || null,
config: room.config,
players,
deckCount: room.deck.length,
trump: room.trump,
table: room.table.map(pair => ({
attack: pair.attack ? publicCard(pair.attack) : null,
defense: pair.defense ? publicCard(pair.defense) : null,
})),
attackerId: room.attackerId,
defenderId: room.defenderId,
phase: room.phase,
winnerId: room.winnerId,
loserId: room.loserId,
turnNumber: room.turnNumber,
log: room.log.slice(-40),
version: room.version,
maxThrow: getMaxThrow(room),
};
}
function startGame(room) {
if (room.phase !== 'lobby') return false;
const required = Number(room.config.players);
if (room.players.length < required) {
return false;
}
room.players = room.players.slice(0, required);
for (const player of room.players) {
player.ready = true;
}
room.startedAt = Date.now();
dealInitial(room);
return true;
}
function findSessionPlayer(req, res) {
const session = getSession(req, res);
if (!session.room || !session.playerId) {
return {
session,
room: null,
player: null,
};
}
const room = rooms.get(session.room);
if (!room) {
session.room = null;
session.playerId = null;
return {
session,
room: null,
player: null,
};
}
const player = getPlayer(room, session.playerId);
return {
session,
room,
player,
};
}
function action(room, player, type, data) {
room.lastActivity = Date.now();
if (room.phase === 'finished') {
return {
ok: false,
error: 'Игра уже закончена.',
};
}
if (type === 'playAttack') {
return playAttack(room, player, data.cardId);
}
if (type === 'playDefense') {
return playDefense(room, player, data.cardId);
}
if (type === 'throwCard') {
return throwCard(room, player, data.cardId);
}
if (type === 'translate') {
return translate(room, player, data.cardId);
}
if (type === 'take') {
if (room.defenderId !== player.id) {
return {
ok: false,
error: 'Сейчас брать карты может только защищающийся.',
};
}
finishRoundTake(room);
return {
ok: true,
};
}
if (type === 'done') {
if (room.defenderId !== player.id) {
return {
ok: false,
error: 'Завершить защиту может только защищающийся.',
};