fix(ads): share one interstitial cooldown timer across kinds
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 25s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s

Cooldowns were tracked per kind ({move,hint}), so a hint ad and a move ad
did not see each other: after a hinted move's ad the move timer was still
zero, and the next plain move fired an ad immediately — two ads within a
minute, under the cooldown. Track a single shared last-shown time; the kind
only selects the required gap (hint 1m, move 5m, vs_ai 30m) measured from the
last interstitial of any kind. A hint can still fire on its shorter gap
(D30's "independent" hint cooldown), but never stacks a second ad within a
cooldown.

Tests: a hint uses the shorter shared gap; a move does not stack onto a
just-shown hint ad.
This commit is contained in:
Ilia Denisov
2026-07-10 03:36:18 +02:00
parent 2ce80c241d
commit b5c8a04f0b
5 changed files with 53 additions and 31 deletions
+16 -6
View File
@@ -79,7 +79,7 @@ describe('maybeShowInterstitial (client-mirrored gate)', () => {
expect(mocks.showToast).not.toHaveBeenCalled();
});
it('respects the per-kind cooldown: a second move within the window is gated', async () => {
it('respects the cooldown: a second move within the window is gated', async () => {
expect(await maybeShowInterstitial(ADS, 'move', ONLINE)).toBe(true);
vi.setSystemTime(BASE + 299_000); // +299s < the 300s global cooldown
expect(await maybeShowInterstitial(ADS, 'move', ONLINE)).toBe(false);
@@ -88,16 +88,26 @@ describe('maybeShowInterstitial (client-mirrored gate)', () => {
expect(mocks.vkShowInterstitial).toHaveBeenCalledTimes(2);
});
it('tracks move and hint cooldowns independently', async () => {
it('shares one timer across kinds: a hint uses the shorter gap from the last ad', async () => {
// A move ad, then a hint: the hint is held until its own (shorter) 60s gap from THAT ad has
// elapsed — not fired at once (a per-kind timer used to let it fire immediately).
expect(await maybeShowInterstitial(ADS, 'move', ONLINE)).toBe(true);
// A hint right after a move still fires — a separate kind key with its own 60s cooldown.
expect(await maybeShowInterstitial(ADS, 'hint', ONLINE)).toBe(true);
vi.setSystemTime(BASE + 59_000); // +59s < the 60s hint cooldown
vi.setSystemTime(BASE + 59_000);
expect(await maybeShowInterstitial(ADS, 'hint', ONLINE)).toBe(false);
vi.setSystemTime(BASE + 60_000);
vi.setSystemTime(BASE + 60_000); // the hint's 60s gap from the last ad has elapsed
expect(await maybeShowInterstitial(ADS, 'hint', ONLINE)).toBe(true);
});
it('does not stack a move ad onto a just-shown hint ad (the reported bug)', async () => {
// A vs_ai hint-move fires the hint ad; a plain vs_ai move 30s later must NOT fire — the shared
// timer holds it for the vs_ai gap. Separate per-kind timers let it fire at once (the bug).
const vsAi = { vsAi: true, online: true };
expect(await maybeShowInterstitial(ADS, 'hint', vsAi)).toBe(true);
vi.setSystemTime(BASE + 30_000); // 30s later, far under the 1800s vs_ai gap
expect(await maybeShowInterstitial(ADS, 'move', vsAi)).toBe(false);
expect(mocks.vkShowInterstitial).toHaveBeenCalledTimes(1); // only the hint ad fired
});
it('uses the longer vs_ai cooldown for a vs_ai move', async () => {
expect(await maybeShowInterstitial(ADS, 'move', { vsAi: true, online: true })).toBe(true);
vi.setSystemTime(BASE + 1_799_000); // +1799s < the 1800s vs_ai cooldown