fix(anti-cheat): анти-фарм XP в играх и при повторном завершении урока (Спринт1 #2,#3)
- games: дневной лимит начислений XP за hangman/crossword (DAILY_WIN_CAP=10, счёт по xp_log.reason) — нельзя бесконечно фармить циклом complete. - lessons.markComplete: XP/монеты только при ПЕРВОМ завершении урока (повторные POST больше ничего не начисляют). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,14 @@ function hangmanWord(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
// Анти-фарм: XP за игры начисляется по «честному слову» клиента, поэтому
|
||||
// ограничиваем число начислений за тип игры в сутки (счёт по xp_log.reason).
|
||||
const DAILY_WIN_CAP = 10;
|
||||
function _gameWinsToday(userId, reason) {
|
||||
try { return db.prepare("SELECT COUNT(*) AS n FROM xp_log WHERE user_id=? AND reason=? AND created_at >= date('now')").get(userId, reason).n; }
|
||||
catch (e) { return 0; }
|
||||
}
|
||||
|
||||
/* ── POST /api/games/hangman/complete ─────────────────────────────────── */
|
||||
function hangmanComplete(req, res) {
|
||||
const { won, errors } = req.body;
|
||||
@@ -243,6 +251,7 @@ function hangmanComplete(req, res) {
|
||||
// 15 XP perfect, -2 per error, min 5
|
||||
xpGain = Math.max(5, 15 - (Number(errors) || 0) * 2);
|
||||
}
|
||||
if (xpGain > 0 && _gameWinsToday(req.user.id, 'hangman_win') >= DAILY_WIN_CAP) xpGain = 0;
|
||||
|
||||
if (xpGain > 0) {
|
||||
try { awardXP(req.user.id, xpGain, 'hangman_win'); } catch (e) { console.error('[games] hangman XP:', e.message); }
|
||||
@@ -299,6 +308,7 @@ function crosswordComplete(req, res) {
|
||||
if (completed) {
|
||||
xpGain = Math.max(5, 20 - (Number(hintsUsed) || 0) * 3);
|
||||
}
|
||||
if (xpGain > 0 && _gameWinsToday(req.user.id, 'crossword_win') >= DAILY_WIN_CAP) xpGain = 0;
|
||||
|
||||
if (xpGain > 0) {
|
||||
try { awardXP(req.user.id, xpGain, 'crossword_win'); } catch (e) { console.error('[games] crossword XP:', e.message); }
|
||||
|
||||
Reference in New Issue
Block a user