846 lines
No EOL
33 KiB
JavaScript
846 lines
No EOL
33 KiB
JavaScript
const express = require('express');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const DatabaseManager = require('../database/init');
|
|
|
|
class GameAPI {
|
|
constructor(db) {
|
|
this.db = db;
|
|
this.router = express.Router();
|
|
this.setupRoutes();
|
|
}
|
|
|
|
setupRoutes() {
|
|
// Game management
|
|
this.router.post('/games', this.createGame.bind(this));
|
|
this.router.get('/games/:id', this.getGameState.bind(this));
|
|
this.router.post('/games/:id/join', this.joinGame.bind(this));
|
|
this.router.post('/games/:id/submit-turn', this.submitTurn.bind(this));
|
|
this.router.get('/games/:id/status', this.getGameStatus.bind(this));
|
|
this.router.post('/games/:id/rematch', this.createRematch.bind(this));
|
|
|
|
// Player management
|
|
this.router.post('/players', this.createPlayer.bind(this));
|
|
this.router.get('/players/:id', this.getPlayer.bind(this));
|
|
|
|
// Statistics and leaderboard
|
|
this.router.get('/leaderboard', this.getLeaderboard.bind(this));
|
|
this.router.get('/games/:id/history', this.getGameHistory.bind(this));
|
|
this.router.get('/games/:id/replay', this.getGameReplay.bind(this));
|
|
|
|
// Tournament endpoints (future expansion)
|
|
this.router.get('/tournaments', this.getTournaments.bind(this));
|
|
this.router.post('/tournaments', this.createTournament.bind(this));
|
|
|
|
// Configuration endpoint
|
|
this.router.get('/config', this.getConfig.bind(this));
|
|
}
|
|
|
|
// Create a new game
|
|
async createGame(req, res) {
|
|
try {
|
|
const { playerName, gameMode = 'standard' } = req.body;
|
|
|
|
if (!playerName || playerName.trim().length === 0) {
|
|
return res.status(400).json({ error: 'Player name is required' });
|
|
}
|
|
|
|
// Create or get player
|
|
const playerId = await this.getOrCreatePlayer(playerName.trim());
|
|
|
|
// Create game
|
|
const gameId = await this.db.createGame(gameMode);
|
|
|
|
// Generate spawn positions (player 1 spawns first)
|
|
const spawnPositions = this.generateSpawnPositions();
|
|
await this.db.addPlayerToGame(gameId, playerId, 1, spawnPositions.player1.x, spawnPositions.player1.y);
|
|
|
|
// Log game creation event
|
|
await this.db.logGameEvent(gameId, 0, 1, 'game_created', playerId, {
|
|
game_mode: gameMode,
|
|
spawn_x: spawnPositions.player1.x,
|
|
spawn_y: spawnPositions.player1.y
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
gameId: gameId,
|
|
playerId: playerId,
|
|
playerNumber: 1,
|
|
inviteLink: `${req.protocol}://${req.get('host')}/game/${gameId}`,
|
|
spawnPosition: spawnPositions.player1
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error creating game:', error);
|
|
res.status(500).json({ error: 'Failed to create game' });
|
|
}
|
|
}
|
|
|
|
// Join an existing game
|
|
async joinGame(req, res) {
|
|
try {
|
|
const { id: gameId } = req.params;
|
|
const { playerName } = req.body;
|
|
|
|
if (!playerName || playerName.trim().length === 0) {
|
|
return res.status(400).json({ error: 'Player name is required' });
|
|
}
|
|
|
|
// Check if game exists and is waiting for players
|
|
const gameState = await this.db.getGameState(gameId);
|
|
if (!gameState) {
|
|
return res.status(404).json({ error: 'Game not found' });
|
|
}
|
|
|
|
if (gameState.game.status !== 'waiting') {
|
|
return res.status(400).json({ error: 'Game is not accepting new players' });
|
|
}
|
|
|
|
if (gameState.players.length >= GAME_CONFIG.MAX_PLAYERS) {
|
|
return res.status(400).json({ error: 'Game is full' });
|
|
}
|
|
|
|
// Create or get player
|
|
const playerId = await this.getOrCreatePlayer(playerName.trim());
|
|
|
|
// Check if player is already in the game
|
|
const existingPlayer = gameState.players.find(p => p.player_id === playerId);
|
|
if (existingPlayer) {
|
|
return res.status(400).json({ error: 'You are already in this game' });
|
|
}
|
|
|
|
// Add player 2
|
|
const spawnPositions = this.generateSpawnPositions();
|
|
await this.db.addPlayerToGame(gameId, playerId, 2, spawnPositions.player2.x, spawnPositions.player2.y);
|
|
|
|
// Update game status to playing
|
|
await this.db.runQuery(`
|
|
UPDATE games
|
|
SET status = 'playing', started_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`, [gameId]);
|
|
|
|
// Log join event
|
|
await this.db.logGameEvent(gameId, 0, 2, 'player_joined', playerId, {
|
|
player_number: 2,
|
|
spawn_x: spawnPositions.player2.x,
|
|
spawn_y: spawnPositions.player2.y
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
gameId: gameId,
|
|
playerId: playerId,
|
|
playerNumber: 2,
|
|
spawnPosition: spawnPositions.player2
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error joining game:', error);
|
|
res.status(500).json({ error: 'Failed to join game' });
|
|
}
|
|
}
|
|
|
|
// Submit turn actions
|
|
async submitTurn(req, res) {
|
|
try {
|
|
const { id: gameId } = req.params;
|
|
const { playerId, actions } = req.body;
|
|
|
|
if (!playerId || !actions || !Array.isArray(actions)) {
|
|
return res.status(400).json({ error: 'Invalid turn submission' });
|
|
}
|
|
|
|
// Validate game state
|
|
const gameState = await this.db.getGameState(gameId);
|
|
if (!gameState || gameState.game.status !== 'playing') {
|
|
return res.status(400).json({ error: 'Game is not active' });
|
|
}
|
|
|
|
// Validate player is in the game
|
|
const player = gameState.players.find(p => p.player_id === playerId);
|
|
if (!player) {
|
|
return res.status(400).json({ error: 'Player not in this game' });
|
|
}
|
|
|
|
const currentTurn = gameState.game.current_turn;
|
|
|
|
// Check if player has already submitted for this turn
|
|
const existingSubmission = await this.db.getRow(`
|
|
SELECT id FROM turn_submissions
|
|
WHERE game_id = ? AND player_id = ? AND turn_number = ?
|
|
`, [gameId, playerId, currentTurn]);
|
|
|
|
if (existingSubmission) {
|
|
return res.status(400).json({ error: 'Turn already submitted' });
|
|
}
|
|
|
|
// Validate actions
|
|
const validation = this.validateActions(actions);
|
|
if (!validation.valid) {
|
|
return res.status(400).json({ error: validation.error });
|
|
}
|
|
|
|
// Submit turn
|
|
await this.db.runQuery(`
|
|
INSERT INTO turn_submissions (game_id, player_id, turn_number, actions, moves_used, shot_used)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`, [gameId, playerId, currentTurn, JSON.stringify(actions), validation.movesUsed, validation.shotUsed]);
|
|
|
|
// Check if both players have submitted
|
|
const submissions = await this.db.getAllRows(`
|
|
SELECT player_id FROM turn_submissions
|
|
WHERE game_id = ? AND turn_number = ?
|
|
`, [gameId, currentTurn]);
|
|
|
|
let bothSubmitted = false;
|
|
if (submissions.length === GAME_CONFIG.MAX_PLAYERS) {
|
|
// Execute turn
|
|
await this.executeTurn(gameId, currentTurn);
|
|
bothSubmitted = true;
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
turnSubmitted: true,
|
|
bothPlayersSubmitted: bothSubmitted,
|
|
waitingForOpponent: !bothSubmitted
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error submitting turn:', error);
|
|
res.status(500).json({ error: 'Failed to submit turn' });
|
|
}
|
|
}
|
|
|
|
// Get current game status (for polling)
|
|
async getGameStatus(req, res) {
|
|
try {
|
|
const { id: gameId } = req.params;
|
|
const { playerId } = req.query;
|
|
|
|
const gameState = await this.db.getGameState(gameId);
|
|
if (!gameState) {
|
|
return res.status(404).json({ error: 'Game not found' });
|
|
}
|
|
|
|
// Get latest events for this turn
|
|
const currentTurn = gameState.game.current_turn;
|
|
const events = await this.db.getAllRows(`
|
|
SELECT * FROM game_events
|
|
WHERE game_id = ? AND turn_number >= ?
|
|
ORDER BY turn_number DESC, sequence_number DESC
|
|
LIMIT 10
|
|
`, [gameId, Math.max(0, currentTurn - 1)]);
|
|
|
|
// Check turn submission status
|
|
let turnStatus = 'waiting_for_submission';
|
|
if (playerId) {
|
|
const mySubmission = await this.db.getRow(`
|
|
SELECT id FROM turn_submissions
|
|
WHERE game_id = ? AND player_id = ? AND turn_number = ?
|
|
`, [gameId, playerId, currentTurn]);
|
|
|
|
const allSubmissions = await this.db.getAllRows(`
|
|
SELECT player_id FROM turn_submissions
|
|
WHERE game_id = ? AND turn_number = ?
|
|
`, [gameId, currentTurn]);
|
|
|
|
if (mySubmission) {
|
|
turnStatus = allSubmissions.length === 2 ? 'executed' : 'waiting_for_opponent';
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
game: gameState.game,
|
|
players: gameState.players,
|
|
recentEvents: events,
|
|
turnStatus: turnStatus,
|
|
needsUpdate: events.length > 0
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error getting game status:', error);
|
|
res.status(500).json({ error: 'Failed to get game status' });
|
|
}
|
|
}
|
|
|
|
// Get complete game state
|
|
async getGameState(req, res) {
|
|
try {
|
|
const { id: gameId } = req.params;
|
|
const gameState = await this.db.getGameState(gameId);
|
|
|
|
if (!gameState) {
|
|
return res.status(404).json({ error: 'Game not found' });
|
|
}
|
|
|
|
res.json(gameState);
|
|
} catch (error) {
|
|
console.error('Error getting game state:', error);
|
|
res.status(500).json({ error: 'Failed to get game state' });
|
|
}
|
|
}
|
|
|
|
// Create or get existing player
|
|
async getOrCreatePlayer(username) {
|
|
// Check if player exists
|
|
const existingPlayer = await this.db.getRow(
|
|
'SELECT id FROM players WHERE username = ?',
|
|
[username]
|
|
);
|
|
|
|
if (existingPlayer) {
|
|
// Update last active
|
|
await this.db.runQuery(
|
|
'UPDATE players SET last_active = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[existingPlayer.id]
|
|
);
|
|
return existingPlayer.id;
|
|
}
|
|
|
|
// Create new player
|
|
return await this.db.createPlayer(username);
|
|
}
|
|
|
|
// Generate spawn positions with minimum distance
|
|
generateSpawnPositions() {
|
|
const GAME_CONFIG = require('../game-config');
|
|
const minDistance = GAME_CONFIG.SPAWN_BUFFER;
|
|
const gridWidth = GAME_CONFIG.GRID_WIDTH;
|
|
const gridHeight = GAME_CONFIG.GRID_HEIGHT;
|
|
|
|
// Simple spawn logic: corners with buffer
|
|
const player1 = {
|
|
x: minDistance,
|
|
y: minDistance
|
|
};
|
|
|
|
const player2 = {
|
|
x: gridWidth - minDistance - 1,
|
|
y: gridHeight - minDistance - 1
|
|
};
|
|
|
|
return { player1, player2 };
|
|
}
|
|
|
|
// Validate turn actions
|
|
validateActions(actions) {
|
|
const GAME_CONFIG = require('../game-config');
|
|
let movesUsed = 0;
|
|
let shotUsed = false;
|
|
|
|
for (const action of actions) {
|
|
if (!action.type || !action.direction) {
|
|
return { valid: false, error: 'Invalid action format' };
|
|
}
|
|
|
|
if (!['north', 'south', 'east', 'west'].includes(action.direction)) {
|
|
return { valid: false, error: 'Invalid direction' };
|
|
}
|
|
|
|
if (action.type === 'move') {
|
|
movesUsed++;
|
|
if (movesUsed > GAME_CONFIG.MOVES_PER_TURN) {
|
|
return { valid: false, error: 'Too many moves' };
|
|
}
|
|
} else if (action.type === 'shoot') {
|
|
if (shotUsed) {
|
|
return { valid: false, error: 'Only one shot per turn allowed' };
|
|
}
|
|
shotUsed = true;
|
|
} else {
|
|
return { valid: false, error: 'Invalid action type' };
|
|
}
|
|
}
|
|
|
|
return { valid: true, movesUsed, shotUsed };
|
|
}
|
|
|
|
// Execute turn when both players have submitted
|
|
async executeTurn(gameId, turnNumber) {
|
|
console.log(`Executing turn ${turnNumber} for game ${gameId}`);
|
|
|
|
try {
|
|
// Get current game state
|
|
const gameState = await this.db.getGameState(gameId);
|
|
if (!gameState || gameState.game.status !== 'playing') {
|
|
console.error('Cannot execute turn: game not in playing state');
|
|
return;
|
|
}
|
|
|
|
// Get both players' submitted actions for this turn
|
|
const submissions = await this.db.getAllRows(`
|
|
SELECT player_id, actions, moves_used, shot_used
|
|
FROM turn_submissions
|
|
WHERE game_id = ? AND turn_number = ?
|
|
ORDER BY submitted_at ASC
|
|
`, [gameId, turnNumber]);
|
|
|
|
if (submissions.length !== 2) {
|
|
console.error(`Expected 2 submissions, got ${submissions.length}`);
|
|
return;
|
|
}
|
|
|
|
// Parse actions for both players
|
|
const playerActions = {};
|
|
submissions.forEach(submission => {
|
|
playerActions[submission.player_id] = JSON.parse(submission.actions);
|
|
});
|
|
|
|
// Get current player positions
|
|
const players = {};
|
|
gameState.players.forEach(player => {
|
|
players[player.player_id] = {
|
|
id: player.player_id,
|
|
number: player.player_number,
|
|
x: player.current_x,
|
|
y: player.current_y,
|
|
alive: Boolean(player.is_alive)
|
|
};
|
|
});
|
|
|
|
// Execute actions sequentially - shots can fire during movement
|
|
console.log('Executing actions sequentially for all players');
|
|
const executionResults = await this.executeSequentialActions(gameId, turnNumber, playerActions, players);
|
|
|
|
// Update player positions in database
|
|
for (const playerId in players) {
|
|
const player = players[playerId];
|
|
await this.db.runQuery(`
|
|
UPDATE game_players
|
|
SET current_x = ?, current_y = ?, is_alive = ?
|
|
WHERE game_id = ? AND player_id = ?
|
|
`, [player.x, player.y, player.alive ? 1 : 0, gameId, playerId]);
|
|
}
|
|
|
|
// Check for game end condition
|
|
const alivePlayers = Object.values(players).filter(p => p.alive);
|
|
if (alivePlayers.length <= 1) {
|
|
// Game ends
|
|
const winner = alivePlayers.length === 1 ? alivePlayers[0] : null;
|
|
await this.endGame(gameId, winner, 'combat');
|
|
console.log(`Game ${gameId} ended. Winner: ${winner ? winner.id : 'none'}`);
|
|
} else {
|
|
// Advance to next turn
|
|
await this.db.runQuery(`
|
|
UPDATE games
|
|
SET current_turn = current_turn + 1, last_action_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`, [gameId]);
|
|
console.log(`Turn ${turnNumber} completed, advanced to turn ${turnNumber + 1}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Error executing turn ${turnNumber} for game ${gameId}:`, error);
|
|
}
|
|
}
|
|
|
|
// Execute all actions sequentially - allowing shots during movement
|
|
async executeSequentialActions(gameId, turnNumber, playerActions, players) {
|
|
// Determine the maximum number of actions any player has
|
|
const maxActions = Math.max(...Object.values(playerActions).map(actions => actions.length));
|
|
|
|
// Execute actions step by step for all players simultaneously
|
|
for (let actionIndex = 0; actionIndex < maxActions; actionIndex++) {
|
|
console.log(`Executing action step ${actionIndex + 1} of ${maxActions}`);
|
|
|
|
// Store original positions before any actions at this step
|
|
const originalPositions = {};
|
|
for (const playerId in players) {
|
|
originalPositions[playerId] = { x: players[playerId].x, y: players[playerId].y };
|
|
}
|
|
|
|
// STEP 1: Process all shots first (from original positions before movement)
|
|
for (const playerId in playerActions) {
|
|
const actions = playerActions[playerId];
|
|
const player = players[playerId];
|
|
|
|
if (!player.alive || actionIndex >= actions.length) continue;
|
|
|
|
const action = actions[actionIndex];
|
|
|
|
if (action.type === 'shoot') {
|
|
// Shot fires from original position (before any movement this step)
|
|
const shooterX = originalPositions[playerId].x;
|
|
const shooterY = originalPositions[playerId].y;
|
|
const bulletPath = this.calculateBulletPath({ x: shooterX, y: shooterY }, action.direction);
|
|
|
|
// Log shot fired event with bullet path
|
|
await this.db.logGameEvent(gameId, turnNumber, player.number, 'shot_fired', playerId, {
|
|
action_index: actionIndex,
|
|
shooter_x: shooterX,
|
|
shooter_y: shooterY,
|
|
direction: action.direction,
|
|
bullet_path: bulletPath
|
|
});
|
|
|
|
// Check if shot hits any player at their original positions (before movement)
|
|
const hitTarget = this.checkInstantShotHit({ x: shooterX, y: shooterY, id: playerId }, action.direction, originalPositions, actionIndex, playerActions);
|
|
|
|
if (hitTarget) {
|
|
// Player hit! Find the actual player object and mark as dead
|
|
const actualTarget = players[hitTarget.playerId];
|
|
actualTarget.alive = false;
|
|
|
|
// Log hit event
|
|
await this.db.logGameEvent(gameId, turnNumber, player.number, 'player_hit', actualTarget.id, {
|
|
action_index: actionIndex,
|
|
shooter_id: player.id,
|
|
target_id: actualTarget.id,
|
|
target_x: hitTarget.x,
|
|
target_y: hitTarget.y,
|
|
shot_direction: action.direction,
|
|
bullet_path: bulletPath
|
|
});
|
|
|
|
console.log(`Player ${player.id} hit player ${actualTarget.id} at action step ${actionIndex + 1}!`);
|
|
} else {
|
|
console.log(`Player ${player.id} shot ${action.direction} from (${shooterX},${shooterY}) - no hit`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// STEP 2: Process all movements (after shots have been resolved)
|
|
for (const playerId in playerActions) {
|
|
const actions = playerActions[playerId];
|
|
const player = players[playerId];
|
|
|
|
if (!player.alive || actionIndex >= actions.length) continue;
|
|
|
|
const action = actions[actionIndex];
|
|
|
|
if (action.type === 'move') {
|
|
const oldX = player.x;
|
|
const oldY = player.y;
|
|
const newPosition = this.calculateNewPosition(player.x, player.y, action.direction);
|
|
|
|
// Validate movement (bounds check)
|
|
if (this.isValidPosition(newPosition.x, newPosition.y)) {
|
|
player.x = newPosition.x;
|
|
player.y = newPosition.y;
|
|
|
|
// Log movement event
|
|
await this.db.logGameEvent(gameId, turnNumber, player.number, 'player_moved', playerId, {
|
|
action_index: actionIndex,
|
|
from_x: oldX,
|
|
from_y: oldY,
|
|
to_x: player.x,
|
|
to_y: player.y,
|
|
direction: action.direction
|
|
});
|
|
|
|
console.log(`Player ${player.id} moved from (${oldX},${oldY}) to (${player.x},${player.y})`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
// Helper function to calculate new position based on direction
|
|
calculateNewPosition(x, y, direction) {
|
|
const offset = this.getDirectionOffset(direction);
|
|
return {
|
|
x: x + offset.x,
|
|
y: y + offset.y
|
|
};
|
|
}
|
|
|
|
// Helper function to get direction offset
|
|
getDirectionOffset(direction) {
|
|
const GAME_CONFIG = require('../game-config');
|
|
const directionKey = direction.toUpperCase();
|
|
const offset = GAME_CONFIG.DIRECTION_OFFSETS[directionKey];
|
|
|
|
if (offset) {
|
|
// Convert row/col format to x/y format for server use
|
|
return { x: offset.col, y: offset.row };
|
|
}
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
// Check if position is within game bounds
|
|
isValidPosition(x, y) {
|
|
const GAME_CONFIG = require('../game-config');
|
|
return x >= 0 && x < GAME_CONFIG.GRID_WIDTH &&
|
|
y >= 0 && y < GAME_CONFIG.GRID_HEIGHT;
|
|
}
|
|
|
|
// Calculate complete bullet path for visualization
|
|
calculateBulletPath(shooter, direction) {
|
|
const path = [];
|
|
const offset = this.getDirectionOffset(direction);
|
|
let currentX = shooter.x + offset.x;
|
|
let currentY = shooter.y + offset.y;
|
|
|
|
// Follow bullet path until it goes out of bounds
|
|
while (this.isValidPosition(currentX, currentY)) {
|
|
path.push({ x: currentX, y: currentY });
|
|
currentX += offset.x;
|
|
currentY += offset.y;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
// Check if an instant shot hits any player at their current positions
|
|
checkInstantShotHit(shooter, direction, positions, currentActionIndex, playerActions) {
|
|
console.log(`Checking shot from shooter ${shooter.id} at (${shooter.x},${shooter.y}) direction ${direction}`);
|
|
|
|
const offset = this.getDirectionOffset(direction);
|
|
let bulletX = shooter.x + offset.x;
|
|
let bulletY = shooter.y + offset.y;
|
|
|
|
// Follow bullet path and check for hits
|
|
while (this.isValidPosition(bulletX, bulletY)) {
|
|
console.log(`Bullet checking position (${bulletX},${bulletY})`);
|
|
|
|
// Check if any player is at this position (excluding the shooter)
|
|
for (const playerId in positions) {
|
|
const targetPosition = positions[playerId];
|
|
if (playerId !== shooter.id &&
|
|
targetPosition.x === bulletX &&
|
|
targetPosition.y === bulletY) {
|
|
console.log(`HIT! Player ${playerId} at (${targetPosition.x},${targetPosition.y})`);
|
|
return { ...targetPosition, playerId: playerId };
|
|
}
|
|
}
|
|
|
|
// Continue bullet path
|
|
bulletX += offset.x;
|
|
bulletY += offset.y;
|
|
}
|
|
|
|
console.log('Shot missed - no players hit');
|
|
return null; // No hit
|
|
}
|
|
|
|
// Check if a shot hits any player (legacy function - kept for compatibility)
|
|
checkShotHit(shooter, direction, players) {
|
|
const offset = this.getDirectionOffset(direction);
|
|
let currentX = shooter.x + offset.x;
|
|
let currentY = shooter.y + offset.y;
|
|
|
|
// Follow bullet path until it hits something or goes out of bounds
|
|
while (this.isValidPosition(currentX, currentY)) {
|
|
// Check if any player is at this position
|
|
for (const playerId in players) {
|
|
const player = players[playerId];
|
|
if (player.alive &&
|
|
player.id !== shooter.id &&
|
|
player.x === currentX &&
|
|
player.y === currentY) {
|
|
return player;
|
|
}
|
|
}
|
|
|
|
// Continue bullet path
|
|
currentX += offset.x;
|
|
currentY += offset.y;
|
|
}
|
|
|
|
return null; // No hit
|
|
}
|
|
|
|
// End the game
|
|
async endGame(gameId, winner, reason) {
|
|
await this.db.runQuery(`
|
|
UPDATE games
|
|
SET status = 'finished',
|
|
winner_id = ?,
|
|
end_reason = ?,
|
|
finished_at = CURRENT_TIMESTAMP,
|
|
last_action_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`, [winner ? winner.id : null, reason, gameId]);
|
|
|
|
// Log game end event
|
|
await this.db.logGameEvent(gameId, 0, 0, 'game_ended', winner ? winner.id : null, {
|
|
reason: reason,
|
|
winner_player_number: winner ? winner.number : null
|
|
});
|
|
}
|
|
|
|
// Additional endpoints for features
|
|
async createPlayer(req, res) {
|
|
try {
|
|
const { username, email } = req.body;
|
|
const playerId = await this.db.createPlayer(username, email);
|
|
res.json({ success: true, playerId });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to create player' });
|
|
}
|
|
}
|
|
|
|
async getPlayer(req, res) {
|
|
try {
|
|
const player = await this.db.getRow('SELECT * FROM players WHERE id = ?', [req.params.id]);
|
|
if (!player) {
|
|
return res.status(404).json({ error: 'Player not found' });
|
|
}
|
|
res.json(player);
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to get player' });
|
|
}
|
|
}
|
|
|
|
async getLeaderboard(req, res) {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
const leaderboard = await this.db.getLeaderboard(limit);
|
|
res.json(leaderboard);
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to get leaderboard' });
|
|
}
|
|
}
|
|
|
|
async getGameHistory(req, res) {
|
|
try {
|
|
const history = await this.db.getGameHistory(req.params.id);
|
|
res.json(history);
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to get game history' });
|
|
}
|
|
}
|
|
|
|
async getGameReplay(req, res) {
|
|
try {
|
|
const gameState = await this.db.getGameState(req.params.id);
|
|
const history = await this.db.getGameHistory(req.params.id);
|
|
res.json({ game: gameState, events: history });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to get game replay' });
|
|
}
|
|
}
|
|
|
|
async getTournaments(req, res) {
|
|
try {
|
|
const tournaments = await this.db.getAllRows('SELECT * FROM tournaments ORDER BY created_at DESC');
|
|
res.json(tournaments);
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to get tournaments' });
|
|
}
|
|
}
|
|
|
|
async createTournament(req, res) {
|
|
// Tournament creation logic (future feature)
|
|
res.status(501).json({ error: 'Tournament creation not yet implemented' });
|
|
}
|
|
|
|
// Get game configuration
|
|
async getConfig(req, res) {
|
|
try {
|
|
const GAME_CONFIG = require('../game-config');
|
|
res.json({
|
|
success: true,
|
|
config: GAME_CONFIG
|
|
});
|
|
} catch (error) {
|
|
console.error('Error getting config:', error);
|
|
res.status(500).json({ error: 'Failed to get configuration' });
|
|
}
|
|
}
|
|
|
|
// Create a rematch with the same players
|
|
async createRematch(req, res) {
|
|
try {
|
|
const { id: oldGameId } = req.params;
|
|
const { playerId, playerName } = req.body;
|
|
|
|
if (!playerId || !playerName) {
|
|
return res.status(400).json({ error: 'Player ID and name are required' });
|
|
}
|
|
|
|
// Get the original game to find both players
|
|
const originalGame = await this.db.getGameState(oldGameId);
|
|
if (!originalGame || originalGame.game.status !== 'finished') {
|
|
return res.status(400).json({ error: 'Can only rematch finished games' });
|
|
}
|
|
|
|
// Verify the requesting player was in the original game
|
|
const requestingPlayer = originalGame.players.find(p => p.player_id === playerId);
|
|
if (!requestingPlayer) {
|
|
return res.status(400).json({ error: 'Player was not in the original game' });
|
|
}
|
|
|
|
// Find the opponent
|
|
const opponent = originalGame.players.find(p => p.player_id !== playerId);
|
|
if (!opponent) {
|
|
return res.status(400).json({ error: 'Could not find opponent from original game' });
|
|
}
|
|
|
|
// Create new game
|
|
const newGameId = await this.db.createGame('standard');
|
|
|
|
// Generate spawn positions for new game
|
|
const spawnPositions = this.generateSpawnPositions();
|
|
|
|
// Add the requesting player as player 1
|
|
await this.db.addPlayerToGame(newGameId, playerId, 1, spawnPositions.player1.x, spawnPositions.player1.y);
|
|
|
|
// Log game creation event
|
|
await this.db.logGameEvent(newGameId, 0, 1, 'rematch_created', playerId, {
|
|
original_game_id: oldGameId,
|
|
opponent_id: opponent.player_id,
|
|
spawn_x: spawnPositions.player1.x,
|
|
spawn_y: spawnPositions.player1.y
|
|
});
|
|
|
|
// Check if the opponent is currently active (has recent activity)
|
|
const opponentActiveCheck = await this.db.getRow(`
|
|
SELECT last_active FROM players
|
|
WHERE id = ? AND last_active > datetime('now', '-10 minutes')
|
|
`, [opponent.player_id]);
|
|
|
|
let gameStatus = 'waiting';
|
|
let inviteLink = `${req.protocol}://${req.get('host')}/game/${newGameId}`;
|
|
|
|
// If opponent is recently active, try to auto-add them
|
|
if (opponentActiveCheck) {
|
|
try {
|
|
// Add opponent as player 2
|
|
await this.db.addPlayerToGame(newGameId, opponent.player_id, 2, spawnPositions.player2.x, spawnPositions.player2.y);
|
|
|
|
// Update game status to playing
|
|
await this.db.runQuery(`
|
|
UPDATE games
|
|
SET status = 'playing', started_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`, [newGameId]);
|
|
|
|
// Log opponent auto-join
|
|
await this.db.logGameEvent(newGameId, 0, 2, 'rematch_auto_joined', opponent.player_id, {
|
|
spawn_x: spawnPositions.player2.x,
|
|
spawn_y: spawnPositions.player2.y
|
|
});
|
|
|
|
gameStatus = 'playing';
|
|
} catch (error) {
|
|
console.log('Could not auto-add opponent, will wait for manual join:', error.message);
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
newGameId: newGameId,
|
|
playerId: playerId,
|
|
playerNumber: 1, // The requesting player becomes Player 1 in the rematch
|
|
status: gameStatus,
|
|
inviteLink: inviteLink,
|
|
opponentName: opponent.username,
|
|
spawnPosition: spawnPositions.player1,
|
|
message: gameStatus === 'playing'
|
|
? 'Rematch started! Both players ready.'
|
|
: 'Rematch created. Share the link with your opponent.'
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error creating rematch:', error);
|
|
res.status(500).json({ error: 'Failed to create rematch' });
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = GameAPI; |