- Curve bumper: spin-modifier ±35° на отскоке (детерминированный знак на bumper) + визуальный вихрь-индикатор. Теперь curve механически отличается от standard. - Easy/Medium AI delta: распознавательная дистанция (Easy 150px / Medium 280px), reliability (Easy 0.75 / Medium 0.95). difficulty прокидывается из каждой personality в shouldActivateFlipperBase. Hard остаётся placeholder для Phase 3. - Share button на ResultScene: snapshot canvas → PNG blob → platform.shareImage(). Mock логирует, YG использует SDK share. Telemetry: share_clicked / share_failed. - Setup UX переписан: палитра типов сверху (4 типа + trash) + tap-палитра-tap-слот (unified mobile/desktop) + drag-from-palette ghost (desktop). Long-tap слота без выбранного типа удаляет содержимое (быстрый shortcut). Заменил cycle-on-click на normalуй setup, как в DoD. Closes codex finding #2 (Phase 2 DoD gap). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
76 lines
2.2 KiB
TypeScript
76 lines
2.2 KiB
TypeScript
import type {
|
|
AIPlayer,
|
|
AIPlayerContext,
|
|
} from '../AIPlayer';
|
|
import { shouldActivateFlipperBase } from '../AIPlayer';
|
|
import type {
|
|
AIDifficulty,
|
|
AIDifficultyParams,
|
|
AIPersonality,
|
|
SetupConfig,
|
|
} from '../../types';
|
|
|
|
// DefensiveAI — «Бэтти-Защитница».
|
|
// Стратегия: цепкая защита, бустер только на cradle (не реализовано в Phase 1-2).
|
|
// Setup: 2 slingshot в углах ворот, 1 турбо рядом, 1 вираж в дальнем углу, 1 пустой.
|
|
// см. ~/Knowledge/Projects/pinball-duel/concepts/bot-ai-design.md#1-defensive-«бэтти-защитница»
|
|
|
|
export class DefensiveAI implements AIPlayer {
|
|
readonly personality: AIPersonality = 'defensive';
|
|
readonly difficulty: AIDifficulty;
|
|
readonly params: AIDifficultyParams;
|
|
|
|
private lastReactionTimeLeft = 0;
|
|
private lastReactionTimeRight = 0;
|
|
|
|
constructor(difficulty: AIDifficulty, params: AIDifficultyParams) {
|
|
this.difficulty = difficulty;
|
|
this.params = params;
|
|
}
|
|
|
|
update(ctx: AIPlayerContext): void {
|
|
// Defensive — реагирует надёжно на любой приближающийся мяч
|
|
const shouldLeft = shouldActivateFlipperBase(
|
|
ctx,
|
|
'left',
|
|
this.params,
|
|
this.lastReactionTimeLeft,
|
|
this.difficulty,
|
|
);
|
|
if (shouldLeft) {
|
|
if (ctx.leftFlipper.activate()) {
|
|
this.lastReactionTimeLeft = performance.now();
|
|
}
|
|
}
|
|
const shouldRight = shouldActivateFlipperBase(
|
|
ctx,
|
|
'right',
|
|
this.params,
|
|
this.lastReactionTimeRight,
|
|
this.difficulty,
|
|
);
|
|
if (shouldRight) {
|
|
if (ctx.rightFlipper.activate()) {
|
|
this.lastReactionTimeRight = performance.now();
|
|
}
|
|
}
|
|
}
|
|
|
|
generateSetup(): SetupConfig {
|
|
// см. bot-ai-design.md#defensive-setup-эвристика
|
|
return {
|
|
slots: [
|
|
{ slotId: 'goal_corner_left', bumperType: 'slingshot' },
|
|
{ slotId: 'goal_corner_right', bumperType: 'slingshot' },
|
|
{ slotId: 'near_goal_center', bumperType: 'turbo' },
|
|
{ slotId: 'mid_left', bumperType: 'curve' },
|
|
{ slotId: 'mid_right', bumperType: null }, // 1 пустой слот
|
|
],
|
|
};
|
|
}
|
|
|
|
destroy(): void {
|
|
// No state to clean
|
|
}
|
|
}
|