feat: Schocken - erstes spielbares Grundgerüst (admin only)
This commit is contained in:
581
backend/src/tools/schocken/routes.js
Normal file
581
backend/src/tools/schocken/routes.js
Normal file
@@ -0,0 +1,581 @@
|
||||
const express = require('express');
|
||||
const db = require('../../db');
|
||||
const { authenticate } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// ── DB-Migration ──────────────────────────────────────────────────────────────
|
||||
(function migrate() {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schocken_games (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
players TEXT NOT NULL, -- JSON: [{id, username, order}]
|
||||
status TEXT NOT NULL DEFAULT 'lobby',
|
||||
-- status: lobby | half1 | half2 | endkampf | finished
|
||||
phase_status TEXT NOT NULL DEFAULT 'playing',
|
||||
-- phase_status: playing | finished
|
||||
stock INTEGER NOT NULL DEFAULT 15, -- Scheiben auf dem Stock
|
||||
player_chips TEXT NOT NULL DEFAULT '{}', -- JSON: {userId: chipCount}
|
||||
has_16th INTEGER, -- userId der die 16. Scheibe hat
|
||||
loser_h1 INTEGER, -- userId Verlierer Hälfte 1
|
||||
loser_h2 INTEGER, -- userId Verlierer Hälfte 2
|
||||
current_round TEXT, -- JSON: aktueller Rundenstand
|
||||
round_number INTEGER NOT NULL DEFAULT 1,
|
||||
beginner_id INTEGER, -- wer fängt die Runde an
|
||||
current_player_idx INTEGER NOT NULL DEFAULT 0,
|
||||
max_rolls INTEGER, -- vom Beginner bestimmt
|
||||
first_round INTEGER NOT NULL DEFAULT 1, -- 1 = erste Runde der Hälfte
|
||||
drink_losses TEXT NOT NULL DEFAULT '{}', -- {userId: count}
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
`);
|
||||
})();
|
||||
|
||||
// ── Würfellogik ───────────────────────────────────────────────────────────────
|
||||
function rollDice(count = 3) {
|
||||
return Array.from({ length: count }, () => Math.floor(Math.random() * 6) + 1);
|
||||
}
|
||||
|
||||
// Würfel klassifizieren
|
||||
function classifyDice(dice) {
|
||||
const sorted = [...dice].sort((a,b) => a-b);
|
||||
const [a,b,c] = sorted;
|
||||
|
||||
// Schock aus: 1-1-1
|
||||
if (a===1 && b===1 && c===1) return { type:'schock_aus', scheiben: 0, label:'Schock aus! 💀' };
|
||||
|
||||
// Julchen: 4-2-1 (unsortiert erkannt)
|
||||
if (dice.includes(1) && dice.includes(2) && dice.includes(4)) return { type:'julchen', scheiben:7, label:'Julchen! 🎉' };
|
||||
|
||||
// General: 3x gleich (außer 3x1)
|
||||
if (a===b && b===c) return { type:'general', scheiben:3, label:`General ${a}er! 🎲`, value: a };
|
||||
|
||||
// Schock: 2x Eins + andere
|
||||
if (a===1 && b===1) {
|
||||
const schockVal = c;
|
||||
const label = schockVal === 2 ? 'Schock doof 😅' : `Schock ${schockVal}!`;
|
||||
return { type:'schock', scheiben: schockVal, label, value: schockVal };
|
||||
}
|
||||
|
||||
// Straße: aufeinanderfolgende Zahlen (nur wenn alle 3 auf einmal gewürfelt)
|
||||
if (c-a===2 && b-a===1) return { type:'strasse', scheiben:2, label:`Straße ${a}-${b}-${c}!`, value: a };
|
||||
|
||||
// Normale Zahl
|
||||
return { type:'normal', scheiben:1, label:'Normal', value: sorted.reduce((s,v)=>s+v,0) };
|
||||
}
|
||||
|
||||
// Wertungsvergleich: wer hat schlechtere Würfel (bekommt Scheiben)
|
||||
// Gibt -1 wenn a schlechter, 1 wenn b schlechter, 0 bei echtem Gleichstand
|
||||
const TYPE_ORDER = ['normal','strasse','general','schock','julchen','schock_aus'];
|
||||
function compareResults(a, b) {
|
||||
const ai = TYPE_ORDER.indexOf(a.result.type);
|
||||
const bi = TYPE_ORDER.indexOf(b.result.type);
|
||||
if (ai !== bi) return ai < bi ? -1 : 1; // niedrigerer Typ = schlechter
|
||||
|
||||
// Gleicher Typ — Detailvergleich
|
||||
if (a.result.type === 'normal') {
|
||||
// Niedrigere Summe ist schlechter — aber beide haben immer 1 Scheibe, kein Unterschied im Typ
|
||||
// Tatsächlich keine Hierarchie bei normalen Würfen außer Scheiben=1
|
||||
return 0;
|
||||
}
|
||||
if (a.result.type === 'strasse') {
|
||||
if (a.result.value !== b.result.value) return a.result.value < b.result.value ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
if (a.result.type === 'schock') {
|
||||
if (a.result.value !== b.result.value) return a.result.value < b.result.value ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
if (a.result.type === 'general') {
|
||||
if (a.result.value !== b.result.value) return a.result.value < b.result.value ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sendPush(userId, title, message) {
|
||||
const cfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(userId);
|
||||
if (!cfg?.app_token || !cfg?.user_key) return;
|
||||
fetch('https://api.pushover.net/1/messages.json', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ token:cfg.app_token, user:cfg.user_key, title, message, priority:0 }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
const uid = req => req.user.id;
|
||||
|
||||
// ── Alle Spiele des Users ─────────────────────────────────────────────────────
|
||||
router.get('/', authenticate, (req, res) => {
|
||||
const me = uid(req);
|
||||
const games = db.prepare(`
|
||||
SELECT * FROM schocken_games
|
||||
WHERE players LIKE ? ORDER BY updated_at DESC LIMIT 20
|
||||
`).all(`%"id":${me}%`);
|
||||
res.json(games.map(g => ({
|
||||
...g,
|
||||
players: JSON.parse(g.players),
|
||||
player_chips: JSON.parse(g.player_chips),
|
||||
drink_losses: JSON.parse(g.drink_losses),
|
||||
current_round: g.current_round ? JSON.parse(g.current_round) : null,
|
||||
})));
|
||||
});
|
||||
|
||||
// ── Users für Einladen ────────────────────────────────────────────────────────
|
||||
router.get('/users', authenticate, (req, res) => {
|
||||
const me = uid(req);
|
||||
const isAdmin = req.user?.role === 'admin';
|
||||
const users = isAdmin
|
||||
? db.prepare('SELECT id, username FROM users WHERE id != ? ORDER BY username').all(me)
|
||||
: db.prepare('SELECT id, username FROM users WHERE id != ? AND hidden != 1 ORDER BY username').all(me);
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
// ── Einzelnes Spiel ───────────────────────────────────────────────────────────
|
||||
router.get('/:id', authenticate, (req, res) => {
|
||||
const me = uid(req);
|
||||
const game = db.prepare('SELECT * FROM schocken_games WHERE id=?').get(req.params.id);
|
||||
if (!game) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
const players = JSON.parse(game.players);
|
||||
if (!players.find(p => p.id === me)) return res.status(403).json({ error: 'Kein Zugriff' });
|
||||
res.json({
|
||||
...game,
|
||||
players,
|
||||
player_chips: JSON.parse(game.player_chips),
|
||||
drink_losses: JSON.parse(game.drink_losses),
|
||||
current_round: game.current_round ? JSON.parse(game.current_round) : null,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Neues Spiel erstellen ─────────────────────────────────────────────────────
|
||||
router.post('/', authenticate, (req, res) => {
|
||||
const me = uid(req);
|
||||
const { player_ids = [] } = req.body;
|
||||
if (player_ids.length < 1) return res.status(400).json({ error: 'Mindestens 1 weiterer Spieler' });
|
||||
|
||||
const allIds = [me, ...player_ids.filter(id => id !== me)];
|
||||
const userRows = db.prepare(`SELECT id, username FROM users WHERE id IN (${allIds.map(()=>'?').join(',')}) ORDER BY username`).all(...allIds);
|
||||
|
||||
// Reihenfolge: Ersteller zuerst, dann die anderen
|
||||
const ordered = [
|
||||
userRows.find(u => u.id === me),
|
||||
...allIds.slice(1).map(id => userRows.find(u => u.id === id)).filter(Boolean),
|
||||
];
|
||||
const players = ordered.map((u, i) => ({ id: u.id, username: u.username, order: i }));
|
||||
const playerChips = Object.fromEntries(players.map(p => [p.id, 0]));
|
||||
const drinkLosses = Object.fromEntries(players.map(p => [p.id, 0]));
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO schocken_games (players, stock, player_chips, drink_losses, beginner_id, status)
|
||||
VALUES (?, 15, ?, ?, ?, 'lobby')
|
||||
`).run(JSON.stringify(players), JSON.stringify(playerChips), JSON.stringify(drinkLosses), me);
|
||||
|
||||
// Pushover an andere Spieler
|
||||
const myName = ordered[0].username;
|
||||
for (const p of players.slice(1)) {
|
||||
sendPush(p.id, '🎲 Schocken', `${myName} lädt dich zu einer Runde Schocken ein!`);
|
||||
}
|
||||
|
||||
res.json({ id: result.lastInsertRowid });
|
||||
});
|
||||
|
||||
// ── Spiel starten (Lobby → Hälfte 1) ─────────────────────────────────────────
|
||||
router.post('/:id/start', authenticate, (req, res) => {
|
||||
const me = uid(req);
|
||||
const game = db.prepare('SELECT * FROM schocken_games WHERE id=?').get(req.params.id);
|
||||
if (!game) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
const players = JSON.parse(game.players);
|
||||
if (players[0].id !== me) return res.status(403).json({ error: 'Nur der Ersteller kann starten' });
|
||||
if (game.status !== 'lobby') return res.status(400).json({ error: 'Spiel läuft schon' });
|
||||
|
||||
db.prepare(`UPDATE schocken_games SET status='half1', first_round=1, beginner_id=?,
|
||||
current_player_idx=0, updated_at=datetime('now','localtime') WHERE id=?`).run(me, game.id);
|
||||
|
||||
for (const p of players.slice(1)) {
|
||||
sendPush(p.id, '🎲 Schocken', 'Das Spiel beginnt! Du bist dran.');
|
||||
}
|
||||
|
||||
res.json(getGame(game.id, me));
|
||||
});
|
||||
|
||||
// ── Würfeln ───────────────────────────────────────────────────────────────────
|
||||
router.post('/:id/roll', authenticate, (req, res) => {
|
||||
const me = uid(req);
|
||||
const { keep_dice = [], flip_sixes = false, go_dark = false } = req.body;
|
||||
// keep_dice: array of indices (0-2) der Würfel die stehen bleiben
|
||||
// flip_sixes: zwei Sechsen zu Eins umdrehen
|
||||
// go_dark: dunkel legen nach diesem Wurf
|
||||
|
||||
const game = db.prepare('SELECT * FROM schocken_games WHERE id=?').get(req.params.id);
|
||||
if (!game) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
|
||||
const players = JSON.parse(game.players);
|
||||
const activePlayers = getActivePlayers(game);
|
||||
const currentPlayer = activePlayers[game.current_player_idx % activePlayers.length];
|
||||
|
||||
if (!currentPlayer || currentPlayer.id !== me)
|
||||
return res.status(400).json({ error: 'Nicht dein Zug' });
|
||||
|
||||
let round = game.current_round ? JSON.parse(game.current_round) : initRound(game, activePlayers);
|
||||
const myState = round.playerStates[me];
|
||||
|
||||
if (myState.done) return res.status(400).json({ error: 'Du hast schon gewürfelt' });
|
||||
if (myState.dark) return res.status(400).json({ error: 'Du bist dunkel' });
|
||||
|
||||
// Flip sixes: nur wenn noch Würfe übrig und nicht letzter Wurf
|
||||
if (flip_sixes) {
|
||||
const sixes = myState.dice.reduce((acc, v, i) => v === 6 ? [...acc, i] : acc, []);
|
||||
if (sixes.length >= 2) {
|
||||
myState.dice[sixes[0]] = 1;
|
||||
myState.dice[sixes[1]] = 1;
|
||||
// Kostet keinen Wurf, braucht aber noch einen
|
||||
}
|
||||
}
|
||||
|
||||
// Würfeln
|
||||
const rollCount = myState.roll_count;
|
||||
const isFirstRound = !!game.first_round;
|
||||
|
||||
// Neue Würfel für nicht-stehen-gelassene
|
||||
if (!isFirstRound || rollCount === 0) {
|
||||
const newDice = rollDice(3 - keep_dice.length);
|
||||
let newIdx = 0;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (!keep_dice.includes(i)) myState.dice[i] = newDice[newIdx++];
|
||||
}
|
||||
}
|
||||
|
||||
myState.roll_count++;
|
||||
|
||||
// Straße nur gültig wenn im ersten Wurf (keine stehen gelassenen Würfel)
|
||||
const classified = classifyDice(myState.dice);
|
||||
if (classified.type === 'strasse' && (keep_dice.length > 0 || myState.roll_count > 1)) {
|
||||
// Straße ungültig — als normal werten
|
||||
classified.type = 'normal';
|
||||
classified.scheiben = 1;
|
||||
classified.label = 'Normal (Straße ungültig)';
|
||||
}
|
||||
|
||||
// Max rolls durch Beginner bestimmt
|
||||
const maxRolls = round.max_rolls || 3;
|
||||
const isDark = go_dark || myState.roll_count >= maxRolls || isFirstRound;
|
||||
const isDone = isDark || myState.roll_count >= maxRolls || isFirstRound;
|
||||
|
||||
myState.dark = go_dark || (myState.roll_count >= maxRolls && !isDone) || isFirstRound;
|
||||
myState.done = isDone;
|
||||
myState.result = isDone && !go_dark ? classified : null; // bei dunkel: result erst am Ende
|
||||
|
||||
// Wenn Beginner (erster Spieler): max_rolls setzen
|
||||
if (currentPlayer.id === round.beginner_id && round.max_rolls === null) {
|
||||
round.max_rolls = myState.roll_count;
|
||||
}
|
||||
|
||||
// Nächsten Spieler
|
||||
const nextIdx = (game.current_player_idx + 1) % activePlayers.length;
|
||||
const allDone = activePlayers.every(p => round.playerStates[p.id]?.done);
|
||||
|
||||
round.playerStates[me] = myState;
|
||||
|
||||
if (allDone) {
|
||||
// Alle dunkel aufdecken
|
||||
for (const p of activePlayers) {
|
||||
const ps = round.playerStates[p.id];
|
||||
if (!ps.result) {
|
||||
ps.result = classifyDice(ps.dice);
|
||||
// Straße bei dunkel-legen auch ungültig wenn keep_dice vorhanden (nicht trackbar nachträglich)
|
||||
}
|
||||
}
|
||||
round.phase = 'reveal';
|
||||
}
|
||||
|
||||
db.prepare(`UPDATE schocken_games SET current_round=?, current_player_idx=?,
|
||||
updated_at=datetime('now','localtime') WHERE id=?`)
|
||||
.run(JSON.stringify(round), allDone ? game.current_player_idx : nextIdx, game.id);
|
||||
|
||||
// Pushover an nächsten Spieler
|
||||
if (!allDone) {
|
||||
const next = activePlayers[nextIdx];
|
||||
if (next && next.id !== me) {
|
||||
sendPush(next.id, '🎲 Schocken', `${currentPlayer.username} hat gewürfelt — du bist dran!`);
|
||||
}
|
||||
}
|
||||
|
||||
res.json(getGame(game.id, me));
|
||||
});
|
||||
|
||||
// ── Runde auswerten ───────────────────────────────────────────────────────────
|
||||
router.post('/:id/evaluate', authenticate, (req, res) => {
|
||||
const me = uid(req);
|
||||
const game = db.prepare('SELECT * FROM schocken_games WHERE id=?').get(req.params.id);
|
||||
if (!game) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
|
||||
const players = JSON.parse(game.players);
|
||||
const activePlayers = getActivePlayers(game);
|
||||
if (players[0].id !== me && activePlayers[0]?.id !== me)
|
||||
return res.status(403).json({ error: 'Kein Zugriff' });
|
||||
|
||||
const round = JSON.parse(game.current_round);
|
||||
if (round.phase !== 'reveal') return res.status(400).json({ error: 'Noch nicht alle fertig' });
|
||||
|
||||
const playerChips = JSON.parse(game.player_chips);
|
||||
let stock = game.stock;
|
||||
|
||||
// Schock aus prüfen
|
||||
const schockAus = activePlayers.find(p => round.playerStates[p.id]?.result?.type === 'schock_aus');
|
||||
if (schockAus) {
|
||||
return handleSchockAus(game, round, activePlayers, res, me);
|
||||
}
|
||||
|
||||
// Bestes Ergebnis finden (höchster Typ)
|
||||
const results = activePlayers.map(p => ({
|
||||
player: p,
|
||||
state: round.playerStates[p.id],
|
||||
result: round.playerStates[p.id]?.result,
|
||||
rolls: round.playerStates[p.id]?.roll_count || 1,
|
||||
})).filter(r => r.result);
|
||||
|
||||
// Schlechtestes Ergebnis = bekommt Scheiben
|
||||
results.sort((a, b) => {
|
||||
const cmp = compareResults(a, b);
|
||||
if (cmp !== 0) return cmp; // -1 wenn a schlechter
|
||||
// Gleicher Typ: mehr Würfe = schlechter
|
||||
if (a.rolls !== b.rolls) return b.rolls - a.rolls; // mehr Würfe = schlechter (hinten)
|
||||
// Nachleger: später in Reihenfolge = schlechter
|
||||
return activePlayers.indexOf(b.player) - activePlayers.indexOf(a.player);
|
||||
});
|
||||
|
||||
const loser = results[0]; // schlechtestes Ergebnis
|
||||
const scheiben = loser.result.scheiben;
|
||||
|
||||
// Scheiben verteilen
|
||||
let fromStock = 0;
|
||||
let newStock = stock;
|
||||
if (stock >= scheiben) {
|
||||
fromStock = scheiben;
|
||||
newStock = stock - scheiben;
|
||||
} else {
|
||||
fromStock = stock;
|
||||
newStock = 0;
|
||||
}
|
||||
|
||||
const fromOthers = scheiben - fromStock;
|
||||
playerChips[loser.player.id] = (playerChips[loser.player.id] || 0) + scheiben;
|
||||
|
||||
// Wenn vom Stock nicht genug: Rest von aktivsten Spielern nehmen (noch nicht implementiert vereinfacht)
|
||||
if (fromOthers > 0) {
|
||||
// Scheiben von anderen Spielern umverteilen (von denen mit meisten Scheiben)
|
||||
const donors = activePlayers
|
||||
.filter(p => p.id !== loser.player.id && playerChips[p.id] > 0)
|
||||
.sort((a,b) => playerChips[b.id] - playerChips[a.id]);
|
||||
let remaining = fromOthers;
|
||||
for (const donor of donors) {
|
||||
if (remaining <= 0) break;
|
||||
const take = Math.min(playerChips[donor.id], remaining);
|
||||
playerChips[donor.id] -= take;
|
||||
remaining -= take;
|
||||
}
|
||||
}
|
||||
|
||||
// Hälfte/Endkampf vorbei? Prüfen ob Stock leer und nur einer Scheiben hat
|
||||
const playersWithChips = activePlayers.filter(p => playerChips[p.id] > 0);
|
||||
const phaseOver = newStock === 0 && playersWithChips.length <= 1;
|
||||
|
||||
let newStatus = game.status;
|
||||
let newPhaseStatus = 'playing';
|
||||
let loserH1 = game.loser_h1;
|
||||
let loserH2 = game.loser_h2;
|
||||
let has16th = game.has_16th;
|
||||
let eventMsg = null;
|
||||
|
||||
if (phaseOver) {
|
||||
const phaseLoser = playersWithChips[0] || loser.player;
|
||||
|
||||
if (game.status === 'half1') {
|
||||
// Verlierer H1 bestimmt
|
||||
loserH1 = phaseLoser.id;
|
||||
has16th = phaseLoser.id;
|
||||
newStatus = 'half2';
|
||||
// Chips reset, Stock reset
|
||||
Object.keys(playerChips).forEach(k => { playerChips[k] = 0; });
|
||||
newStock = 15;
|
||||
eventMsg = `${phaseLoser.username} hat die erste Hälfte verloren. Die erste Hälfte kostet nur Nerven! 😅`;
|
||||
// Pushover
|
||||
for (const p of players) {
|
||||
sendPush(p.id, '🎲 Schocken', eventMsg);
|
||||
}
|
||||
} else if (game.status === 'half2') {
|
||||
loserH2 = phaseLoser.id;
|
||||
if (loserH1 === loserH2) {
|
||||
// Doppelfeige!
|
||||
newStatus = 'finished';
|
||||
newPhaseStatus = 'finished';
|
||||
const dl = JSON.parse(game.drink_losses);
|
||||
dl[phaseLoser.id] = (dl[phaseLoser.id] || 0) + 1;
|
||||
eventMsg = `${phaseLoser.username} ist DOPPELFEIGE! 🪶 Die Runde ist vorbei.`;
|
||||
db.prepare(`UPDATE schocken_games SET status=?, phase_status=?, loser_h2=?, has_16th=?,
|
||||
player_chips=?, stock=?, drink_losses=?, current_round=NULL,
|
||||
updated_at=datetime('now','localtime') WHERE id=?`)
|
||||
.run(newStatus, newPhaseStatus, loserH2, has16th, JSON.stringify(playerChips),
|
||||
newStock, dl, game.id);
|
||||
for (const p of players) sendPush(p.id, '🎲 Schocken', eventMsg);
|
||||
return res.json(getGame(game.id, me));
|
||||
}
|
||||
// Endkampf
|
||||
newStatus = 'endkampf';
|
||||
Object.keys(playerChips).forEach(k => { playerChips[k] = 0; });
|
||||
newStock = 15;
|
||||
eventMsg = `${phaseLoser.username} verliert die zweite Hälfte! Endkampf: ${players.find(p=>p.id===loserH1)?.username} vs ${phaseLoser.username}`;
|
||||
for (const p of players) sendPush(p.id, '🎲 Schocken', eventMsg);
|
||||
} else if (game.status === 'endkampf') {
|
||||
newStatus = 'finished';
|
||||
newPhaseStatus = 'finished';
|
||||
const dl = JSON.parse(game.drink_losses);
|
||||
dl[phaseLoser.id] = (dl[phaseLoser.id] || 0) + 1;
|
||||
eventMsg = `${phaseLoser.username} hat den Endkampf verloren und muss die nächste Runde zahlen! 🍺`;
|
||||
db.prepare(`UPDATE schocken_games SET status=?, phase_status=?, loser_h2=?,
|
||||
player_chips=?, stock=?, drink_losses=?, current_round=NULL,
|
||||
updated_at=datetime('now','localtime') WHERE id=?`)
|
||||
.run(newStatus, newPhaseStatus, loserH2, JSON.stringify(playerChips), newStock, JSON.stringify(dl), game.id);
|
||||
for (const p of players) sendPush(p.id, '🎲 Schocken', eventMsg);
|
||||
return res.json(getGame(game.id, me));
|
||||
}
|
||||
}
|
||||
|
||||
// Nächste Runde vorbereiten
|
||||
const nextBeginner = loser.player;
|
||||
db.prepare(`UPDATE schocken_games SET player_chips=?, stock=?, status=?, loser_h1=?, loser_h2=?,
|
||||
has_16th=?, beginner_id=?, current_round=NULL, current_player_idx=0, first_round=0,
|
||||
updated_at=datetime('now','localtime') WHERE id=?`)
|
||||
.run(JSON.stringify(playerChips), newStock, newStatus, loserH1, loserH2,
|
||||
has16th, nextBeginner.id, game.id);
|
||||
|
||||
// Pushover an Verlierer der Runde (er fängt an)
|
||||
sendPush(loser.player.id, '🎲 Schocken', `Du hast die Runde verloren und fängst die nächste an! (${scheiben} Scheibe${scheiben!==1?'n':''})`);
|
||||
|
||||
res.json({ ...getGame(game.id, me), round_result: { loser: loser.player, scheiben, eventMsg } });
|
||||
});
|
||||
|
||||
// ── Becher umdrehen (Runde bereit signalisieren) ──────────────────────────────
|
||||
router.post('/:id/ready', authenticate, (req, res) => {
|
||||
const me = uid(req);
|
||||
const game = db.prepare('SELECT * FROM schocken_games WHERE id=?').get(req.params.id);
|
||||
if (!game) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
|
||||
const players = JSON.parse(game.players);
|
||||
const playerChips = JSON.parse(game.player_chips);
|
||||
const activePlayers = getActivePlayers(game);
|
||||
|
||||
// Spieler der eigentlich raus wäre aber trotzdem ready drückt → wird wieder aktiv (Falle!)
|
||||
const isActuallyActive = activePlayers.find(p => p.id === me);
|
||||
const isOut = !isActuallyActive && playerChips[me] === 0 && game.stock === 0;
|
||||
|
||||
let round = game.current_round ? JSON.parse(game.current_round) : null;
|
||||
if (!round) {
|
||||
round = initRound(game, isOut ? [...activePlayers, players.find(p=>p.id===me)] : activePlayers);
|
||||
}
|
||||
|
||||
round.ready = round.ready || {};
|
||||
round.ready[me] = true;
|
||||
|
||||
// Wenn isOut und drückt trotzdem: wird als "gefangen" markiert
|
||||
if (isOut) {
|
||||
round.trapped = round.trapped || [];
|
||||
if (!round.trapped.includes(me)) round.trapped.push(me);
|
||||
}
|
||||
|
||||
db.prepare(`UPDATE schocken_games SET current_round=?, updated_at=datetime('now','localtime') WHERE id=?`)
|
||||
.run(JSON.stringify(round), game.id);
|
||||
|
||||
res.json(getGame(game.id, me));
|
||||
});
|
||||
|
||||
// ── Hilfsfunktionen ───────────────────────────────────────────────────────────
|
||||
function getActivePlayers(game) {
|
||||
const players = JSON.parse(game.players);
|
||||
const playerChips = JSON.parse(game.player_chips);
|
||||
const stock = game.stock;
|
||||
|
||||
if (game.status === 'endkampf') {
|
||||
return players.filter(p => p.id === game.loser_h1 || p.id === game.loser_h2);
|
||||
}
|
||||
|
||||
// Wenn Stock noch voll oder erste Runde: alle mitspielen
|
||||
if (stock > 0) return players;
|
||||
|
||||
// Stock leer: nur Spieler mit Scheiben
|
||||
return players.filter(p => playerChips[p.id] > 0);
|
||||
}
|
||||
|
||||
function initRound(game, activePlayers) {
|
||||
const playerStates = {};
|
||||
for (const p of activePlayers) {
|
||||
playerStates[p.id] = {
|
||||
dice: [null, null, null],
|
||||
roll_count: 0,
|
||||
done: false,
|
||||
dark: false,
|
||||
result: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
phase: 'rolling',
|
||||
beginner_id: game.beginner_id,
|
||||
max_rolls: game.first_round ? 1 : null,
|
||||
ready: {},
|
||||
trapped: [],
|
||||
playerStates,
|
||||
};
|
||||
}
|
||||
|
||||
function handleSchockAus(game, round, activePlayers, res, me) {
|
||||
const players = JSON.parse(game.players);
|
||||
const playerChips = JSON.parse(game.player_chips);
|
||||
|
||||
// Alle Scheiben (Stock + Spieler) → schlechtester Spieler
|
||||
const allChips = Object.values(playerChips).reduce((s,v) => s+v, 0) + game.stock;
|
||||
|
||||
// Wer hat die wenigsten Punkte (außer dem der Schock aus hat)?
|
||||
const schockAusPlayer = activePlayers.find(p => round.playerStates[p.id]?.result?.type === 'schock_aus');
|
||||
const others = activePlayers.filter(p => p.id !== schockAusPlayer?.id);
|
||||
|
||||
// Vereinfacht: Schock aus Spieler hat gewonnen, alle Scheiben gehen an den schlechtesten der anderen
|
||||
// (Schock aus steht über allem)
|
||||
const newChips = Object.fromEntries(Object.keys(playerChips).map(k => [k, 0]));
|
||||
|
||||
// Verlierer = wer die wenigsten Punkte hat unter den anderen
|
||||
// Bei nur einem anderen: der verliert
|
||||
const phaseLoser = others.length > 0 ? others[others.length - 1] : schockAusPlayer;
|
||||
newChips[phaseLoser.id] = allChips;
|
||||
|
||||
let newStatus = game.status;
|
||||
let loserH1 = game.loser_h1;
|
||||
let has16th = game.has_16th;
|
||||
|
||||
if (game.status === 'half1') {
|
||||
loserH1 = phaseLoser.id;
|
||||
has16th = phaseLoser.id;
|
||||
newStatus = 'half2';
|
||||
Object.keys(newChips).forEach(k => { newChips[k] = 0; });
|
||||
const eventMsg = `SCHOCK AUS! ${phaseLoser.username} verliert die erste Hälfte! Die erste Hälfte kostet nur Nerven! 😅`;
|
||||
for (const p of players) sendPush(p.id, '🎲 Schocken', eventMsg);
|
||||
db.prepare(`UPDATE schocken_games SET player_chips=?, stock=15, status=?, loser_h1=?, has_16th=?,
|
||||
beginner_id=?, current_round=NULL, current_player_idx=0, first_round=1,
|
||||
updated_at=datetime('now','localtime') WHERE id=?`)
|
||||
.run(JSON.stringify(newChips), newStatus, loserH1, has16th, phaseLoser.id, game.id);
|
||||
}
|
||||
|
||||
res.json({ ...getGame(game.id, me), schock_aus: true, phase_loser: phaseLoser });
|
||||
}
|
||||
|
||||
function getGame(id, requesterId) {
|
||||
const game = db.prepare('SELECT * FROM schocken_games WHERE id=?').get(id);
|
||||
if (!game) return null;
|
||||
return {
|
||||
...game,
|
||||
players: JSON.parse(game.players),
|
||||
player_chips: JSON.parse(game.player_chips),
|
||||
drink_losses: JSON.parse(game.drink_losses),
|
||||
current_round: game.current_round ? JSON.parse(game.current_round) : null,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user