diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 734b59e..a94a0ec 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1589,7 +1589,7 @@ const SEARCH_TOOL_INDEX = [
{type:'tool',label:'Whiteboard',icon:'π',sub:'Werkzeuge',id:'whiteboard',keywords:['whiteboard','zeichnen','canvas','board','draw','skizze','malen','zeichen']},
{type:'tool',label:'Dev-Tools',icon:'π§',sub:'Werkzeuge',id:'devtools',keywords:['entwickler','developer','tools','werkzeuge']},
{type:'tool',label:'Paywall-Killer',icon:'π',sub:'Werkzeuge',id:'paywallkiller',keywords:['paywall','archiv','archive','artikel','zeitung','bild','waz','heise','spiegel','zeit','bypass','umgehen','lesen','gesperrt','bezahlschranke']},
- {type:'tool',label:'Gebietseroberung',icon:'πΊοΈ',sub:'Freizeit',id:'gebietseroberung',keywords:['spiel','game','gebietseroberung','multiplayer','gebiet','strategie','raster','einladen','duell']},
+ {type:'tool',label:'Hex Wars',icon:'⬑',sub:'Freizeit',id:'gebietseroberung',keywords:['hex wars','spiel','game','multiplayer','gebiet','strategie','raster','einladen','duell','gebietseroberung']},
{type:'tool',label:'Media',icon:'π¬',sub:'Freizeit',id:'media',keywords:['kino','film','movie','streaming','demnΓ€chst','favoriten','cinema']},
{type:'tool',label:'Kino β Aktuell',icon:'π¬',sub:'Media',id:'media',keywords:['kino','kinocharts','charts','laufend','now playing']},
{type:'tool',label:'Kino β DemnΓ€chst',icon:'π',sub:'Media',id:'media',keywords:['demnΓ€chst','neustart','upcoming','vorschau','kino']},
diff --git a/frontend/src/toolRegistry.js b/frontend/src/toolRegistry.js
index 5f5fe0d..e97fb26 100644
--- a/frontend/src/toolRegistry.js
+++ b/frontend/src/toolRegistry.js
@@ -66,8 +66,8 @@ export const TOOLS = [
{
id: 'gebietseroberung',
Icon: GeoIcon,
- label: 'Gebietseroberung',
- navLabel: 'Gebiete',
+ label: 'Hex Wars',
+ navLabel: 'Hex Wars',
group: 'Freizeit',
component: Gebietseroberung,
},
diff --git a/frontend/src/tools/gebietseroberung.jsx b/frontend/src/tools/gebietseroberung.jsx
index b114968..0fdb6d7 100644
--- a/frontend/src/tools/gebietseroberung.jsx
+++ b/frontend/src/tools/gebietseroberung.jsx
@@ -2,51 +2,58 @@ import { useState, useEffect, useCallback } from 'react';
import { api, S } from '../lib.js';
const GRID = 20;
-const OWNER_COLOR = '#4ecdc4';
-const OPP_COLOR = '#ff6b9d';
-const NEUTRAL_COLOR = 'rgba(255,255,255,0.06)';
-const HOVER_COLOR = 'rgba(255,255,255,0.18)';
+const MY_COLOR = '#4ecdc4';
+const OPP_COLOR = '#ff6b9d';
+
+// User-ID aus JWT lesen (kein API-Call nΓΆtig)
+function getMyId() {
+ try {
+ const token = localStorage.getItem('sk_token');
+ if (!token) return null;
+ return JSON.parse(atob(token.split('.')[1])).id || null;
+ } catch { return null; }
+}
// ββ SpielerklΓ€rung ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function HelpModal({ onClose }) {
return (
-
+
- πΊοΈ Wie funktioniert's?
+ ⬑ Wie funktioniert's?
- β SchlieΓen
+ β SchlieΓen
- Dein Ziel: Am Ende mehr Felder als dein Gegner besitzen.
+ Dein Ziel: Am Ende mehr Felder als dein Gegner besitzen.
So geht's:
- Jeder Spieler startet in einer Ecke des 20Γ20 Rasters mit einem Startfeld.
- Abwechselnd wΓ€hlt jeder eine neutrale (graue) Zelle ,
- die direkt an sein eigenes Gebiet grenzt β oben, unten, links oder rechts.
- Diese Zelle gehΓΆrt dann ihm.
+ Jeder Spieler startet in einer Ecke des 20Γ20 Rasters. Abwechselnd wΓ€hlst du eine
+ neutrale (graue) Zelle ,
+ die direkt an dein Gebiet grenzt β oben, unten, links oder rechts.
Taktik:
- Breite dich schnell aus β aber pass auf: Wenn du deinen Gegner
- einmauerst , kann er nicht mehr wachsen.
- Blockieren ist genauso wichtig wie Expandieren!
+ Breite dich aus β aber mauere deinen Gegner
+ ein , bevor er dich einsperrt!
+ Blockieren ist genauso wichtig wie Expandieren.
Ende:
- Wenn keine neutralen Felder mehr erreichbar sind, gewinnt wer
- mehr Felder besitzt. Du bekommst eine Pushover-Benachrichtigung ,
- wenn du dran bist.
+ Wenn niemand mehr expandieren kann, gewinnt wer mehr Felder hat.
+ Du kriegst eine Pushover-Benachrichtigung wenn du dran bist.
-
- π‘
Klicke auf ein leuchtendes Feld um deinen Zug zu machen. Nur Felder neben deinem Gebiet sind wΓ€hlbar.
+
+ π‘ Leuchtende Felder sind anklickbar β nur Felder neben deinem Gebiet.
@@ -69,7 +76,7 @@ function NewGameModal({ onClose, onCreated, toast }) {
setLoading(true);
try {
const res = await api('/tools/gebietseroberung', { body: { opponent_id: Number(oppId) } });
- toast('Spiel erstellt! Dein Gegner wurde benachrichtigt πΊοΈ');
+ toast('Spiel erstellt! Dein Gegner wurde benachrichtigt ⬑');
onCreated(res.id);
} catch(e) {
toast(e.message || 'Fehler beim Erstellen', 'error');
@@ -89,11 +96,7 @@ function NewGameModal({ onClose, onCreated, toast }) {
β
GEGNER WΓHLEN
-
setOppId(e.target.value)}
- style={{ ...S.inp, marginBottom:16 }}
- >
+ setOppId(e.target.value)} style={{ ...S.inp, marginBottom:16 }}>
-- Benutzer wΓ€hlen --
{users.map(u => {u.username} )}
@@ -110,115 +113,102 @@ function NewGameModal({ onClose, onCreated, toast }) {
// ββ Spielfeld βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function GameBoard({ game, myId, onMove, toast }) {
- const [hover, setHover] = useState(null);
+ const [hover, setHover] = useState(null);
const [moving, setMoving] = useState(false);
- const grid = JSON.parse(game.grid);
+ const grid = JSON.parse(game.grid);
const isMyTurn = game.current_turn === myId;
const opponent = game.owner_id === myId ? game.opponent_id : game.owner_id;
- const myColor = game.owner_id === myId ? OWNER_COLOR : OPP_COLOR;
- const oppColor = game.owner_id === myId ? OPP_COLOR : OWNER_COLOR;
+ const myColor = game.owner_id === myId ? MY_COLOR : OPP_COLOR;
+ const oppColor = game.owner_id === myId ? OPP_COLOR : MY_COLOR;
+ const oppName = game.owner_id === myId ? game.opp_name : game.owner_name;
- const isAdjacent = (r, c) => {
- const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
- return dirs.some(([dr,dc]) => {
- const nr = r+dr, nc = c+dc;
- return nr>=0 && nr=0 && nc [[-1,0],[1,0],[0,-1],[0,1]].some(([dr,dc]) => {
+ const nr = r+dr, nc = c+dc;
+ return nr>=0 && nr=0 && nc isMyTurn && game.status === 'active' && grid[r][c] === 0 && isAdjacent(r, c);
+ const canClick = (r, c) =>
+ isMyTurn && game.status === 'active' && grid[r][c] === 0 && isAdj(r, c);
const handleClick = async (r, c) => {
if (!canClick(r, c) || moving) return;
setMoving(true);
- try {
- await onMove(r, c);
- } finally { setMoving(false); }
+ try { await onMove(r, c); } finally { setMoving(false); }
};
const myCount = grid.flat().filter(v => v === myId).length;
const oppCount = grid.flat().filter(v => v === opponent).length;
const neutral = grid.flat().filter(v => v === 0).length;
- // Gewinner-Banner
let banner = null;
if (game.status === 'finished') {
- if (!game.winner_id) {
- banner = { text: 'Unentschieden! π€', color: '#ffe66d' };
- } else if (game.winner_id === myId) {
- banner = { text: 'Du hast gewonnen! π', color: '#4ecdc4' };
- } else {
- banner = { text: 'Du hast verloren.', color: '#ff6b9d' };
- }
+ if (!game.winner_id) banner = { text:'Unentschieden! π€', color:'#ffe66d' };
+ else if (game.winner_id===myId) banner = { text:'Du hast gewonnen! π', color:MY_COLOR };
+ else banner = { text:'Du hast verloren.', color:OPP_COLOR };
}
- // Grid-ZellengrΓΆΓe responsiv
- const isMob = window.innerWidth < 600;
- const cellSize = isMob ? Math.floor((window.innerWidth - 48) / GRID) : 24;
+ // ZellgrΓΆΓe: passt sich ans Viewport an, nie horizontales Scrollen
+ const vw = Math.min(window.innerWidth, 600);
+ const cellSize = Math.floor((vw - 48) / GRID); // 48px = padding + gap
return (
- {/* Status-Leiste */}
-
-
-
DU
-
{myCount}
+ {/* Scoreboard */}
+
+
-
- {neutral} neutral
+
+ {neutral} frei
-
-
- {game.owner_id === myId ? game.opp_name : game.owner_name}
-
-
{oppCount}
+
- {/* Gewinner-Banner */}
+ {/* Banner */}
{banner && (
- {banner.text}
-
+ }}>{banner.text}
)}
{/* Zug-Hinweis */}
{game.status === 'active' && (
- {isMyTurn ? (moving ? 'Zug wird gespeichertβ¦' : 'βΆ Dein Zug β klicke ein angrenzendes Feld') : 'β³ Warte auf Gegnerβ¦'}
+ {isMyTurn
+ ? (moving ? 'Zug wird gespeichertβ¦' : 'βΆ Dein Zug')
+ : `β³ ${oppName} ist dranβ¦`}
)}
- {/* Grid */}
+ {/* Grid β immer passend ohne Scrollen */}
{grid.map((row, r) => row.map((cell, c) => {
- const key = `${r}-${c}`;
- const clickable = canClick(r, c);
- const isHovered = hover === key && clickable;
- let bg = NEUTRAL_COLOR;
- if (cell === myId) bg = `${myColor}55`;
+ const key = `${r}-${c}`;
+ const clic = canClick(r, c);
+ const hov = hover === key && clic;
+ let bg = 'rgba(255,255,255,0.05)';
+ if (cell === myId) bg = `${myColor}55`;
if (cell === opponent) bg = `${oppColor}55`;
- if (isHovered) bg = HOVER_COLOR;
- if (clickable && !isHovered) bg = `${myColor}25`;
-
+ if (clic && !hov) bg = `${myColor}22`;
+ if (hov) bg = `${myColor}66`;
return (
setHover(key)}
onMouseLeave={() => setHover(null)}
style={{
- width: cellSize, height: cellSize,
- background: bg,
- borderRadius: 2,
- cursor: clickable ? 'pointer' : 'default',
- transition: 'background 0.1s',
+ width:cellSize, height:cellSize,
+ background:bg, borderRadius:1,
+ cursor: clic ? 'pointer' : 'default',
+ transition:'background 0.08s',
+ border: clic ? `1px solid ${myColor}55` : '1px solid transparent',
boxSizing:'border-box',
- border: clickable ? `1px solid ${myColor}66` : '1px solid transparent',
}}
/>
);
@@ -249,30 +238,25 @@ function GameList({ games, myId, onSelect, onNew }) {
const GameRow = ({ g }) => {
const isMyTurn = g.status === 'active' && g.current_turn === myId;
- const opp = g.owner_id === myId ? g.opp_name : g.owner_name;
- const grid = JSON.parse(g.grid);
+ const opp = g.owner_id === myId ? g.opp_name : g.owner_name;
+ const grid = JSON.parse(g.grid);
const myCount = grid.flat().filter(v => v === myId).length;
const oppCount = grid.flat().filter(v => v !== myId && v !== 0).length;
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:'#4ecdc4' };
- else result = { label:'Verloren', color:'#ff6b9d' };
+ 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 (
-
onSelect(g.id)}
- style={{
- ...S.card, width:'100%', textAlign:'left', cursor:'pointer',
- padding:'12px 14px', marginBottom:8, display:'flex',
- alignItems:'center', gap:12, border: isMyTurn
- ? '1px solid rgba(78,205,196,0.4)'
- : '1px solid rgba(255,255,255,0.07)',
- }}
- >
+ onSelect(g.id)} style={{
+ ...S.card, width:'100%', textAlign:'left', cursor:'pointer',
+ padding:'12px 14px', marginBottom:8, display:'flex', alignItems:'center', gap:12,
+ border: isMyTurn ? '1px solid rgba(78,205,196,0.4)' : '1px solid rgba(255,255,255,0.07)',
+ }}>
- {isMyTurn && βΆ }
+ {isMyTurn && βΆ }
vs {opp}
@@ -281,7 +265,7 @@ function GameList({ games, myId, onSelect, onNew }) {
{result
?
{result.label}
- :
+ :
{isMyTurn ? 'Du bist dran' : 'Gegner dran'}
}
@@ -294,21 +278,17 @@ function GameList({ games, myId, onSelect, onNew }) {
+ Neues Spiel starten
- {active.length > 0 && (
- <>
- LAUFENDE SPIELE ({active.length})
- {active.map(g => )}
- >
- )}
- {finished.length > 0 && (
- <>
- BEENDETE SPIELE
- {finished.map(g => )}
- >
- )}
+ {active.length > 0 && <>
+ LAUFEND ({active.length})
+ {active.map(g => )}
+ >}
+ {finished.length > 0 && <>
+ BEENDET
+ {finished.map(g => )}
+ >}
{games.length === 0 && (
- Noch keine Spiele. Starte eines!
+ Noch keine Spiele β starte eines!
)}
@@ -316,27 +296,23 @@ function GameList({ games, myId, onSelect, onNew }) {
}
// ββ Hauptkomponente βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-export default function Gebietseroberung({ toast }) {
- const [games, setGames] = useState([]);
- const [activeId, setActiveId] = useState(null);
- const [game, setGame] = useState(null);
- const [myId, setMyId] = useState(null);
- const [showNew, setShowNew] = useState(false);
- const [showHelp, setShowHelp] = useState(false);
- const [loading, setLoading] = useState(true);
+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);
- // Eigene User-ID aus JWT ermitteln
- useEffect(() => {
- api('/auth/me').then(u => setMyId(u.id)).catch(() => {});
- }, []);
+ // User-ID direkt aus JWT β kein API-Call
+ const myId = getMyId();
const loadGames = useCallback(async () => {
try {
const gs = await api('/tools/gebietseroberung');
setGames(gs);
- } catch(e) {
- toast?.('Fehler beim Laden', 'error');
- } finally { setLoading(false); }
+ } catch { toast?.('Fehler beim Laden', 'error'); }
+ finally { setLoading(false); }
}, []);
useEffect(() => { loadGames(); }, [loadGames]);
@@ -347,42 +323,32 @@ export default function Gebietseroberung({ toast }) {
api(`/tools/gebietseroberung/${activeId}`).then(setGame).catch(() => {});
}, [activeId]);
- // Polling wenn aktives Spiel lΓ€uft und Gegner dran ist
+ // Polling wenn Gegner dran ist
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) {
- setGame(g);
- if (g.current_turn === myId) {
- toast?.('Du bist dran! πΊοΈ');
- }
- }
+ 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) => {
- try {
- const updated = await api(`/tools/gebietseroberung/${activeId}/move`, { body: { row, col } });
- setGame(updated);
- // Spielliste auch aktualisieren
- setGames(prev => prev.map(g => g.id === updated.id ? { ...g, ...updated } : g));
- } catch(e) {
- toast?.(e.message || 'Fehler beim Zug', 'error');
- }
+ 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;
- try {
- 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.');
- } catch(e) { toast?.(e.message, 'error'); }
+ 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.');
};
if (loading) return (
@@ -392,38 +358,28 @@ export default function Gebietseroberung({ toast }) {
);
return (
-
+
{/* Header */}
-
+
{activeId && (
{ setActiveId(null); setGame(null); }} style={S.btn('#666666', true)}>
β ZurΓΌck
)}
-
- πΊοΈ GEBIETSEROBERUNG
-
+ ⬑ HEX WARS
setShowHelp(true)} style={S.btn('#ffe66d', true)}>? Regeln
{activeId && game?.status === 'active' && (
β Aufgeben
)}
- {/* Inhalt */}
- {!activeId ? (
-
setActiveId(id)}
- onNew={() => setShowNew(true)}
- />
- ) : (
- game && myId
+ {!activeId
+ ? setShowNew(true)} />
+ : game && myId
?
: Lade Spielβ¦
- )}
+ }
- {/* Modals */}
{showNew && (
setShowNew(false)}
diff --git a/frontend/src/tools/linkliste.jsx b/frontend/src/tools/linkliste.jsx
index 7ddefee..bfd862c 100644
--- a/frontend/src/tools/linkliste.jsx
+++ b/frontend/src/tools/linkliste.jsx
@@ -16,7 +16,7 @@ const TOOL_ENTRIES = [
{ url:'tool://linkliste', label:'π Linkliste', group:'Werkzeuge' },
{ url:'tool://codeschnipsel', label:'> Code-Schnipsel', group:'Werkzeuge' },
{ url:'tool://media', label:'π¬ Media / Kino', group:'Freizeit' },
- { url:'tool://gebietseroberung', label:'πΊοΈ Gebietseroberung', group:'Freizeit' },
+ { url:'tool://gebietseroberung', label:'⬑ Hex Wars', group:'Freizeit' },
{ url:'tool://devtools', label:'π§ Dev-Tools (alle)', group:'Dev-Tools' },
{ url:'tool://devtools?sub=cron', label:'β± Crontab', group:'Dev-Tools' },
{ url:'tool://devtools?sub=elektro', label:'β‘ Elektro', group:'Dev-Tools' },