handleCellClick(r, c)}
style={{
width:cellSize, height:cellSize, background:bg,
borderRadius:1, cursor:clic?'pointer':'default',
transition:'background 0.08s', border, boxSizing:'border-box',
display:'flex', alignItems:'center', justifyContent:'center',
fontSize: cellSize > 18 ? 10 : 7, lineHeight:1, userSelect:'none',
}}
>{content}
);
};
return (
{/* Scoreboard */}
🌫️{fogCount}
{oppName?.toUpperCase()}
Pkt {oppScore}
{/* Mine-Alert */}
{mineAlert && (
💣
{mineAlert.player === myId
? `💥 Mine! ${mineAlert.destroyedOwn ? mineAlert.destroyedOwn + ' deiner Felder zerstört. ' : ''}Zug verloren.`
: `💥 ${oppName} trat auf eine Mine! ${mineAlert.destroyedOpp ? mineAlert.destroyedOpp + ' deiner Felder zerstört!' : 'Kein Schaden.'} 😈`}
setMineAlert(null)} style={{ ...S.btn('#666666', true), padding:'2px 8px' }}>✕
)}
{/* Gewinner-Banner */}
{banner && (
{banner.text}
)}
{/* Zug-Status */}
{game.status === 'active' && (
{!isMyTurn && !pending && (
⏳ {oppName} ist dran…
)}
{isMyTurn && !pending && (
▶ Wähle ein Feld — Vorsicht vor Minen! 💣
)}
{pending && (
✦ R{pending.row+1} S{pending.col+1} — bestätigen?
setPending(null)} style={S.btn('#666666', true)}>✕
{moving ? '…' : '✓ Ja'}
)}
)}
{/* Legende */}
{[['⭐','Gold +3','#ffe66d'],['💣','Mine −3','#ff6b9d'],['🪨','Felsen','rgba(255,255,255,0.35)'],['🌫️','Nebel','rgba(255,255,255,0.25)']].map(([icon,label,color]) => (
{icon}
{label}
))}
{/* Grid */}
{grid.map((row, r) => row.map((_, c) => renderCell(r, c)))}
);
}
// ── Spielzeile ────────────────────────────────────────────────────────────────
function GameRow({ g, myId, isAdmin, onSelect, onDelete }) {
const isMyTurn = g.status === 'active' && g.current_turn === myId;
const opp = g.owner_id === myId ? g.opp_name : g.owner_name;
const finished = g.status === 'finished' && (g.move_count || 0) >= 2;
let result = null;
if (g.status === 'finished') {
if (!g.winner_id) result = { label:'Unentschieden', color:'#ffe66d' };
else if (g.winner_id === myId) result = { label:'Gewonnen 🎉', color:MY_COLOR };
else result = { label:'Verloren', color:OPP_COLOR };
}
return (
+ Neues Spiel starten
{active.length > 0 && <>
LAUFENDE SPIELE ({active.length})
{active.map(g =>
)}
>}
{finished.length > 0 && <>
BEENDETE SPIELE
{finished.map(g =>
)}
>}
{games.length === 0 && (
Noch keine Spiele — starte eines!
)}
);
}
// ── Hauptkomponente ───────────────────────────────────────────────────────────
export default function HexWars({ toast }) {
const [games, setGames] = useState([]);
const [activeId, setActiveId] = useState(null);
const [game, setGame] = useState(null);
const [showNew, setShowNew] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [loading, setLoading] = useState(true);
const [reloadKey, setReloadKey] = useState(0);
const myId = getMyId();
const isAdmin = getMyRole() === 'admin';
const loadGames = useCallback(async () => {
try { setGames(await api('/tools/gebietseroberung')); }
catch { toast?.('Fehler beim Laden', 'error'); }
finally { setLoading(false); }
}, []);
useEffect(() => { loadGames(); }, [loadGames]);
useEffect(() => {
if (!activeId) { setGame(null); return; }
api(`/tools/gebietseroberung/${activeId}`).then(setGame).catch(() => {});
}, [activeId]);
// Polling wenn Gegner dran
useEffect(() => {
if (!activeId || !game || !myId) return;
if (game.status !== 'active' || game.current_turn === myId) return;
const iv = setInterval(async () => {
const g = await api(`/tools/gebietseroberung/${activeId}`).catch(() => null);
if (!g) return;
setGame(g);
if (g.current_turn === myId) toast?.('Du bist dran! ⬡');
}, 10000);
return () => clearInterval(iv);
}, [activeId, game, myId]);
const handleMove = async (row, col) => {
const updated = await api(`/tools/gebietseroberung/${activeId}/move`, { body: { row, col } });
setGame(updated);
setGames(prev => prev.map(g => g.id === updated.id ? { ...g, ...updated } : g));
};
const handleResign = async () => {
if (!window.confirm('Wirklich aufgeben?')) return;
await api(`/tools/gebietseroberung/${activeId}/resign`, { body: {} });
const updated = await api(`/tools/gebietseroberung/${activeId}`);
setGame(updated);
setGames(prev => prev.map(g => g.id === updated.id ? { ...g, ...updated } : g));
toast('Du hast aufgegeben.');
};
const handleDelete = async (id) => {
if (!window.confirm('Spiel wirklich löschen?')) return;
try {
await api(`/tools/gebietseroberung/${id}`, { method: 'DELETE' });
setGames(prev => prev.filter(g => g.id !== id));
setReloadKey(k => k + 1);
toast('Spiel gelöscht.');
} catch(e) { toast?.(e.message || 'Fehler', 'error'); }
};
if (loading) return