import { useState, useEffect, useCallback } from 'react';
import { api, S } from '../lib.js';
// Testmodus: API-Call als anderer User (nur Admin, nur Schocken)
async function apiAs(userId, path, opts = {}) {
const token = localStorage.getItem('sk_token');
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` };
if (userId) headers['X-Schocken-Test-As'] = String(userId);
const res = await fetch(`/api${path}`, {
method: opts.method || (opts.body ? 'POST' : 'GET'),
headers,
body: opts.body ? JSON.stringify(opts.body) : undefined,
});
if (!res.ok) { const e = await res.json().catch(()=>({error:'Fehler'})); throw new Error(e.error||'Fehler'); }
return res.json();
}
const MY_COLOR = '#4ecdc4';
function getMyId() {
try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).id; } catch { return null; }
}
function getMyRole() {
try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).role; } catch { return null; }
}
// ── Würfel-Visualisierung ─────────────────────────────────────────────────────
const DOTS = {
1: [[50,50]],
2: [[25,25],[75,75]],
3: [[25,25],[50,50],[75,75]],
4: [[25,25],[75,25],[25,75],[75,75]],
5: [[25,25],[75,25],[50,50],[25,75],[75,75]],
6: [[25,25],[75,25],[25,50],[75,50],[25,75],[75,75]],
};
function Die({ value, kept, selected, onClick, dark, size=56 }) {
const dots = value ? DOTS[value] : [];
const bg = dark ? 'rgba(40,40,60,0.9)' : kept ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.08)';
const border = selected ? '2px solid #4ecdc4' : kept ? '2px solid rgba(78,205,196,0.4)' : '1px solid rgba(255,255,255,0.15)';
return (
{dark &&
🎲
}
{!dark && dots.map(([x,y],i) => (
40?10:7, height:size>40?10:7,
borderRadius:'50%', background:'#fff',
}}/>
))}
{!value && !dark &&
?
}
);
}
// ── Spieler-Zeile ─────────────────────────────────────────────────────────────
function PlayerRow({ player, chips, has16th, isMe, isCurrent, result, drinkLosses, game }) {
const isLoserH1 = game.has_16th === player.id;
return (
{isCurrent && ▶ }
{player.username}
{isMe && (du) }
{isLoserH1 && 🪶 }
{drinkLosses > 0 && (
🍺×{drinkLosses}
)}
0?'#ff6b9d':MY_COLOR,fontFamily:'Space Mono,monospace',fontSize:18,fontWeight:700,lineHeight:1}}>{chips}
Scheiben
{result && (
{result.label}
)}
);
}
// ── Würfelbereich ─────────────────────────────────────────────────────────────
function DiceArea({ game, myId, onRoll, onReady, onEvaluate }) {
const [selectedKeep, setSelectedKeep] = useState([]);
const [goDark, setGoDark] = useState(false);
const [flipSixes, setFlipSixes] = useState(false);
const [rolling, setRolling] = useState(false);
const round = game.current_round;
const players = game.players;
const activePlayers = getActivePlayers(game);
const myState = round?.playerStates?.[myId];
const currentPlayer = activePlayers[game.current_player_idx % activePlayers.length];
const isMyTurn = currentPlayer?.id === myId;
const allReady = activePlayers.every(p => round?.ready?.[p.id]);
const allDone = round?.phase === 'reveal';
const isFirstRound = !!game.first_round;
const myDice = myState?.dice || [null, null, null];
const myRolls = myState?.roll_count || 0;
const maxRolls = round?.max_rolls || 3;
const canFlipSixes = myDice.filter(d => d===6).length >= 2 && myRolls < maxRolls - 1;
// Becher umdrehen Phase — warte bis alle ready
if (!round || !allReady) {
const iMReady = round?.ready?.[myId];
const readyCount = Object.keys(round?.ready || {}).length;
const activeCount = activePlayers.length;
return (
{readyCount}/{activeCount} haben den Becher umgedreht
{!iMReady && (
🎲 Becher umdrehen
)}
{iMReady && (
✓ Becher umgedreht — warte auf andere…
)}
{isFirstRound && iMReady && readyCount === activeCount && (
Alle bereit — Becher werden gleichzeitig gehoben!
)}
);
}
// Auswertungsphase — alle Würfel sichtbar
if (allDone) {
const isFirstPlayer = activePlayers[0]?.id === myId || players[0]?.id === myId;
return (
🎲 Alle Becher oben — wer hat was?
{/* Alle Spieler mit Würfeln und Ergebnis */}
{activePlayers.map(p => {
const ps = round.playerStates?.[p.id];
const isP = p.id === myId;
return (
{p.username}{isP?' (du)':''}
{ps?.dark && 🌑 war dunkel }
{ps?.roll_count && {ps.roll_count}. Wurf }
{(ps?.dice||[null,null,null]).map((d,i) => )}
{ps?.result && (
{ps.result.label} {ps.result.scheiben > 0 ? `→ ${ps.result.scheiben} Scheibe${ps.result.scheiben!==1?'n':''}` : ''}
)}
);
})}
{isFirstPlayer && (
✓ Scheiben verteilen
)}
{!isFirstPlayer && (
Warte auf {activePlayers[0]?.username} zum Auswerten…
)}
);
}
// Mein Würfeln
return (
{/* Status */}
{isMyTurn ? `▶ Dein Wurf (${myRolls}/${maxRolls||'?'})` : `⏳ ${currentPlayer?.username} würfelt…`}
{/* Würfel */}
{myDice.map((d, i) => (
0 && !myState?.done ? () => {
setSelectedKeep(prev =>
prev.includes(i) ? prev.filter(x=>x!==i) : [...prev,i]
);
} : undefined}
size={64}
/>
))}
{/* Optionen */}
{isMyTurn && !myState?.done && (
)}
{myState?.done && !allDone && (
{myState.dark ? '🎲 Du bist dunkel — warte auf andere…' : `✓ Du stehst im ${myRolls}. — warte auf andere…`}
)}
{/* Andere Spieler Status */}
{activePlayers.filter(p => p.id !== myId).map(p => {
const ps = round.playerStates?.[p.id];
return (
{ps?.done ? '✓' : '⏳'} {p.username} {ps?.dark && '🌑'}
);
})}
);
}
// ── Schockstock ───────────────────────────────────────────────────────────────
function Schockstock({ total=15, stock, players, chips }) {
const used = total - stock;
return (
SCHOCKSTOCK
{stock} verbleibend
{Array.from({length:16}).map((_,i) => {
const is16th = i === 15;
const isUsed = !is16th && i >= stock;
return (
);
})}
);
}
// ── Neue Runde Button ─────────────────────────────────────────────────────────
function NewRoundButton({ game, myId, onReady }) {
// Der Beginner der neuen Runde startet mit Becher umdrehen
const isStarter = game.beginner_id === myId;
return (
Neue Runde — {isStarter ? 'du fängst an!' : `${game.players.find(p=>p.id===game.beginner_id)?.username} fängt an`}
🎲 Becher umdrehen
);
}
// ── Spielende ─────────────────────────────────────────────────────────────────
function GameOver({ game }) {
const dl = game.drink_losses;
const loser = game.players.find(p => dl[p.id] > 0);
const isDoppelfeige = game.loser_h1 === game.loser_h2;
return (
{isDoppelfeige ? '🪶🪶' : '🍺'}
{isDoppelfeige ? (
DOPPELFEIGE!
) : (
RUNDE VORBEI
)}
{loser && (
{loser.username} zahlt die nächste Runde! 🍺
)}
GETRÄNKE-STATISTIK
{game.players.map(p => (
{p.username}
0?'#f59e0b':MY_COLOR}}>🍺 {dl[p.id]||0}
))}
);
}
// ── Aktive Spieler berechnen (Frontend) ───────────────────────────────────────
function getActivePlayers(game) {
const { players, player_chips, stock, status, loser_h1, loser_h2 } = game;
if (status === 'endkampf') {
return players.filter(p => p.id === loser_h1 || p.id === loser_h2);
}
if (stock > 0) return players;
return players.filter(p => player_chips[p.id] > 0);
}
// ── Neues Spiel Modal ─────────────────────────────────────────────────────────
function NewGameModal({ onClose, onCreate, toast }) {
const [users, setUsers] = useState([]);
const [picked, setPicked] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => { api('/tools/schocken/users').then(setUsers).catch(()=>{}); }, []);
const toggle = id => setPicked(p => p.includes(id) ? p.filter(x=>x!==id) : [...p,id]);
const create = async () => {
if (!picked.length) return;
setLoading(true);
try {
const r = await api('/tools/schocken', { body: { player_ids: picked } });
toast('Spiel erstellt! 🎲');
onCreate(r.id);
} catch(e) { toast(e.message||'Fehler','error'); }
finally { setLoading(false); }
};
return (
NEUES SPIEL
✕
MITSPIELER WÄHLEN
{users.map(u => (
toggle(u.id)} style={{
display:'flex',alignItems:'center',gap:10,padding:'8px 10px',
cursor:'pointer',borderRadius:8,marginBottom:4,
background: picked.includes(u.id) ? 'rgba(78,205,196,0.1)' : 'rgba(255,255,255,0.03)',
border: picked.includes(u.id) ? '1px solid rgba(78,205,196,0.3)' : '1px solid rgba(255,255,255,0.06)',
}}>
{picked.includes(u.id)?'☑':'☐'}
{u.username}
))}
Abbrechen
{loading ? 'Erstelle…' : `Starten (${picked.length+1} Spieler)`}
);
}
// ── Spielliste ────────────────────────────────────────────────────────────────
function GameList({ games, myId, onSelect, onNew, onDelete, isAdmin }) {
const active = games.filter(g => g.status !== 'finished');
const finished = games.filter(g => g.status === 'finished');
const statusLabel = s => ({
lobby:'Lobby', half1:'1. Hälfte', half2:'2. Hälfte', endkampf:'Endkampf', finished:'Beendet'
}[s]||s);
const GameRow = ({ g }) => {
const others = g.players.filter(p=>p.id!==myId).map(p=>p.username).join(', ');
return (
onSelect(g.id)} style={{flex:1,background:'none',border:'none',cursor:'pointer',textAlign:'left',padding:0}}>
🎲 mit {others}
{g.players.length} Spieler · {statusLabel(g.status)}
{isAdmin && (
{
e.stopPropagation();
if (!window.confirm('Spiel löschen?')) return;
try {
await apiAs(myId, `/tools/schocken/${g.id}/cancel`, { body:{} });
onDelete(g.id);
} catch(e) { alert(e.message||'Fehler'); }
}} style={{...S.btn('#ff6b9d',true),flexShrink:0}}>🗑
)}
);
};
return (
+ Neues Spiel starten
{active.length > 0 && <>
LAUFEND ({active.length})
{active.map(g=>
)}
>}
{finished.length > 0 && <>
BEENDET
{finished.map(g=>)}
>}
{games.length===0 && (
Noch keine Spiele — starte eines!
)}
);
}
// ── Spielansicht ──────────────────────────────────────────────────────────────
function GameView({ game, myId, onRefresh, toast, isAdmin=false, onSeatChange }) {
const players = game.players;
const chips = game.player_chips;
const dl = game.drink_losses;
const round = game.current_round;
const activePlayers = getActivePlayers(game);
const currentPlayer = activePlayers[game.current_player_idx % activePlayers.length];
const handleRoll = async (keepDice, flipSixes, goDark) => {
await apiAs(myId, `/tools/schocken/${game.id}/roll`, {
body: { keep_dice: keepDice, flip_sixes: flipSixes, go_dark: goDark }
});
onRefresh();
};
const handleReady = async () => {
await apiAs(myId, `/tools/schocken/${game.id}/ready`, { body: {} });
onRefresh();
};
const handleEvaluate = async () => {
const result = await apiAs(myId, `/tools/schocken/${game.id}/evaluate`, { body: {} });
if (result.round_result?.eventMsg) toast(result.round_result.eventMsg);
onRefresh();
};
const handleStart = async () => {
await apiAs(myId, `/tools/schocken/${game.id}/start`, { body: {} });
onRefresh();
};
const statusLabel = { lobby:'Lobby', half1:'1. Hälfte', half2:'2. Hälfte', endkampf:'Endkampf', finished:'Beendet' };
return (
{/* Phase-Header */}
{statusLabel[game.status]}
Runde {game.round_number}
{/* Testmodus-Switcher (nur Admin) */}
{isAdmin && (
🧪 TESTMODUS — SPIELER WECHSELN
{players.map(p => (
onSeatChange(p.id)}
style={{...S.btn(myId===p.id?'#f59e0b':'#666666',true),fontSize:10}}>
{p.username}{myId===p.id?' ✓':''}
))}
)}
{/* Schockstock */}
{/* Spieler */}
{/* Lobby */}
{game.status==='lobby' && (
{players[0]?.id===myId ? (
🎲 Spiel starten
) : (
Warte auf {players[0]?.username} um das Spiel zu starten…
)}
)}
{/* Spielfeld */}
{game.status!=='lobby' && game.status!=='finished' && (
)}
{/* Spielende */}
{game.status==='finished' && }
);
}
// ── Hauptkomponente ───────────────────────────────────────────────────────────
export default function Schocken({ toast }) {
const [games, setGames] = useState([]);
const [activeId, setActiveId] = useState(null);
const [game, setGame] = useState(null);
const [showNew, setShowNew] = useState(false);
const [loading, setLoading] = useState(true);
const [testSeat, setTestSeat] = useState(null); // null = eigener Account, sonst userId
const myRealId = getMyId();
const isAdmin = getMyRole() === 'admin';
// Im Testmodus: als anderen Spieler agieren
const myId = (isAdmin && testSeat) ? testSeat : myRealId;
const loadGames = useCallback(async () => {
try { setGames(await api('/tools/schocken')); }
catch { toast?.('Fehler beim Laden','error'); }
finally { setLoading(false); }
}, []);
const loadGame = useCallback(async () => {
if (!activeId) { setGame(null); return; }
try { setGame(await api(`/tools/schocken/${activeId}`)); }
catch {}
}, [activeId]);
useEffect(() => { loadGames(); }, [loadGames]);
useEffect(() => { loadGame(); }, [loadGame]);
// Polling wenn Spiel aktiv
useEffect(() => {
if (!activeId) return;
const iv = setInterval(loadGame, 5000);
return () => clearInterval(iv);
}, [activeId, loadGame]);
if (loading) return
Lade…
;
return (
{activeId && (
{setActiveId(null);setGame(null);loadGames();}} style={S.btn('#666666',true)}>← Zurück
)}
🎲 SCHOCKEN
{activeId && game?.status !== 'finished' && isAdmin && (
{
if (!window.confirm('Spiel wirklich abbrechen? Keine Punkte werden vergeben.')) return;
try {
await apiAs(myRealId, `/tools/schocken/${activeId}/cancel`, { body:{} });
setActiveId(null); setGame(null); loadGames();
toast('Spiel abgebrochen.');
} catch(e) { toast(e.message||'Fehler','error'); }
}} style={S.btn('#ff6b9d', true)}>✕ Abbrechen
)}
{!activeId
?
setShowNew(true)} onDelete={id=>setGames(prev=>prev.filter(g=>g.id!==id))} isAdmin={isAdmin}/>
: game && myId
? {setTestSeat(id);}} />
: Lade…
}
{showNew && setShowNew(false)} toast={toast} onCreate={id=>{setShowNew(false);setActiveId(id);loadGames();}}/>}
);
}