feat: Hex Wars - Kundschaften, Mine-Alert auto-clear, Spielliste live

This commit is contained in:
2026-06-28 18:52:23 +02:00
parent a6b29c38ad
commit 247bcf5d44
2 changed files with 156 additions and 29 deletions

View File

@@ -25,11 +25,13 @@ const router = express.Router();
`);
}
for (const [col, def] of [
['move_count', 'INTEGER NOT NULL DEFAULT 0'],
['terrain', 'TEXT'],
['last_event', 'TEXT'],
['owner_head', 'TEXT'],
['opp_head', 'TEXT'],
['move_count', 'INTEGER NOT NULL DEFAULT 0'],
['terrain', 'TEXT'],
['last_event', 'TEXT'],
['owner_head', 'TEXT'],
['opp_head', 'TEXT'],
['owner_scouts', 'TEXT'],
['opp_scouts', 'TEXT'],
]) {
if (!cols.includes(col)) db.exec(`ALTER TABLE geo_games ADD COLUMN ${col} ${def}`);
}
@@ -69,7 +71,7 @@ function generateTerrain(ownerHead, oppHead) {
}
// ── Fog of War ────────────────────────────────────────────────────────────────
function computeFog(grid, playerId) {
function computeFog(grid, playerId, scouts = []) {
const visible = Array(GRID).fill(null).map(() => Array(GRID).fill(false));
for (let r=0;r<GRID;r++) for (let c=0;c<GRID;c++) {
if (grid[r][c] === playerId) {
@@ -80,6 +82,13 @@ function computeFog(grid, playerId) {
}
}
}
// Scout-Aufdeckungen: 3x3 um jeden Scout-Mittelpunkt
for (const [sr,sc] of scouts) {
for (let dr=-1;dr<=1;dr++) for (let dc=-1;dc<=1;dc++) {
const nr=sr+dr, nc=sc+dc;
if (nr>=0&&nr<GRID&&nc>=0&&nc<GRID) visible[nr][nc] = true;
}
}
return visible;
}
@@ -228,25 +237,30 @@ function getGameForUser(id, requesterId) {
return { ...game, myScore:0, oppScore:0, myHead, oppHead };
}
// Scouts des anfragenden Spielers laden
const myScoutsRaw = requesterId === game.owner_id ? game.owner_scouts : game.opp_scouts;
const myScouts = myScoutsRaw ? JSON.parse(myScoutsRaw) : [];
// Bei beendetem Spiel: vollständiges Grid ohne Fog
let maskedGrid, maskedTerrain;
if (game.status === 'finished') {
maskedGrid = grid;
maskedTerrain = terrain;
} else {
const visible = computeFog(grid, requesterId);
const visible = computeFog(grid, requesterId, myScouts);
maskedGrid = grid.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : (cell===requesterId ? cell : 0)));
maskedTerrain = terrain.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : -1));
}
return {
...game,
grid: JSON.stringify(maskedGrid),
terrain: JSON.stringify(maskedTerrain),
grid: JSON.stringify(maskedGrid),
terrain: JSON.stringify(maskedTerrain),
myScore,
oppScore: oppSc,
oppScore: oppSc,
myHead,
oppHead,
myScouts,
};
}
@@ -442,10 +456,14 @@ router.post('/:id/move', authenticate, (req, res) => {
winnerId = ms > os ? me : (os > ms ? opponent : null);
}
// last_event: nach jedem Zug das vorherige Event clearen (war vom letzten Zug)
// Nur das neue Event (falls Mine) bleibt stehen
const finalEvent = lastEvent; // null wenn kein Mine-Treffer
db.prepare(`
UPDATE geo_games SET grid=?, terrain=?, current_turn=?, status=?, winner_id=?, move_count=?, last_event=?,
owner_head=?, opp_head=?, updated_at=datetime('now','localtime') WHERE id=?
`).run(JSON.stringify(grid), JSON.stringify(terrain), nextTurn, status, winnerId, newMoves, lastEvent, newOwnerHead, newOppHead, game.id);
`).run(JSON.stringify(grid), JSON.stringify(terrain), nextTurn, status, winnerId, newMoves, finalEvent, newOwnerHead, newOppHead, game.id);
// Pushover
if (status === 'finished') {
@@ -474,6 +492,58 @@ router.post('/:id/move', authenticate, (req, res) => {
res.json(getGameForUser(game.id, me));
});
// ── Scout: 3x3 Bereich aufdecken (kostet Zug) ───────────────────────────────
router.post('/:id/scout', authenticate, (req, res) => {
const me = uid(req);
const { row, col } = req.body;
if (row === undefined || col === undefined) return res.status(400).json({ error: 'row/col fehlt' });
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) return res.status(403).json({ error: 'Kein Zugriff' });
if (game.status !== 'active') return res.status(400).json({ error: 'Spiel beendet' });
if (game.current_turn !== me) return res.status(400).json({ error: 'Nicht dein Zug' });
const opponent = game.owner_id === me ? game.opponent_id : game.owner_id;
const newMoves = (game.move_count || 0) + 1;
// Scouts des Spielers laden und neuen hinzufügen
const isOwner = game.owner_id === me;
const scoutsKey = isOwner ? 'owner_scouts' : 'opp_scouts';
const existingRaw = game[scoutsKey];
const existing = existingRaw ? JSON.parse(existingRaw) : [];
existing.push([row, col]);
const newScoutsStr = JSON.stringify(existing);
// Zug weitergeben (Scout kostet Zug)
const grid = JSON.parse(game.grid);
const terrain = JSON.parse(game.terrain || '[]');
let status = 'active', winnerId = null;
// Schnellsieg-Check
if (newMoves >= 10 && terrain.length) {
const ms = calcScore(grid, terrain, me);
const os = calcScore(grid, terrain, opponent);
if (ms >= os*2 && os > 0) status = 'finished';
else if (os >= ms*2 && ms > 0) status = 'finished';
if (status === 'finished') {
winnerId = ms > os ? me : (os > ms ? opponent : null);
}
}
const updateFields = isOwner
? 'owner_scouts=?, current_turn=?, status=?, winner_id=?, move_count=?, last_event=NULL'
: 'opp_scouts=?, current_turn=?, status=?, winner_id=?, move_count=?, last_event=NULL';
db.prepare(`UPDATE geo_games SET ${updateFields}, updated_at=datetime('now','localtime') WHERE id=?`)
.run(newScoutsStr, opponent, status, winnerId, newMoves, game.id);
const myName = db.prepare('SELECT username FROM users WHERE id=?').get(me)?.username || 'Jemand';
sendPush(opponent, '⬡ Hex Wars — Du bist dran!', `${myName} hat gekundschaftet — jetzt bist du dran!`);
res.json(getGameForUser(game.id, me));
});
// ── Aufgeben ──────────────────────────────────────────────────────────────────
router.post('/:id/resign', authenticate, (req, res) => {
const me = uid(req);