@@ -2,51 +2,58 @@ import { useState, useEffect, useCallback } from 'react';
import { api , S } from '../lib.js' ;
import { api , S } from '../lib.js' ;
const GRID = 20 ;
const GRID = 20 ;
const OWNER _COLOR = '#4ecdc4' ;
const MY _COLOR = '#4ecdc4' ;
const OPP _COLOR = '#ff6b9d' ;
const OPP _COLOR = '#ff6b9d' ;
const NEUTRAL _COLOR = 'rgba(255,255,255,0.06)' ;
const HOVER _COLOR = 'rgba(255,255,255,0.18)' ;
// 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 ────────────────────────────────────────────────────────────
// ── Spielerklärung ────────────────────────────────────────────────────────────
function HelpModal ( { onClose } ) {
function HelpModal ( { onClose } ) {
return (
return (
< div style = { {
< div style = { {
position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,0.75)' ,
position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,0.75)' ,
display : 'flex' , alignItems : 'center' , justifyContent : 'center' , zIndex : 1000 ,
display : 'flex' , alignItems : 'center' , justifyContent : 'center' , zIndex : 1000 , padding : 20 ,
padding : 20 ,
} } >
} } >
< div style = { { ... S . card , maxWidth : 440 , width : '100%' , position : 'relative' } } >
< div style = { { ... S . card , maxWidth : 440 , width : '100%' } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 16 } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 16 } } >
< span style = { { color : '#fff' , fontFamily : 'Space Mono,monospace' , fontSize : 15 , fontWeight : 700 } } >
< span style = { { color : '#fff' , fontFamily : 'Space Mono,monospace' , fontSize : 15 , fontWeight : 700 } } >
🗺 ️ Wie funktioniert 's?
⬡ Wie funktioniert 's?
</span>
</span>
<button onClick={onClose} style={{ ... S.btn(' # ff6b9d ', true) } }>✕ Schließen</button>
<button onClick={onClose} style={S.btn(' # ff6b9d ', true)}>✕ Schließen</button>
</div>
</div>
<div style={{ color:' rgba ( 255 , 255 , 255 , 0.75 ) ', fontSize:13, lineHeight:1.8, fontFamily:' monospace ' }}>
<div style={{ color:' rgba ( 255 , 255 , 255 , 0.75 ) ', fontSize:13, lineHeight:1.8, fontFamily:' monospace ' }}>
<p style={{ marginTop:0 }}>
<p style={{ marginTop:0 }}>
<strong style={{ color:OWNER _COLOR }}>Dein Ziel:</strong> Am Ende mehr Felder als dein Gegner besitzen.
<strong style={{ color:MY _COLOR }}>Dein Ziel:</strong> Am Ende mehr Felder als dein Gegner besitzen.
</p>
</p>
<p>
<p>
<strong style={{ color:' # fff ' }}>So geht' s : < / strong > < br / >
<strong style={{ color:' # fff ' }}>So geht' s : < / strong > < br / >
Jeder Spieler startet in einer Ecke des 20 × 20 Rasters mit einem Startfeld .
Jeder Spieler startet in einer Ecke des 20 × 20 Rasters . Abwechselnd wählst du eine
Abwechselnd wählt jeder eine < strong style = { { color : 'rgba(255,255,255,0.9)' } } > neutrale ( graue ) Zelle < / strong > ,
< strong style = { { color : 'rgba(255,255,255,0.9)' } } > neutrale ( graue ) Zelle < / strong > ,
die direkt an sein eigenes Gebiet grenzt — oben , unten , links oder rechts .
die direkt an dein Gebiet grenzt — oben , unten , links oder rechts .
Diese Zelle gehört dann ihm .
< / p >
< / p >
< p >
< p >
< strong style = { { color : '#fff' } } > Taktik : < / strong > < br / >
< strong style = { { color : '#fff' } } > Taktik : < / strong > < br / >
Breite dich schnell aus — aber pass auf : Wenn du deinen Gegner
Breite dich aus — aber mauere deinen Gegner
< strong style = { { color : OPP _COLOR } } > einmauerst < / strong > , kann er n icht mehr wachsen .
< strong style = { { color : OPP _COLOR } } > ein < / strong > , bevor er d ich einsperrt !
Blockieren ist genauso wichtig wie Expandieren !
Blockieren ist genauso wichtig wie Expandieren .
< / p >
< / p >
< p >
< p >
< strong style = { { color : '#fff' } } > Ende : < / strong > < br / >
< strong style = { { color : '#fff' } } > Ende : < / strong > < br / >
Wenn keine neutralen Felder mehr erreichbar sind , gewinnt wer
Wenn niemand mehr expandieren kann , gewinnt wer mehr Felder hat .
mehr Felder besitzt . Du bekomm st eine < strong > Pushover - Benachrichtigung < / strong > ,
Du krieg st eine < strong > Pushover - Benachrichtigung < / strong > wenn du dran bist .
wenn du dran bist .
< / p >
< / p >
< div style = { { background : 'rgba(255,255,255,0.04)' , borderRadius : 8 , padding : '10px 14px' , marginTop : 8 , border : '1px solid rgba(255,255,255,0.08)' } } >
< div style = { {
💡 < em > Klicke auf ein leuchtendes Feld um deinen Zug zu machen . Nur Felder neben deinem Gebiet sind wählbar . < / em >
background : 'rgba(255,255,255,0.04)' , borderRadius : 8 ,
padding : '10px 14px' , marginTop : 8 , border : '1px solid rgba(255,255,255,0.08)' ,
} } >
💡 < em > Leuchtende Felder sind anklickbar — nur Felder neben deinem Gebiet . < / em >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
@@ -69,7 +76,7 @@ function NewGameModal({ onClose, onCreated, toast }) {
setLoading ( true ) ;
setLoading ( true ) ;
try {
try {
const res = await api ( '/tools/gebietseroberung' , { body : { opponent _id : Number ( oppId ) } } ) ;
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 ) ;
onCreated ( res . id ) ;
} catch ( e ) {
} catch ( e ) {
toast ( e . message || 'Fehler beim Erstellen' , 'error' ) ;
toast ( e . message || 'Fehler beim Erstellen' , 'error' ) ;
@@ -89,11 +96,7 @@ function NewGameModal({ onClose, onCreated, toast }) {
< button onClick = { onClose } style = { S . btn ( '#666666' , true ) } > ✕ < / button >
< button onClick = { onClose } style = { S . btn ( '#666666' , true ) } > ✕ < / button >
< / div >
< / div >
< label style = { { ... S . head , display : 'block' , marginBottom : 6 } } > GEGNER WÄHLEN < / label >
< label style = { { ... S . head , display : 'block' , marginBottom : 6 } } > GEGNER WÄHLEN < / label >
< select
< select value = { oppId } onChange = { e => setOppId ( e . target . value ) } style = { { ... S . inp , marginBottom : 16 } } >
value = { oppId }
onChange = { e => setOppId ( e . target . value ) }
style = { { ... S . inp , marginBottom : 16 } }
>
< option value = '' > -- Benutzer wählen -- < / option >
< option value = '' > -- Benutzer wählen -- < / option >
{ users . map ( u => < option key = { u . id } value = { u . id } > { u . username } < / option > ) }
{ users . map ( u => < option key = { u . id } value = { u . id } > { u . username } < / option > ) }
< / select >
< / select >
@@ -116,109 +119,96 @@ function GameBoard({ game, myId, onMove, toast }) {
const grid = JSON . parse ( game . grid ) ;
const grid = JSON . parse ( game . grid ) ;
const isMyTurn = game . current _turn === myId ;
const isMyTurn = game . current _turn === myId ;
const opponent = game . owner _id === myId ? game . opponent _id : game . owner _id ;
const opponent = game . owner _id === myId ? game . opponent _id : game . owner _id ;
const myColor = game . owner _id === myId ? OWNER _COLOR : OPP _COLOR ;
const myColor = game . owner _id === myId ? MY _COLOR : OPP _COLOR ;
const oppColor = game . owner _id === myId ? OPP _COLOR : OWNER _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 isAdj = ( r , c ) => [ [ - 1 , 0 ] , [ 1 , 0 ] , [ 0 , - 1 ] , [ 0 , 1 ] ] . some ( ( [ dr , dc ] ) => {
const dirs = [ [ - 1 , 0 ] , [ 1 , 0 ] , [ 0 , - 1 ] , [ 0 , 1 ] ] ;
return dirs . some ( ( [ dr , dc ] ) => {
const nr = r + dr , nc = c + dc ;
const nr = r + dr , nc = c + dc ;
return nr >= 0 && nr < GRID & & nc > = 0 && nc < GRID & & grid [ nr ] [ nc ] = = = myId ;
return nr >= 0 && nr < GRID & & nc > = 0 && nc < GRID & & grid [ nr ] [ nc ] = = = myId ;
} ) ;
} ) ;
} ;
const canClick = ( r , c ) = > 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 ) => {
const handleClick = async ( r , c ) => {
if ( ! canClick ( r , c ) || moving ) return ;
if ( ! canClick ( r , c ) || moving ) return ;
setMoving ( true ) ;
setMoving ( true ) ;
try {
try { await onMove ( r , c ) ; } finally { setMoving ( false ) ; }
await onMove ( r , c ) ;
} finally { setMoving ( false ) ; }
} ;
} ;
const myCount = grid . flat ( ) . filter ( v => v === myId ) . length ;
const myCount = grid . flat ( ) . filter ( v => v === myId ) . length ;
const oppCount = grid . flat ( ) . filter ( v => v === opponent ) . length ;
const oppCount = grid . flat ( ) . filter ( v => v === opponent ) . length ;
const neutral = grid . flat ( ) . filter ( v => v === 0 ) . length ;
const neutral = grid . flat ( ) . filter ( v => v === 0 ) . length ;
// Gewinner-Banner
let banner = null ;
let banner = null ;
if ( game . status === 'finished' ) {
if ( game . status === 'finished' ) {
if ( ! game . winner _id ) {
if ( ! game . winner _id ) banner = { text : 'Unentschieden! 🤝' , color : '#ffe66d' } ;
banner = { text : 'Unentschied en! 🤝 ' , color : '#ffe66d' } ;
else if ( game . winner _id === myId ) banner = { text : 'Du hast gewonn en! 🎉 ' , color : MY _COLOR } ;
} else if ( game . winner _id === myId ) {
else banner = { text : 'Du hast verloren.' , color : OPP _COLOR } ;
banner = { text : 'Du hast gewonnen! 🎉' , color : '#4ecdc4' } ;
} else {
banner = { text : 'Du hast verloren.' , color : '#ff6b9d' } ;
}
}
}
// Grid- Zellen größe responsiv
// Zellgröße: passt sich ans Viewport an, nie horizontales Scrollen
const isMob = window . innerWidth < 600 ;
const vw = Math . min ( window. innerWidth , 600 ) ;
const cellSize = isMob ? Math . floor ( ( window . innerWidth - 48 ) / GRID ) : 24 ;
const cellSize = Math . floor ( ( vw - 48 ) / GRID ) ; // 48px = padding + gap
return (
return (
< div >
< div >
{ /* Status-Leiste */ }
{ /* Scoreboard */ }
< div style = { { display : 'flex' , gap : 10 , alignItems : 'center' , marginBottom : 12 , flexWrap : 'wrap' } } >
< div style = { { display : 'flex' , gap : 8 , alignItems : 'center' , marginBottom : 10 } } >
< div style = { { ... S . card , padding : '8px 14 px' , flex : 1 , minWidth : 120 } } >
< div style = { { ... S . card , padding : '8px 12 px' , flex : 1 } } >
< div style = { { color : myColor , fontFamily : 'monospace' , fontSize : 11 , marginBottom : 2 } } > DU < / div >
< div style = { { color : myColor , fontFamily : 'monospace' , fontSize : 10 , marginBottom : 2 } } > DU < / div >
< div style = { { color : '#fff' , fontFamily : 'Space Mono,monospace' , fontSize : 20 , fontWeight : 700 } } > { myCount } < / div >
< div style = { { color : '#fff' , fontFamily : 'Space Mono,monospace' , fontSize : 22 , fontWeight : 700 } } > { myCount } < / div >
< / div >
< / div >
< div style = { { textAlign : 'center' , color : 'rgba(255,255,255,0.4 )' , fontFamily : 'monospace' , fontSize : 11 } } >
< div style = { { textAlign : 'center' , color : 'rgba(255,255,255,0.35 )' , fontFamily : 'monospace' , fontSize : 10 , lineHeight : 1.5 } } >
{ neutral } neutral
{ neutral } < br / > frei
< / div >
< / div >
< div style = { { ... S . card , padding : '8px 14 px' , flex : 1 , minWidth : 120 , textAlign : 'right' } } >
< div style = { { ... S . card , padding : '8px 12 px' , flex : 1 , textAlign : 'right' } } >
< div style = { { color : oppColor , fontFamily : 'monospace' , fontSize : 11 , marginBottom : 2 } } >
< div style = { { color : oppColor , fontFamily : 'monospace' , fontSize : 10 , marginBottom : 2 } } > { oppName } < / div >
{ game . owner _id === myId ? game . opp _nam e : game . owner _name }
< div style = { { color : '#fff' , fontFamily : 'Space Mono,monospace' , fontSiz e: 22 , fontWeight : 700 } } > { oppCount } < / div >
< / div >
< div style = { { color : '#fff' , fontFamily : 'Space Mono,monospace' , fontSize : 20 , fontWeight : 700 } } > { oppCount } < / div >
< / div >
< / div >
< / div >
< / div >
{ /* Gewinner- Banner */ }
{ /* Banner */ }
{ banner && (
{ banner && (
< div style = { {
< div style = { {
background : ` ${ banner . color } 22 ` , border : ` 1px solid ${ banner . color } 55 ` ,
background : ` ${ banner . color } 22 ` , border : ` 1px solid ${ banner . color } 55 ` ,
borderRadius : 8 , padding : '10px 14px' , marginBottom : 12 ,
borderRadius : 8 , padding : '10px 14px' , marginBottom : 10 ,
color : banner . color , fontFamily : 'Space Mono,monospace' , fontSize : 14 ,
color : banner . color , fontFamily : 'Space Mono,monospace' , fontSize : 14 ,
textAlign : 'center' , fontWeight : 700 ,
textAlign : 'center' , fontWeight : 700 ,
} } >
} } > { banner . text } < / div >
{ banner . text }
< / div >
) }
) }
{ /* Zug-Hinweis */ }
{ /* Zug-Hinweis */ }
{ game . status === 'active' && (
{ game . status === 'active' && (
< div style = { {
< div style = { {
color : isMyTurn ? myColor : 'rgba(255,255,255,0.4 )' ,
color : isMyTurn ? myColor : 'rgba(255,255,255,0.35 )' ,
fontFamily : 'monospace' , fontSize : 11 , marginBottom : 10 , textAlign : 'center' ,
fontFamily : 'monospace' , fontSize : 11 , marginBottom : 8 , textAlign : 'center' ,
} } >
} } >
{ isMyTurn ? ( moving ? 'Zug wird gespeichert…' : '▶ Dein Zug — klicke ein angrenzendes Feld' ) : '⏳ Warte auf Gegner…' }
{ isMyTurn
? ( moving ? 'Zug wird gespeichert…' : '▶ Dein Zug' )
: ` ⏳ ${ oppName } ist dran… ` }
< / div >
< / div >
) }
) }
{ /* Grid */ }
{ /* Grid — immer passend ohne Scrollen */ }
< div style = { {
< div style = { {
display : 'grid' ,
display : 'grid' ,
gridTemplateColumns : ` repeat( ${ GRID } , ${ cellSize } px) ` ,
gridTemplateColumns : ` repeat( ${ GRID } , ${ cellSize } px) ` ,
gap : 1 ,
gap : 1 ,
background : 'rgba(255,255,255,0.04)' ,
background : 'rgba(255,255,255,0.04)' ,
borderRadius : 8 ,
borderRadius : 8 ,
padding : 4 ,
padding : 3 ,
overflow : 'auto' ,
maxWidth : '100%' ,
} } >
} } >
{ grid . map ( ( row , r ) => row . map ( ( cell , c ) => {
{ grid . map ( ( row , r ) => row . map ( ( cell , c ) => {
const key = ` ${ r } - ${ c } ` ;
const key = ` ${ r } - ${ c } ` ;
const clickable = canClick ( r , c ) ;
const clic = canClick ( r , c ) ;
const isHovered = hover === key && clickable ;
const hov = hover === key && clic ;
let bg = NEUTRAL _COLOR ;
let bg = 'rgba(255,255,255,0.05)' ;
if ( cell === myId ) bg = ` ${ myColor } 55 ` ;
if ( cell === myId ) bg = ` ${ myColor } 55 ` ;
if ( cell === opponent ) bg = ` ${ oppColor } 55 ` ;
if ( cell === opponent ) bg = ` ${ oppColor } 55 ` ;
if ( isHovered ) bg = HOVER _COLOR ;
if ( clic && ! hov ) bg = ` ${ myColor } 22 ` ;
if ( clickable && ! isHovered ) bg = ` ${ myColor } 25 ` ;
if ( hov ) bg = ` ${ myColor } 66 ` ;
return (
return (
< div
< div
key = { key }
key = { key }
@@ -227,12 +217,11 @@ function GameBoard({ game, myId, onMove, toast }) {
onMouseLeave = { ( ) => setHover ( null ) }
onMouseLeave = { ( ) => setHover ( null ) }
style = { {
style = { {
width : cellSize , height : cellSize ,
width : cellSize , height : cellSize ,
background : bg ,
background : bg, borderRadius : 1 ,
borderRadius : 2 ,
cursor : clic ? 'pointer' : 'default' ,
cursor : clickable ? 'pointer' : 'default ' ,
transition : 'background 0.08s ' ,
transition : 'background 0.1s ',
border : clic ? ` 1px solid ${ myColor } 55 ` : '1px solid transparent ',
boxSizing : 'border-box' ,
boxSizing : 'border-box' ,
border : clickable ? ` 1px solid ${ myColor } 66 ` : '1px solid transparent' ,
} }
} }
/ >
/ >
) ;
) ;
@@ -256,23 +245,18 @@ function GameList({ games, myId, onSelect, onNew }) {
let result = null ;
let result = null ;
if ( g . status === 'finished' ) {
if ( g . status === 'finished' ) {
if ( ! g . winner _id ) result = { label : 'Unentschieden' , color : '#ffe66d' } ;
if ( ! g . winner _id ) result = { label : 'Unentschieden' , color : '#ffe66d' } ;
else if ( g . winner _id === myId ) result = { label : 'Gewonnen 🎉' , color : '#4ecdc4' } ;
else if ( g . winner _id === myId ) result = { label : 'Gewonnen 🎉' , color : MY _COLOR } ;
else result = { label : 'Verloren' , color : '#ff6b9d' } ;
else result = { label : 'Verloren' , color : OPP _COLOR } ;
}
}
return (
return (
< button
< button onClick = { ( ) => onSelect ( g . id ) } style = { {
onClick = { ( ) => onSelect ( g . id ) }
style = { {
... S . card , width : '100%' , textAlign : 'left' , cursor : 'pointer' ,
... S . card , width : '100%' , textAlign : 'left' , cursor : 'pointer' ,
padding : '12px 14px' , marginBottom : 8 , display : 'flex' ,
padding : '12px 14px' , marginBottom : 8 , display : 'flex' , alignItems : 'center' , gap : 12 ,
alignItems : 'center' , gap : 12 , border : isMyTurn
border : isMyTurn ? '1px solid rgba(78,205,196,0.4)' : '1px solid rgba(255,255,255,0.07)' ,
? '1px solid rgba(78,205,196,0.4)'
} } >
: '1px solid rgba(255,255,255,0.07)' ,
} }
>
< div style = { { flex : 1 } } >
< div style = { { flex : 1 } } >
< div style = { { color : '#fff' , fontFamily : 'monospace' , fontSize : 13 , marginBottom : 3 } } >
< div style = { { color : '#fff' , fontFamily : 'monospace' , fontSize : 13 , marginBottom : 3 } } >
{ isMyTurn && < span style = { { color : '#4ecdc4' , marginRight : 6 } } > ▶ < / span > }
{ isMyTurn && < span style = { { color : MY _COLOR , marginRight : 6 } } > ▶ < / span > }
vs < strong > { opp } < / strong >
vs < strong > { opp } < / strong >
< / div >
< / div >
< div style = { { color : 'rgba(255,255,255,0.4)' , fontSize : 11 , fontFamily : 'monospace' } } >
< div style = { { color : 'rgba(255,255,255,0.4)' , fontSize : 11 , fontFamily : 'monospace' } } >
@@ -281,7 +265,7 @@ function GameList({ games, myId, onSelect, onNew }) {
< / div >
< / div >
{ result
{ result
? < span style = { { color : result . color , fontSize : 11 , fontFamily : 'monospace' } } > { result . label } < / span >
? < span style = { { color : result . color , fontSize : 11 , fontFamily : 'monospace' } } > { result . label } < / span >
: < span style = { { color : isMyTurn ? '#4ecdc4' : 'rgba(255,255,255,0.3)' , fontSize : 11 , fontFamily : 'monospace' } } >
: < span style = { { color : isMyTurn ? MY _COLOR : 'rgba(255,255,255,0.3)' , fontSize : 11 , fontFamily : 'monospace' } } >
{ isMyTurn ? 'Du bist dran' : 'Gegner dran' }
{ isMyTurn ? 'Du bist dran' : 'Gegner dran' }
< / span >
< / span >
}
}
@@ -294,21 +278,17 @@ function GameList({ games, myId, onSelect, onNew }) {
< button onClick = { onNew } style = { { ... S . btn ( '#4ecdc4' ) , marginBottom : 20 , width : '100%' } } >
< button onClick = { onNew } style = { { ... S . btn ( '#4ecdc4' ) , marginBottom : 20 , width : '100%' } } >
+ Neues Spiel starten
+ Neues Spiel starten
< / button >
< / button >
{ active . length > 0 && (
{ active . length > 0 && < >
< >
< div style = { { ... S . head , marginBottom : 8 } } > LAUFEND ( { active . length } ) < / div >
< div style = { { ... S . head , marginBottom : 8 } } > LAUFENDE SPIELE ( { active . length } ) < / div >
{ active . map ( g => < GameRow key = { g . id } g = { g } / > ) }
{ active . map ( g => < GameRow key = { g . id } g = { g } / > ) }
< / >
< / > }
) }
{ finished . length > 0 && < >
{ finis hed. length > 0 && (
< div style = { { ... S . hea d , marginTop : 16 , marginBottom : 8 } } > BEENDET < / div >
< >
< div style = { { ... S . head , marginTop : 16 , marginBottom : 8 } } > BEENDETE SPIELE < / div >
{ finished . map ( g => < GameRow key = { g . id } g = { g } / > ) }
{ finished . map ( g => < GameRow key = { g . id } g = { g } / > ) }
< / >
< / > }
) }
{ games . length === 0 && (
{ games . length === 0 && (
< div style = { { color : 'rgba(255,255,255,0.3)' , fontFamily : 'monospace' , fontSize : 13 , textAlign : 'center' , paddingTop : 40 } } >
< div style = { { color : 'rgba(255,255,255,0.3)' , fontFamily : 'monospace' , fontSize : 13 , textAlign : 'center' , paddingTop : 40 } } >
Noch keine Spiele . S tarte eines !
Noch keine Spiele — s tarte eines !
< / div >
< / div >
) }
) }
< / div >
< / div >
@@ -316,27 +296,23 @@ function GameList({ games, myId, onSelect, onNew }) {
}
}
// ── Hauptkomponente ───────────────────────────────────────────────────────────
// ── Hauptkomponente ───────────────────────────────────────────────────────────
export default function Gebietseroberung ( { toast } ) {
export default function HexWars ( { toast } ) {
const [ games , setGames ] = useState ( [ ] ) ;
const [ games , setGames ] = useState ( [ ] ) ;
const [ activeId , setActiveId ] = useState ( null ) ;
const [ activeId , setActiveId ] = useState ( null ) ;
const [ game , setGame ] = useState ( null ) ;
const [ game , setGame ] = useState ( null ) ;
const [ myId , setMyId ] = useState ( null ) ;
const [ showNew , setShowNew ] = useState ( false ) ;
const [ showNew , setShowNew ] = useState ( false ) ;
const [ showHelp , setShowHelp ] = useState ( false ) ;
const [ showHelp , setShowHelp ] = useState ( false ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ loading , setLoading ] = useState ( true ) ;
// Eigene User-ID aus JWT ermitteln
// User-ID direkt aus JWT — kein API-Call
useEffect ( ( ) => {
const myId = getMyId ( ) ;
api ( '/auth/me' ) . then ( u => setMyId ( u . id ) ) . catch ( ( ) => { } ) ;
} , [ ] ) ;
const loadGames = useCallback ( async ( ) => {
const loadGames = useCallback ( async ( ) => {
try {
try {
const gs = await api ( '/tools/gebietseroberung' ) ;
const gs = await api ( '/tools/gebietseroberung' ) ;
setGames ( gs ) ;
setGames ( gs ) ;
} catch ( e ) {
} catch { toast ? . ( 'Fehler beim Laden' , 'error' ) ; }
toast ? . ( 'Fehler beim Laden' , 'error' ) ;
finally { setLoading ( false ) ; }
} finally { setLoading ( false ) ; }
} , [ ] ) ;
} , [ ] ) ;
useEffect ( ( ) => { loadGames ( ) ; } , [ loadGames ] ) ;
useEffect ( ( ) => { loadGames ( ) ; } , [ loadGames ] ) ;
@@ -347,42 +323,32 @@ export default function Gebietseroberung({ toast }) {
api ( ` /tools/gebietseroberung/ ${ activeId } ` ) . then ( setGame ) . catch ( ( ) => { } ) ;
api ( ` /tools/gebietseroberung/ ${ activeId } ` ) . then ( setGame ) . catch ( ( ) => { } ) ;
} , [ activeId ] ) ;
} , [ activeId ] ) ;
// Polling wenn aktives Spiel läuft und Gegner dran ist
// Polling wenn Gegner dran ist
useEffect ( ( ) => {
useEffect ( ( ) => {
if ( ! activeId || ! game || ! myId ) return ;
if ( ! activeId || ! game || ! myId ) return ;
if ( game . status !== 'active' || game . current _turn === myId ) return ;
if ( game . status !== 'active' || game . current _turn === myId ) return ;
const iv = setInterval ( async ( ) => {
const iv = setInterval ( async ( ) => {
const g = await api ( ` /tools/gebietseroberung/ ${ activeId } ` ) . catch ( ( ) => null ) ;
const g = await api ( ` /tools/gebietseroberung/ ${ activeId } ` ) . catch ( ( ) => null ) ;
if ( g ) {
if ( ! g ) return ;
setGame ( g ) ;
setGame ( g ) ;
if ( g . current _turn === myId ) {
if ( g . current _turn === myId ) toast ? . ( 'Du bist dran! ⬡' ) ;
toast ? . ( 'Du bist dran! 🗺️' ) ;
}
}
} , 10000 ) ;
} , 10000 ) ;
return ( ) => clearInterval ( iv ) ;
return ( ) => clearInterval ( iv ) ;
} , [ activeId , game , myId ] ) ;
} , [ activeId , game , myId ] ) ;
const handleMove = async ( row , col ) => {
const handleMove = async ( row , col ) => {
try {
const updated = await api ( ` /tools/gebietseroberung/ ${ activeId } /move ` , { body : { row , col } } ) ;
const updated = await api ( ` /tools/gebietseroberung/ ${ activeId } /move ` , { body : { row , col } } ) ;
setGame ( updated ) ;
setGame ( updated ) ;
// Spielliste auch aktualisieren
setGames ( prev => prev . map ( g => g . id === updated . id ? { ... g , ... updated } : g ) ) ;
setGames ( prev => prev . map ( g => g . id === updated . id ? { ... g , ... updated } : g ) ) ;
} catch ( e ) {
toast ? . ( e . message || 'Fehler beim Zug' , 'error' ) ;
}
} ;
} ;
const handleResign = async ( ) => {
const handleResign = async ( ) => {
if ( ! window . confirm ( 'Wirklich aufgeben?' ) ) return ;
if ( ! window . confirm ( 'Wirklich aufgeben?' ) ) return ;
try {
await api ( ` /tools/gebietseroberung/ ${ activeId } /resign ` , { body : { } } ) ;
await api ( ` /tools/gebietseroberung/ ${ activeId } /resign ` , { body : { } } ) ;
const updated = await api ( ` /tools/gebietseroberung/ ${ activeId } ` ) ;
const updated = await api ( ` /tools/gebietseroberung/ ${ activeId } ` ) ;
setGame ( updated ) ;
setGame ( updated ) ;
setGames ( prev => prev . map ( g => g . id === updated . id ? { ... g , ... updated } : g ) ) ;
setGames ( prev => prev . map ( g => g . id === updated . id ? { ... g , ... updated } : g ) ) ;
toast ( 'Du hast aufgegeben.' ) ;
toast ( 'Du hast aufgegeben.' ) ;
} catch ( e ) { toast ? . ( e . message , 'error' ) ; }
} ;
} ;
if ( loading ) return (
if ( loading ) return (
@@ -392,38 +358,28 @@ export default function Gebietseroberung({ toast }) {
) ;
) ;
return (
return (
< div style = { { maxWidth : 60 0, margin : '0 auto' , padding : '0 4px' } } >
< div style = { { maxWidth : 52 0, margin : '0 auto' , padding : '0 4px' } } >
{ /* Header */ }
{ /* Header */ }
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 , marginBottom : 20 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , marginBottom : 16 } } >
{ activeId && (
{ activeId && (
< button onClick = { ( ) => { setActiveId ( null ) ; setGame ( null ) ; } } style = { S . btn ( '#666666' , true ) } >
< button onClick = { ( ) => { setActiveId ( null ) ; setGame ( null ) ; } } style = { S . btn ( '#666666' , true ) } >
← Zurück
← Zurück
< / button >
< / button >
) }
) }
< h2 style = { { ... S . head , margin : 0 , flex : 1 , fontSize : 12 } } >
< span style = { { ... S . head , margin : 0 , flex : 1 , fontSize : 12 } } > ⬡ HEX WARS < / span >
🗺 ️ GEBIETSEROBERUNG
< / h2 >
< button onClick = { ( ) => setShowHelp ( true ) } style = { S . btn ( '#ffe66d' , true ) } > ? Regeln < / button >
< button onClick = { ( ) => setShowHelp ( true ) } style = { S . btn ( '#ffe66d' , true ) } > ? Regeln < / button >
{ activeId && game ? . status === 'active' && (
{ activeId && game ? . status === 'active' && (
< button onClick = { handleResign } style = { S . btn ( '#ff6b9d' , true ) } > ⚑ Aufgeben < / button >
< button onClick = { handleResign } style = { S . btn ( '#ff6b9d' , true ) } > ⚑ Aufgeben < / button >
) }
) }
< / div >
< / div >
{ /* Inhalt */ }
{ ! activeId
{ ! activeId ? (
? < GameList games = { games } myId = { myId } onSelect = { setActiveId } onNew = { ( ) => setShowNew ( true ) } / >
< GameList
: game && myId
games = { games }
myId = { myId }
onSelect = { id => setActiveId ( id ) }
onNew = { ( ) => setShowNew ( true ) }
/ >
) : (
game && myId
? < GameBoard game = { game } myId = { myId } onMove = { handleMove } toast = { toast } / >
? < GameBoard game = { game } myId = { myId } onMove = { handleMove } toast = { toast } / >
: < div style = { { color : 'rgba(255,255,255,0.3)' , fontFamily : 'monospace' , textAlign : 'center' , paddingTop : 40 } } > Lade Spiel … < / div >
: < div style = { { color : 'rgba(255,255,255,0.3)' , fontFamily : 'monospace' , textAlign : 'center' , paddingTop : 40 } } > Lade Spiel … < / div >
) }
}
{ /* Modals */ }
{ showNew && (
{ showNew && (
< NewGameModal
< NewGameModal
onClose = { ( ) => setShowNew ( false ) }
onClose = { ( ) => setShowNew ( false ) }