diff --git a/backend/src/tools/gebietseroberung/routes.js b/backend/src/tools/gebietseroberung/routes.js index d572ba0..6c76e8c 100644 --- a/backend/src/tools/gebietseroberung/routes.js +++ b/backend/src/tools/gebietseroberung/routes.js @@ -12,60 +12,56 @@ const router = express.Router(); id INTEGER PRIMARY KEY AUTOINCREMENT, owner_id INTEGER NOT NULL, opponent_id INTEGER NOT NULL, - owner_color TEXT NOT NULL DEFAULT '#4ecdc4', - opp_color TEXT NOT NULL DEFAULT '#ff6b9d', grid TEXT NOT NULL, current_turn INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'active', winner_id INTEGER, + move_count INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')), updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime')) ); `); } + // move_count nachrüsten falls Tabelle schon existiert + if (!cols.includes('move_count')) { + db.exec(`ALTER TABLE geo_games ADD COLUMN move_count INTEGER NOT NULL DEFAULT 0`); + } })(); -const GRID_SIZE = 20; +const GRID = 20; -// Leeres Grid erstellen: 0=neutral, owner_id=Spieler1, opponent_id=Spieler2 function createGrid(ownerId, oppId) { - const grid = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(0)); - // Startpositionen: Owner oben-links, Opponent unten-rechts + const grid = Array(GRID).fill(null).map(() => Array(GRID).fill(0)); grid[0][0] = ownerId; - grid[GRID_SIZE - 1][GRID_SIZE - 1] = oppId; + grid[GRID - 1][GRID - 1] = oppId; return JSON.stringify(grid); } -// Prüft ob Zelle an Spieler-Gebiet grenzt (oben/unten/links/rechts) +// Diagonal erlaubt: alle 8 Richtungen function isAdjacent(grid, row, col, playerId) { - const dirs = [[-1,0],[1,0],[0,-1],[0,1]]; + const dirs = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]; for (const [dr, dc] of dirs) { const r = row + dr, c = col + dc; - if (r >= 0 && r < GRID_SIZE && c >= 0 && c < GRID_SIZE && grid[r][c] === playerId) return true; + if (r >= 0 && r < GRID && c >= 0 && c < GRID && grid[r][c] === playerId) return true; } return false; } -// Prüft ob ein Spieler noch Züge machen kann function canMove(grid, playerId) { - for (let r = 0; r < GRID_SIZE; r++) { - for (let c = 0; c < GRID_SIZE; c++) { + for (let r = 0; r < GRID; r++) + for (let c = 0; c < GRID; c++) if (grid[r][c] === 0 && isAdjacent(grid, r, c, playerId)) return true; - } - } return false; } -// Felder zählen function countCells(grid, playerId) { - let count = 0; - for (let r = 0; r < GRID_SIZE; r++) - for (let c = 0; c < GRID_SIZE; c++) - if (grid[r][c] === playerId) count++; - return count; + let n = 0; + for (let r = 0; r < GRID; r++) + for (let c = 0; c < GRID; c++) + if (grid[r][c] === playerId) n++; + return n; } -// Pushover helper function sendPush(userId, title, message) { const cfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(userId); if (!cfg?.app_token || !cfg?.user_key) return; @@ -76,24 +72,52 @@ function sendPush(userId, title, message) { }).catch(() => {}); } +// Spiel mit JOIN laden (inkl. owner_name / opp_name) +function getGame(id) { + return db.prepare(` + SELECT g.*, u1.username as owner_name, u2.username as opp_name + FROM geo_games g + JOIN users u1 ON u1.id = g.owner_id + JOIN users u2 ON u2.id = g.opponent_id + WHERE g.id = ? + `).get(id); +} + const uid = req => req.user.id; // ── Alle Spiele des Users ───────────────────────────────────────────────────── router.get('/', authenticate, (req, res) => { const me = uid(req); const games = db.prepare(` - SELECT g.*, - u1.username as owner_name, - u2.username as opp_name + SELECT g.*, u1.username as owner_name, u2.username as opp_name FROM geo_games g JOIN users u1 ON u1.id = g.owner_id JOIN users u2 ON u2.id = g.opponent_id - WHERE g.owner_id=? OR g.opponent_id=? + WHERE (g.owner_id=? OR g.opponent_id=?) + AND NOT (g.status='finished' AND g.move_count < 2) ORDER BY g.updated_at DESC `).all(me, me); res.json(games); }); +// ── Topliste ────────────────────────────────────────────────────────────────── +router.get('/leaderboard', authenticate, (req, res) => { + // Nur Spiele die wirklich gespielt wurden (mind. 1 Zug pro Spieler = move_count >= 2) + // und nicht durch Aufgeben ohne Züge beendet wurden + const rows = db.prepare(` + SELECT u.username, COUNT(*) as wins + FROM geo_games g + JOIN users u ON u.id = g.winner_id + WHERE g.status = 'finished' + AND g.winner_id IS NOT NULL + AND g.move_count >= 2 + GROUP BY g.winner_id + ORDER BY wins DESC + LIMIT 20 + `).all(); + res.json(rows); +}); + // ── Alle User (für Einladen) ────────────────────────────────────────────────── router.get('/users', authenticate, (req, res) => { const me = uid(req); @@ -106,16 +130,8 @@ router.get('/users', authenticate, (req, res) => { // ── Einzelnes Spiel ─────────────────────────────────────────────────────────── router.get('/:id', authenticate, (req, res) => { - const me = uid(req); - const game = db.prepare(` - SELECT g.*, - u1.username as owner_name, - u2.username as opp_name - FROM geo_games g - JOIN users u1 ON u1.id = g.owner_id - JOIN users u2 ON u2.id = g.opponent_id - WHERE g.id=? - `).get(req.params.id); + const me = uid(req); + const game = getGame(req.params.id); if (!game) return res.status(404).json({ error: 'Nicht gefunden' }); if (game.owner_id !== me && game.opponent_id !== me) return res.status(403).json({ error: 'Kein Zugriff' }); @@ -132,14 +148,14 @@ router.post('/', authenticate, (req, res) => { const opp = db.prepare('SELECT id, username FROM users WHERE id=?').get(opponent_id); if (!opp) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); - const grid = createGrid(me, opponent_id); + const grid = createGrid(me, opponent_id); const result = db.prepare(` - INSERT INTO geo_games (owner_id, opponent_id, grid, current_turn) - VALUES (?, ?, ?, ?) + INSERT INTO geo_games (owner_id, opponent_id, grid, current_turn, move_count) + VALUES (?, ?, ?, ?, 0) `).run(me, opponent_id, grid, me); const myName = db.prepare('SELECT username FROM users WHERE id=?').get(me)?.username || 'Jemand'; - sendPush(opponent_id, '🗺️ Gebietseroberung', `${myName} hat dich zu einem Spiel eingeladen! Du bist zuerst dran.`); + sendPush(opponent_id, '⬡ Hex Wars', `${myName} hat dich zu einem Spiel eingeladen! Du bist zuerst dran.`); res.json({ id: result.lastInsertRowid }); }); @@ -158,71 +174,63 @@ router.post('/:id/move', authenticate, (req, res) => { if (game.current_turn !== me) return res.status(400).json({ error: 'Nicht dein Zug' }); const grid = JSON.parse(game.grid); - - if (row < 0 || row >= GRID_SIZE || col < 0 || col >= GRID_SIZE) + 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 bereits belegt' }); if (!isAdjacent(grid, row, col, me)) return res.status(400).json({ error: 'Zelle grenzt nicht an dein Gebiet' }); - // Zug ausführen grid[row][col] = me; + const opponent = game.owner_id === me ? game.opponent_id : game.owner_id; + const newMoves = (game.move_count || 0) + 1; + let nextTurn = opponent; + let status = 'active'; + let winnerId = null; - const opponent = game.owner_id === me ? game.opponent_id : game.owner_id; - let nextTurn = opponent; - let status = 'active'; - let winnerId = null; - - // Prüfen ob Gegner noch ziehen kann if (!canMove(grid, opponent)) { - // Gegner kann nicht — prüfen ob ich auch nicht mehr kann if (!canMove(grid, me)) { - // Spiel vorbei - status = 'finished'; - const myCount = countCells(grid, me); - const oppCount = countCells(grid, opponent); - winnerId = myCount > oppCount ? me : (oppCount > myCount ? opponent : null); // null = Unentschieden + status = 'finished'; + const mc = countCells(grid, me); + const oc = countCells(grid, opponent); + winnerId = mc > oc ? me : (oc > mc ? opponent : null); } else { - // Gegner überspringen, ich mache weiter - nextTurn = me; + nextTurn = me; // Gegner überspringen } } - const newGrid = JSON.stringify(grid); db.prepare(` - UPDATE geo_games SET grid=?, current_turn=?, status=?, winner_id=?, + UPDATE geo_games SET grid=?, current_turn=?, status=?, winner_id=?, move_count=?, updated_at=datetime('now','localtime') WHERE id=? - `).run(newGrid, nextTurn, status, winnerId, game.id); + `).run(JSON.stringify(grid), nextTurn, status, winnerId, newMoves, game.id); // Pushover - const myName = db.prepare('SELECT username FROM users WHERE id=?').get(me)?.username || 'Jemand'; + const myName = db.prepare('SELECT username FROM users WHERE id=?').get(me)?.username || 'Jemand'; const oppName = db.prepare('SELECT username FROM users WHERE id=?').get(opponent)?.username || 'Jemand'; if (status === 'finished') { - const myCount = countCells(grid, me); - const oppCount = countCells(grid, opponent); + const mc = countCells(grid, me); + const oc = countCells(grid, opponent); if (winnerId === me) { - sendPush(opponent, '🗺️ Spiel beendet', `${myName} hat gewonnen! ${myCount} vs ${oppCount} Felder.`); - sendPush(me, '🗺️ Spiel beendet', `Du hast gewonnen! ${myCount} vs ${oppCount} Felder. 🎉`); + sendPush(opponent, '⬡ Hex Wars beendet', `${myName} hat gewonnen! ${mc} vs ${oc} Felder.`); + sendPush(me, '⬡ Hex Wars gewonnen!', `Du hast gewonnen! ${mc} vs ${oc} Felder. 🎉`); } else if (winnerId === opponent) { - sendPush(me, '🗺️ Spiel beendet', `${oppName} hat gewonnen. ${myCount} vs ${oppCount} Felder.`); - sendPush(opponent, '🗺️ Spiel beendet', `Du hast gewonnen! ${oppCount} vs ${myCount} Felder. 🎉`); + sendPush(me, '⬡ Hex Wars beendet', `${oppName} hat gewonnen. ${mc} vs ${oc} Felder.`); + sendPush(opponent, '⬡ Hex Wars gewonnen!', `Du hast gewonnen! ${oc} vs ${mc} Felder. 🎉`); } else { - sendPush(me, '🗺️ Unentschieden!', `Gleichstand — ${myCount} vs ${oppCount} Felder.`); - sendPush(opponent, '🗺️ Unentschieden!', `Gleichstand — ${oppCount} vs ${myCount} Felder.`); + sendPush(me, '⬡ Hex Wars — Unentschieden!', `${mc} vs ${oc} Felder.`); + sendPush(opponent, '⬡ Hex Wars — Unentschieden!', `${oc} vs ${mc} Felder.`); } } else if (nextTurn === opponent) { - sendPush(opponent, '🗺️ Du bist dran!', `${myName} hat gezogen — jetzt bist du dran!`); + sendPush(opponent, '⬡ Hex Wars — Du bist dran!', `${myName} hat gezogen!`); } - const updated = db.prepare('SELECT * FROM geo_games WHERE id=?').get(game.id); - res.json(updated); + res.json(getGame(game.id)); }); -// ── Spiel aufgeben ──────────────────────────────────────────────────────────── +// ── Aufgeben ────────────────────────────────────────────────────────────────── router.post('/:id/resign', authenticate, (req, res) => { - const me = uid(req); + const me = uid(req); const game = db.prepare('SELECT * FROM geo_games WHERE id=?').get(req.params.id); if (!game) return res.status(404).json({ error: 'Nicht gefunden' }); if (game.owner_id !== me && game.opponent_id !== me) @@ -230,10 +238,14 @@ router.post('/:id/resign', authenticate, (req, res) => { if (game.status !== 'active') return res.status(400).json({ error: 'Spiel bereits beendet' }); const winner = game.owner_id === me ? game.opponent_id : game.owner_id; - db.prepare(`UPDATE geo_games SET status='finished', winner_id=?, updated_at=datetime('now','localtime') WHERE id=?`).run(winner, game.id); + // move_count bleibt wie es ist — wird für Leaderboard/Liste genutzt + db.prepare(` + UPDATE geo_games SET status='finished', winner_id=?, + updated_at=datetime('now','localtime') WHERE id=? + `).run(winner, game.id); const myName = db.prepare('SELECT username FROM users WHERE id=?').get(me)?.username || 'Jemand'; - sendPush(winner, '🗺️ Spiel gewonnen!', `${myName} hat aufgegeben. Du gewinnst! 🎉`); + sendPush(winner, '⬡ Hex Wars gewonnen!', `${myName} hat aufgegeben. Du gewinnst! 🎉`); res.json({ ok: true }); }); diff --git a/frontend/src/tools/gebietseroberung.jsx b/frontend/src/tools/gebietseroberung.jsx index 0fdb6d7..7f3f3c2 100644 --- a/frontend/src/tools/gebietseroberung.jsx +++ b/frontend/src/tools/gebietseroberung.jsx @@ -2,10 +2,9 @@ import { useState, useEffect, useCallback } from 'react'; import { api, S } from '../lib.js'; const GRID = 20; -const MY_COLOR = '#4ecdc4'; -const OPP_COLOR = '#ff6b9d'; +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'); @@ -17,43 +16,26 @@ function getMyId() { // ── Spielerklärung ──────────────────────────────────────────────────────────── function HelpModal({ onClose }) { return ( -
- Dein Ziel: Am Ende mehr Felder als dein Gegner besitzen. -
-
- So geht's:
- 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 aus — aber mauere deinen Gegner
- ein, bevor er dich einsperrt!
- Blockieren ist genauso wichtig wie Expandieren.
-
- Ende:
- Wenn niemand mehr expandieren kann, gewinnt wer mehr Felder hat.
- Du kriegst eine Pushover-Benachrichtigung wenn du dran bist.
-
Dein Ziel: Am Ende mehr Felder als dein Gegner besitzen.
+So geht's:
+ Jeder Spieler startet in einer Ecke des 20×20 Rasters. Abwechselnd wählst du eine
+ neutrale (graue) Zelle,
+ die an dein Gebiet grenzt — auch diagonal.
Taktik:
+ Breite dich aus — aber mauere deinen Gegner
+ ein, bevor er dich einsperrt!
Ende:
+ Wenn niemand mehr expandieren kann, gewinnt wer mehr Felder hat.
+ Du kriegst eine Pushover-Benachrichtigung wenn du dran bist.