diff --git a/backend/src/tools/gebietseroberung/routes.js b/backend/src/tools/gebietseroberung/routes.js index 02abd98..79fad04 100644 --- a/backend/src/tools/gebietseroberung/routes.js +++ b/backend/src/tools/gebietseroberung/routes.js @@ -25,9 +25,11 @@ const router = express.Router(); `); } for (const [col, def] of [ - ['move_count', 'INTEGER NOT NULL DEFAULT 0'], - ['terrain', 'TEXT'], - ['last_event', 'TEXT'], + ['move_count', 'INTEGER NOT NULL DEFAULT 0'], + ['terrain', 'TEXT'], + ['last_event', 'TEXT'], + ['owner_head', 'TEXT'], + ['opp_head', 'TEXT'], ]) { if (!cols.includes(col)) db.exec(`ALTER TABLE geo_games ADD COLUMN ${col} ${def}`); } @@ -82,6 +84,12 @@ function createGrid(ownerId, oppId) { return g; } +// Prüft ob [row,col] an den Kopf [hr,hc] angrenzt (8 Richtungen) +function isAdjacentToHead(row, col, hr, hc) { + return Math.abs(row - hr) <= 1 && Math.abs(col - hc) <= 1 && !(row === hr && col === hc); +} + +// Fallback für canMove: prüft ob Spieler noch irgendwo vom Kopf aus ziehen kann function isAdjacent(grid, row, col, playerId) { for (const [dr,dc] of DIRS8) { const r=row+dr, c=col+dc; @@ -90,7 +98,16 @@ function isAdjacent(grid, row, col, playerId) { return false; } -function canMove(grid, terrain, playerId) { +function canMove(grid, terrain, playerId, headRow, headCol) { + // Schlangen-Regel: nur Felder die an den Kopf grenzen sind erreichbar + if (headRow !== undefined && headCol !== undefined) { + for (const [dr,dc] of DIRS8) { + const r=headRow+dr, c=headCol+dc; + if (r>=0&&r=0&&c row.map((cell,c) => visible[r][c] ? cell : (cell===requesterId ? cell : 0))); const maskedTerrain = terrain.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : -1)); + // Kopf des anfragenden Spielers mitliefern (für Frontend-Validierung) + const myHead = requesterId === game.owner_id ? game.owner_head : game.opp_head; + const oppHead = requesterId === game.owner_id ? game.opp_head : game.owner_head; + return { ...game, grid: JSON.stringify(maskedGrid), @@ -171,6 +192,8 @@ function getGameForUser(id, requesterId) { fog: JSON.stringify(visible), myScore, oppScore: oppSc, + myHead, + oppHead, }; } @@ -236,9 +259,9 @@ router.post('/', authenticate, (req, res) => { const terrain = generateTerrain(); const result = db.prepare(` - INSERT INTO geo_games (owner_id, opponent_id, grid, terrain, current_turn, move_count) - VALUES (?, ?, ?, ?, ?, 0) - `).run(me, Number(opponent_id), JSON.stringify(grid), JSON.stringify(terrain), me); + INSERT INTO geo_games (owner_id, opponent_id, grid, terrain, current_turn, move_count, owner_head, opp_head) + VALUES (?, ?, ?, ?, ?, 0, ?, ?) + `).run(me, Number(opponent_id), JSON.stringify(grid), JSON.stringify(terrain), me, '0,0', `${GRID-1},${GRID-1}`); const myName = db.prepare('SELECT username FROM users WHERE id=?').get(me)?.username || 'Jemand'; sendPush(Number(opponent_id), '⬡ Hex Wars', `${myName} hat dich zu einem Spiel eingeladen! Du bist zuerst dran.`); @@ -263,7 +286,13 @@ router.post('/:id/move', authenticate, (req, res) => { if (row<0||row>=GRID||col<0||col>=GRID) return res.status(400).json({ error: 'Ungültige Position' }); if (grid[row][col] !== 0) return res.status(400).json({ error: 'Zelle belegt' }); if (terrain.length && terrain[row][col] === T_ROCK) return res.status(400).json({ error: 'Felsen — nicht betretbar' }); - if (!isAdjacent(grid, row, col, me)) return res.status(400).json({ error: 'Nicht angrenzend' }); + // Schlangen-Regel: Zug muss an den eigenen Kopf angrenzen + const myHeadStr = game.owner_id === me ? game.owner_head : game.opp_head; + const [myHR, myHC] = myHeadStr ? myHeadStr.split(',').map(Number) : [null, null]; + if (myHR !== null && !isAdjacentToHead(row, col, myHR, myHC)) + return res.status(400).json({ error: 'Nur an deinen Kopf anbauen!' }); + if (myHR === null && !isAdjacent(grid, row, col, me)) + return res.status(400).json({ error: 'Nicht angrenzend' }); const opponent = game.owner_id === me ? game.opponent_id : game.owner_id; const newMoves = (game.move_count || 0) + 1; @@ -273,6 +302,12 @@ router.post('/:id/move', authenticate, (req, res) => { // Zug ausführen grid[row][col] = me; + // Neuen Kopf setzen + let newOwnerHead = game.owner_head; + let newOppHead = game.opp_head; + if (game.owner_id === me) newOwnerHead = `${row},${col}`; + else newOppHead = `${row},${col}`; + let lastEvent = null; let mineHit = false; let blastCells = []; @@ -300,10 +335,15 @@ router.post('/:id/move', authenticate, (req, res) => { let status = 'active'; let winnerId = null; + // Köpfe nach Zug + const oppHeadStr = game.owner_id === me ? game.opp_head : game.owner_head; + const [oppHR, oppHC] = oppHeadStr ? oppHeadStr.split(',').map(Number) : [null, null]; + const myNewHR = row, myNewHC = col; + // Spielende: niemand kann mehr ziehen - if (!canMove(grid, terrain, opponent) && !canMove(grid, terrain, me)) { + if (!canMove(grid, terrain, opponent, oppHR, oppHC) && !canMove(grid, terrain, me, myNewHR, myNewHC)) { status = 'finished'; - } else if (!canMove(grid, terrain, opponent)) { + } else if (!canMove(grid, terrain, opponent, oppHR, oppHC)) { nextTurn = me; } @@ -323,8 +363,8 @@ router.post('/:id/move', authenticate, (req, res) => { db.prepare(` UPDATE geo_games SET grid=?, current_turn=?, status=?, winner_id=?, move_count=?, last_event=?, - updated_at=datetime('now','localtime') WHERE id=? - `).run(JSON.stringify(grid), nextTurn, status, winnerId, newMoves, lastEvent, game.id); + owner_head=?, opp_head=?, updated_at=datetime('now','localtime') WHERE id=? + `).run(JSON.stringify(grid), nextTurn, status, winnerId, newMoves, lastEvent, newOwnerHead, newOppHead, game.id); // Pushover if (status === 'finished') { diff --git a/frontend/src/tools/gebietseroberung.jsx b/frontend/src/tools/gebietseroberung.jsx index aa0e8b2..e62dc13 100644 --- a/frontend/src/tools/gebietseroberung.jsx +++ b/frontend/src/tools/gebietseroberung.jsx @@ -35,7 +35,7 @@ function HelpModal({ onClose }) {

Ablauf:
- 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. + Beide starten in gegenüberliegenden Ecken. Du hast einen Kopf — dein letzter Zug, markiert mit einem leuchtenden Rahmen. Du kannst nur an den Kopf anbauen (alle 8 Richtungen). Dein Gebiet bleibt erhalten, aber du kannst nicht mehr von alten Feldern aus expandieren. Tippe ein Feld an, bestätige — fertig.

@@ -169,18 +169,27 @@ function GameBoard({ game, myId, onMove, toast }) { } catch {} }, [game.last_event]); - const isAdj = (r, c) => DIRS8.some(([dr,dc]) => { - const nr=r+dr, nc=c+dc; - return nr>=0&&nr=0&&nc { + if (myHR === null) return false; + return Math.abs(r - myHR) <= 1 && Math.abs(c - myHC) <= 1 && !(r === myHR && c === myHC); + }; const canClick = (r, c) => { if (!isMyTurn || game.status !== 'active') return false; if (grid[r][c] !== 0) return false; - // T_ROCK kommt vom Server auch im Nebel nicht — Felsen sind immer sichtbar - // T_FOG darf canClick NICHT blocken: Felder können klickbar aber noch im Nebel sein if (hasTerrain && terrain[r][c] === T_ROCK) return false; - return isAdj(r, c); + // Schlangen-Regel: nur Felder an den Kopf + if (myHead) return isAdjacentToHead(r, c); + // Fallback für alte Spiele ohne head + return DIRS8.some(([dr,dc]) => { + const nr=r+dr, nc=c+dc; + return nr>=0&&nr=0&&nc { @@ -243,6 +252,7 @@ function GameBoard({ game, myId, onMove, toast }) { const clic = canClick(r, c); const isPend = pending?.row === r && pending?.col === c; const isBlast = blastHighlight.has(`${r}-${c}`); + const isHead = myHR === r && myHC === c; // eigener Kopf const isFog = t === T_FOG; const isRock = t === T_ROCK; const isGold = t === T_GOLD; @@ -271,8 +281,9 @@ function GameBoard({ game, myId, onMove, toast }) { } if (isBlast) { bg = '#ff440066'; border = '2px solid #ff4400'; } + if (isHead && !isPend) { border = `2px solid ${myColor}`; } // Kopf immer markieren if (isPend) { bg = myColor; border = `2px solid ${myColor}`; } - else if (clic && !isPend && !isBlast) border = `1px solid ${myColor}44`; + else if (clic && !isPend && !isBlast && !isHead) border = `1px solid ${myColor}44`; return (
- ▶ Wähle ein Feld — Vorsicht vor Minen! 💣 + ▶ Baue vom Kopf aus — Vorsicht vor Minen! 💣
)} {pending && (