feat: Hex Wars - Server-Scores, Minenexplosion, Dominanzsieg, Regel-Update

This commit is contained in:
2026-06-26 16:50:57 +02:00
parent afd2aa6029
commit df9947303c
2 changed files with 163 additions and 131 deletions

View File

@@ -31,11 +31,11 @@ function HelpModal({ onClose }) {
</div>
<div style={{ color:'rgba(255,255,255,0.75)', fontSize:13, lineHeight:1.9, fontFamily:'monospace' }}>
<p style={{ marginTop:0 }}>
<strong style={{ color:MY_COLOR }}>Ziel:</strong> Sammle am Ende mehr <strong>Punkte</strong> als dein Gegner.
<strong style={{ color:MY_COLOR }}>Ziel:</strong> Sammle mehr <strong>Punkte</strong> als dein Gegner durch clevere Expansion und das Meiden von Fallen.
</p>
<p><strong style={{ color:'#fff' }}>Ablauf:</strong><br/>
Beide starten in einer Ecke. Reihum wählst du ein <strong>graues Feld</strong> das an dein Gebiet grenzt (auch diagonal) und besetzt es.
Beide starten in gegenüberliegenden Ecken. Reihum wählst du ein freies Feld das an dein Gebiet grenzt (auch diagonal) und besetzt es. Tippe es an, bestätige fertig.
</p>
<div style={{ background:'rgba(255,255,255,0.04)', borderRadius:8, padding:'12px 14px', marginBottom:12, border:'1px solid rgba(255,255,255,0.08)' }}>
@@ -43,23 +43,23 @@ function HelpModal({ onClose }) {
<div style={{ display:'grid', gridTemplateColumns:'28px 1fr', gap:'6px 10px', alignItems:'center' }}>
<span style={{ fontSize:16, textAlign:'center' }}></span><span><strong style={{ color:'rgba(255,255,255,0.9)' }}>Normales Feld</strong> +1 Punkt</span>
<span style={{ fontSize:16, textAlign:'center' }}></span><span><strong style={{ color:'#ffe66d' }}>Goldfeld</strong> +3 Punkte, sehr wertvoll!</span>
<span style={{ fontSize:16, textAlign:'center' }}>💣</span><span><strong style={{ color:'#ff6b9d' }}>Sprengmine</strong> 3 Punkte & du verlierst deinen nächsten Zug</span>
<span style={{ fontSize:16, textAlign:'center' }}>💣</span><span><strong style={{ color:'#ff6b9d' }}>Sprengmine</strong> Betreten zündet eine Explosion: alle 8 umliegenden Felder werden zerstört. Besetzte Felder gehen verloren, du verlierst deinen nächsten Zug.</span>
<span style={{ fontSize:16, textAlign:'center' }}>🪨</span><span><strong style={{ color:'rgba(255,255,255,0.4)' }}>Felsen</strong> unbesetzbar, blockiert Wege</span>
</div>
</div>
<p>
<strong style={{ color:'#fff' }}>🌫 Nebel:</strong><br/>
Du siehst nur dein Gebiet und die direkt angrenzenden Felder. Was dahinter liegt bleibt verborgen auch Minen und Goldfelder. Erst wenn du nah genug rankommst wird es aufgedeckt.
Du siehst nur dein Gebiet und die direkt angrenzenden Felder. Goldfelder und Minen bleiben verborgen bis du nahe genug herankommst also aufgepasst!
</p>
<p>
<strong style={{ color:'#fff' }}>Ende:</strong><br/>
Wenn niemand mehr ziehen kann, gewinnt wer mehr Punkte hat. Tippe ein Feld an und bestätige deinen Zug du kriegst eine <strong>Pushover-Benachrichtigung</strong> wenn du dran bist.
<strong style={{ color:'#fff' }}>Spielende:</strong><br/>
Das Spiel endet wenn niemand mehr ziehen kann oder wenn jemand <strong>doppelt so viele Punkte</strong> hat wie der Gegner (Dominanzsieg 👑). Wer mehr Punkte hat, gewinnt.
</p>
<div style={{ background:'rgba(255,255,255,0.04)', borderRadius:8, padding:'10px 14px', border:'1px solid rgba(255,255,255,0.08)' }}>
📱 <em>Auf dem Handy: Pinch zum Zoomen, dann Feld antippen und bestätigen.</em>
📱 <em>Auf dem Handy: Pinch zum Zoomen, Feld antippen und bestätigen.</em>
</div>
</div>
</div>
@@ -151,11 +151,21 @@ function GameBoard({ game, myId, onMove, toast }) {
const oppName = game.owner_id === myId ? game.opp_name : game.owner_name;
// Mine-Event anzeigen wenn frisch
const [blastHighlight, setBlastHighlight] = useState(new Set());
useEffect(() => {
if (!game.last_event) return;
try {
const ev = JSON.parse(game.last_event);
if (ev.type === 'mine') setMineAlert(ev);
if (ev.type === 'mine') {
setMineAlert(ev);
// Blast-Zellen kurz highlighten
if (ev.blastCells?.length) {
const keys = new Set(ev.blastCells.map(([r,c]) => `${r}-${c}`));
setBlastHighlight(keys);
setTimeout(() => setBlastHighlight(new Set()), 2000);
}
}
} catch {}
}, [game.last_event]);
@@ -190,30 +200,19 @@ function GameBoard({ game, myId, onMove, toast }) {
} finally { setMoving(false); }
};
// Punkte berechnen (clientseitig, nur aus sichtbaren Feldern)
const calcVisible = (playerId) => {
let score = 0;
for (let r=0;r<GRID;r++) for (let c=0;c<GRID;c++) {
if (grid[r][c] !== playerId) continue;
const t = hasTerrain ? terrain[r][c] : T_NORMAL;
if (t === T_GOLD) score += 3;
else if (t === T_MINE) score -= 3;
else score += 1;
}
return Math.max(0, score);
};
const myScore = calcVisible(myId);
const oppScore = calcVisible(opponent);
// Scores kommen vom Server (auf ungemasktem Grid berechnet — korrekt für beide Spieler)
const myScore = game.myScore ?? 0;
const oppScore = game.oppScore ?? 0;
const myCount = grid.flat().filter(v => v === myId).length;
const oppCount = grid.flat().filter(v => v === opponent).length;
const fogCount = grid.flat().filter(v => v === 0 && hasTerrain).length;
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:MY_COLOR };
else banner = { text:'Du hast verloren.', color:OPP_COLOR };
const isDominance = myScore >= oppScore * 2 || oppScore >= myScore * 2;
if (!game.winner_id) banner = { text:'Unentschieden! 🤝', color:'#ffe66d' };
else if (game.winner_id === myId) banner = { text: isDominance ? 'Dominanzsieg! 👑🎉' : 'Du hast gewonnen! 🎉', color:MY_COLOR };
else banner = { text: isDominance ? 'Dominanzniederlage 💀' : 'Du hast verloren.', color:OPP_COLOR };
}
const vw = Math.min(window.innerWidth, 600);
@@ -229,6 +228,7 @@ function GameBoard({ game, myId, onMove, toast }) {
const t = hasTerrain ? terrain[r][c] : T_NORMAL;
const clic = canClick(r, c);
const isPend = pending?.row === r && pending?.col === c;
const isBlast = blastHighlight.has(`${r}-${c}`);
const isFog = t === T_FOG;
const isRock = t === T_ROCK;
const isGold = t === T_GOLD;
@@ -256,8 +256,9 @@ function GameBoard({ game, myId, onMove, toast }) {
else if (clic) bg = `${myColor}22`;
}
if (isBlast) { bg = '#ff440066'; border = '2px solid #ff4400'; }
if (isPend) { bg = myColor; border = `2px solid ${myColor}`; }
else if (clic && !isPend) border = `1px solid ${myColor}44`;
else if (clic && !isPend && !isBlast) border = `1px solid ${myColor}44`;
return (
<div
@@ -299,8 +300,8 @@ function GameBoard({ game, myId, onMove, toast }) {
<span style={{ fontSize:20 }}>💣</span>
<span style={{ color:'#ff6b9d', fontFamily:'monospace', fontSize:12, flex:1 }}>
{mineAlert.player === myId
? 'Du hast eine Mine getroffen! 3 Punkte & Zug verloren.'
: `${oppName} hat eine Mine getroffen! 😈`}
? `💥 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.'} 😈`}
</span>
<button onClick={() => setMineAlert(null)} style={{ ...S.btn('#666666', true), padding:'2px 8px' }}></button>
</div>