feat: anti-bot inspekce a vězení + přepínač hraní/objednávání
CI / Generate TypeScript types (push) Successful in 15s
CI / Server unit tests (push) Successful in 24s
CI / Build server (push) Successful in 31s
CI / Build client (push) Successful in 1m0s
CI / Playwright E2E tests (push) Successful in 1m30s
CI / Build and push Docker image (push) Successful in 1m6s
CI / Notify (push) Successful in 5s

- přepínač Hraní/Objednávání; v objednávání hra nic neruší, v hraní herní vrstva odstíní kliknutí
- scéna žije trvale (přepnutí režimu neobejde pavučinu, omráčení, zabavenou síťku ani ban)
- anti-bot: ovládání jen reálnou myší (isTrusted/webdriver), občasná motýlí inspekce (chyť korunovaného)
- neúspěch inspekce = zabavená síťka 45 s (opticky skrytá); opakovaně = serverový ban s mříží a odpočtem
- během banu server nekredituje úlovky (F5/přepnutí-proof)
- větší síťka se teď viditelně zvětší hned po koupi
- changelog route přeskočí vadný JSON (nespadne celý endpoint) + oprava changelogu

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stánek Pavel
2026-07-27 12:56:20 +02:00
co-authored by Claude Opus 4.8
parent 252d105408
commit 377a350211
11 changed files with 502 additions and 22 deletions
+35
View File
@@ -12,6 +12,7 @@ import {
caterpillarAte,
defeatCaterpillar,
mothHit,
reportInspectionFail,
buyPremium,
waspSpray,
buyInsurance,
@@ -44,6 +45,9 @@ import {
CATERPILLAR_EATS,
CATERPILLAR_REWARD,
AUTO_REPAIR_MS,
BAN_FAIL_THRESHOLD,
BAN_DURATION_MS,
BAN_WINDOW_MS,
MOTH_PENALTY,
PREMIUM_BUY_COST,
WASP_SPRAY_COST,
@@ -470,6 +474,37 @@ describe('achievementy', () => {
});
});
describe('anti-bot ban', () => {
test('po prahu selhání inspekce udělí ban', async () => {
const now = 6_000_000;
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, inspectionWindowStart: now });
let stats;
for (let i = 0; i < BAN_FAIL_THRESHOLD - 1; i++) {
stats = await reportInspectionFail(USER, now + i * 1000);
expect(stats.banUntil).toBe(0);
}
stats = await reportInspectionFail(USER, now + BAN_FAIL_THRESHOLD * 1000);
expect(stats.banUntil).toBe(now + BAN_FAIL_THRESHOLD * 1000 + BAN_DURATION_MS);
});
test('selhání mimo okno se nesčítají do banu', async () => {
const now = 7_000_000;
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, inspectionWindowStart: now });
await reportInspectionFail(USER, now);
// daleko za oknem → počítadlo se resetuje
const stats = await reportInspectionFail(USER, now + BAN_WINDOW_MS + 1000);
expect(stats.banUntil).toBe(0);
});
test('během banu se úlovky nezapočítávají', async () => {
const now = 8_000_000;
await seed(USER, { caught: 10, coins: 5, goldenCaught: 0, banUntil: now + BAN_DURATION_MS, lastCatchDay: formatDate(new Date(now)) });
const result = await recordCatches(USER, 20, 0, now);
expect(result.stats.caught).toBe(10); // beze změny
expect(result.coinsAwarded).toBe(0);
});
});
describe('getLeaderboard', () => {
test('řadí sestupně dle počtu chycených a respektuje limit', async () => {
await seed(USER, { caught: 100, coins: 0, goldenCaught: 2 });