2025-10-30 04:27:43 -04:00
|
|
|
const express = require('express');
|
|
|
|
|
const db = require('../database');
|
|
|
|
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
2025-10-30 13:27:55 -04:00
|
|
|
/**
|
|
|
|
|
* Select a game using weighted random selection based on favor bias
|
|
|
|
|
*
|
|
|
|
|
* Bias system:
|
|
|
|
|
* - favor_bias = 1 (favored): 3x weight
|
|
|
|
|
* - favor_bias = 0 (neutral): 1x weight
|
|
|
|
|
* - favor_bias = -1 (disfavored): 0.2x weight (still possible but less likely)
|
|
|
|
|
*
|
|
|
|
|
* Also considers pack-level bias
|
|
|
|
|
*/
|
|
|
|
|
function selectGameWithBias(games) {
|
|
|
|
|
if (games.length === 0) return null;
|
|
|
|
|
if (games.length === 1) return games[0];
|
|
|
|
|
|
|
|
|
|
// Get pack biases
|
|
|
|
|
const packs = db.prepare('SELECT name, favor_bias FROM packs').all();
|
|
|
|
|
const packBiasMap = {};
|
|
|
|
|
packs.forEach(pack => {
|
|
|
|
|
packBiasMap[pack.name] = pack.favor_bias || 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Calculate weights for each game
|
|
|
|
|
const weights = games.map(game => {
|
|
|
|
|
let weight = 1.0; // Base weight
|
|
|
|
|
|
|
|
|
|
// Apply game-level bias
|
|
|
|
|
const gameBias = game.favor_bias || 0;
|
|
|
|
|
if (gameBias === 1) {
|
|
|
|
|
weight *= 3.0; // Favored games are 3x more likely
|
|
|
|
|
} else if (gameBias === -1) {
|
|
|
|
|
weight *= 0.2; // Disfavored games are 5x less likely
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply pack-level bias
|
|
|
|
|
const packBias = packBiasMap[game.pack_name] || 0;
|
|
|
|
|
if (packBias === 1) {
|
|
|
|
|
weight *= 2.0; // Favored packs are 2x more likely
|
|
|
|
|
} else if (packBias === -1) {
|
|
|
|
|
weight *= 0.3; // Disfavored packs are ~3x less likely
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return weight;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Calculate total weight
|
|
|
|
|
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
|
|
|
|
|
|
|
|
|
// Pick a random number between 0 and totalWeight
|
|
|
|
|
let random = Math.random() * totalWeight;
|
|
|
|
|
|
|
|
|
|
// Select game based on weighted random
|
|
|
|
|
for (let i = 0; i < games.length; i++) {
|
|
|
|
|
random -= weights[i];
|
|
|
|
|
if (random <= 0) {
|
|
|
|
|
return games[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback (shouldn't reach here)
|
|
|
|
|
return games[games.length - 1];
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 04:27:43 -04:00
|
|
|
// Pick a random game with filters and repeat avoidance
|
|
|
|
|
router.post('/pick', (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const {
|
|
|
|
|
playerCount,
|
|
|
|
|
drawing,
|
|
|
|
|
length,
|
|
|
|
|
familyFriendly,
|
2025-10-30 13:27:55 -04:00
|
|
|
sessionId,
|
|
|
|
|
excludePlayed
|
2025-10-30 04:27:43 -04:00
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
// Build query for eligible games
|
|
|
|
|
let query = 'SELECT * FROM games WHERE enabled = 1';
|
|
|
|
|
const params = [];
|
|
|
|
|
|
|
|
|
|
if (playerCount) {
|
|
|
|
|
const count = parseInt(playerCount);
|
|
|
|
|
query += ' AND min_players <= ? AND max_players >= ?';
|
|
|
|
|
params.push(count, count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (drawing === 'only') {
|
|
|
|
|
query += ' AND game_type = ?';
|
|
|
|
|
params.push('Drawing');
|
|
|
|
|
} else if (drawing === 'exclude') {
|
|
|
|
|
query += ' AND (game_type != ? OR game_type IS NULL)';
|
|
|
|
|
params.push('Drawing');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (length) {
|
|
|
|
|
if (length === 'short') {
|
|
|
|
|
query += ' AND (length_minutes <= 15 OR length_minutes IS NULL)';
|
|
|
|
|
} else if (length === 'medium') {
|
|
|
|
|
query += ' AND length_minutes > 15 AND length_minutes <= 25';
|
|
|
|
|
} else if (length === 'long') {
|
|
|
|
|
query += ' AND length_minutes > 25';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (familyFriendly !== undefined) {
|
|
|
|
|
query += ' AND family_friendly = ?';
|
|
|
|
|
params.push(familyFriendly ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get eligible games
|
|
|
|
|
const stmt = db.prepare(query);
|
|
|
|
|
let eligibleGames = stmt.all(...params);
|
|
|
|
|
|
|
|
|
|
if (eligibleGames.length === 0) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
error: 'No games match the current filters',
|
|
|
|
|
suggestion: 'Try adjusting your filters or enabling more games'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply repeat avoidance if session provided
|
|
|
|
|
if (sessionId) {
|
2025-10-30 13:27:55 -04:00
|
|
|
let lastGamesQuery;
|
|
|
|
|
|
|
|
|
|
if (excludePlayed) {
|
|
|
|
|
// Exclude ALL previously played games in this session
|
|
|
|
|
lastGamesQuery = db.prepare(`
|
|
|
|
|
SELECT DISTINCT game_id FROM session_games
|
|
|
|
|
WHERE session_id = ?
|
|
|
|
|
`).all(sessionId);
|
|
|
|
|
} else {
|
|
|
|
|
// Default: only exclude last 2 games
|
|
|
|
|
lastGamesQuery = db.prepare(`
|
|
|
|
|
SELECT game_id FROM session_games
|
|
|
|
|
WHERE session_id = ?
|
|
|
|
|
ORDER BY played_at DESC
|
|
|
|
|
LIMIT 2
|
|
|
|
|
`).all(sessionId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const excludeIds = lastGamesQuery.map(g => g.game_id);
|
2025-10-30 04:27:43 -04:00
|
|
|
|
|
|
|
|
if (excludeIds.length > 0) {
|
|
|
|
|
eligibleGames = eligibleGames.filter(game => !excludeIds.includes(game.id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (eligibleGames.length === 0) {
|
|
|
|
|
return res.status(404).json({
|
2025-10-30 13:27:55 -04:00
|
|
|
error: excludePlayed
|
|
|
|
|
? 'All eligible games have been played in this session'
|
|
|
|
|
: 'All eligible games have been played recently',
|
2025-10-30 04:27:43 -04:00
|
|
|
suggestion: 'Enable more games or adjust your filters',
|
|
|
|
|
recentlyPlayed: excludeIds
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 13:27:55 -04:00
|
|
|
// Apply favor biasing to selection
|
|
|
|
|
const selectedGame = selectGameWithBias(eligibleGames);
|
2025-10-30 04:27:43 -04:00
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
game: selectedGame,
|
|
|
|
|
poolSize: eligibleGames.length,
|
|
|
|
|
totalEnabled: eligibleGames.length + (sessionId ? 2 : 0) // Approximate
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
module.exports = router;
|
|
|
|
|
|