From b40bc1a516c288c98052585d4e33996ae8155856 Mon Sep 17 00:00:00 2001 From: aevgarik Date: Sun, 24 May 2026 18:43:33 +0300 Subject: [PATCH] =?UTF-8?q?Doc=20fix=20+=20stuck=20telemetry=20position=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20hot-spot=20analysis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on c9826ce — 3 findings, 2 actionable, 1 backlog. ## #2 Low: comment в flipperState расходится с реализацией Старый комментарий писал что press во время releasing rejected, но тесты и код специально разрешают re-press (responsive design choice). Fixed: переписан state diagram с explicit «releasing» state как intermediate (cooldown ещё НЕ активен). Добавлено явное упоминание что re-press allowed во время releasing — это design choice, не bug. ## #1 High backlog: bonus telemetry для hot-spot analysis Codex отметил residual ball_unstuck/hard_respawn без конкретных hot-spot данных. Чтобы убрать blind tuning, добавил позицию + bumper type в telemetry events: - ball_unstuck: + ballPositionX, ballPositionY, overlappingBumperType - ball_hard_respawn: + ballPositionX, ballPositionY findBumperTypeAtPosition(pos): O(N) lookup среди fixed+setup bumpers по координатам (с 1px tolerance). Возвращает «:» строку для аналитики (e.g., «player_setup:curve» — самый частый stuck-spot будет видно сразу при анализе аналитики). Этого достаточно для будущего targeted fix без необходимости guess-tuning. ## #3 Known backlog npm audit Vite/esbuild moderate — отложен до Phase 5 Vite 8 upgrade. 186/186 tests, typecheck/lint/build ✅. Co-Authored-By: Claude Opus 4.7 --- src/game/flipperState.ts | 29 +++++++++++++++++++---------- src/scenes/MatchScene.ts | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/game/flipperState.ts b/src/game/flipperState.ts index d70feef..6a5ea6a 100644 --- a/src/game/flipperState.ts +++ b/src/game/flipperState.ts @@ -2,16 +2,25 @@ // Extracted из Flipper.ts чтобы isPressed/cooldown логика была unit-тестируемой // без mocking Phaser tweens. // -// State transitions: -// idle (isPressed=false, cooldown expired) -// → press(now) → pressed (isPressed=true) -// → press during cooldown / already pressed → rejected -// pressed -// → release() → releasing (isPressed=false, tween в процессе) -// → press → rejected (already pressed) -// releasing -// → onReleaseComplete(now) → cooldown started (cooldownUntil = now + cooldownMs) -// → press(now) во время releasing → rejected (если в cooldown) +// States and transitions: +// idle (isPressed=false, cooldownUntil expired) +// press(now) → pressed +// press during cooldown → rejected +// +// pressed (isPressed=true) +// release() → releasing +// press → rejected (already pressed) +// +// releasing (isPressed=false, release-tween в процессе, cooldown ЕЩЁ НЕ активен) +// press(now) → pressed (ALLOWED — responsive re-press до завершения tween; +// cooldownUntil ещё старое значение, canPress=true) +// onReleaseComplete(now) → cooldown starts (cooldownUntil = now + cooldownMs) +// +// cooldown (после onReleaseComplete до истечения cooldownMs) +// press(now < cooldownUntil) → rejected +// time passes → idle +// +// Re-press allowed во время releasing — design choice для responsive control. // // Flipper.ts wrapper'ит это с Phaser tweens + Matter body updates. diff --git a/src/scenes/MatchScene.ts b/src/scenes/MatchScene.ts index f8f1c33..1a7d027 100644 --- a/src/scenes/MatchScene.ts +++ b/src/scenes/MatchScene.ts @@ -584,17 +584,37 @@ export class MatchScene extends Phaser.Scene { matchId: this.matchId, attempt: this.stuckWatchdog.getNudgeAttempts(), hadOverlap: overlap !== null, + // Position для hot-spot analysis — где конкретно ball застревает (codex backlog #1) + ballPositionX: Math.round(bp.x), + ballPositionY: Math.round(bp.y), + overlappingBumperType: overlap?.body + ? this.findBumperTypeAtPosition(overlap.body.position) + : null, matchTimeSeconds: (this.time.now - this.matchStartTime) / 1000, }); } + /** Найти bumper type на позиции (для telemetry hot-spot analysis). */ + private findBumperTypeAtPosition(pos: { x: number; y: number }): string | null { + for (const b of [...this.fixedBumpers, ...this.setupBumpers]) { + const body = b.getBody(); + if (Math.abs(body.position.x - pos.x) < 1 && Math.abs(body.position.y - pos.y) < 1) { + return `${b.source}:${b.type}`; + } + } + return null; + } + /** * Hard respawn — мяч действительно не разлипается после 3 nudge. * Уничтожаем тело, спавним из бокового порта в нейтральном цвете. */ private applyStuckHardRespawn(): void { + const bp = this.ball.position; this.trackEvent('ball_hard_respawn', { matchId: this.matchId, + ballPositionX: Math.round(bp.x), + ballPositionY: Math.round(bp.y), matchTimeSeconds: (this.time.now - this.matchStartTime) / 1000, }); this.ball.destroy();