/** * Neon Ping-Pong Bot AI Module * Copyright (c) 2026 Wspólnie - wspolpraca@togethere.cloud * All rights reserved. Unauthorized copying, distribution, or modification is prohibited. * * AI dla bota w różnych poziomach trudności * Skalowalne dla przyszłych ulepszeń */ (function() { 'use strict'; class BotAI { constructor() { // Konfiguracja dla różnych poziomów trudności this.difficulties = { easy: { maxSpeed: 2.7, reactionDelay: 11, accuracy: 0.45, predictionEnabled: true, predictionStrength: 0.21, returnToCenter: true, aimStrength: 0.018, gain: 0.072, deadzone: 17, closeRangeBoost: 1.012 }, medium: { maxSpeed: 4.2, reactionDelay: 7, accuracy: 0.53, predictionEnabled: true, predictionStrength: 0.41, returnToCenter: true, aimStrength: 0.072, gain: 0.114, deadzone: 11, closeRangeBoost: 1.06 }, hard: { maxSpeed: 5.2, reactionDelay: 5, accuracy: 0.57, predictionEnabled: true, predictionStrength: 0.52, returnToCenter: true, aimStrength: 0.108, gain: 0.132, deadzone: 10, closeRangeBoost: 1.09 }, extreme: { maxSpeed: 7.5, reactionDelay: 2, accuracy: 0.92, predictionEnabled: true, predictionStrength: 0.88, returnToCenter: true, aimStrength: 0.185, gain: 0.195, deadzone: 3, closeRangeBoost: 1.22 } }; this.reactionCounter = 0; this._cachedTargetY = null; } /** * Aktualizacja pozycji bota * @param {Object} bot - Obiekt bota z pozycją i wymiarami * @param {Object} ball - Obiekt piłki z pozycją i prędkością * @param {String} difficulty - Poziom trudności ('easy', 'medium', 'hard') */ update(bot, ball, difficulty = 'easy', canvasHeight = 450) { let config = this.difficulties[difficulty]; if (!config) { console.warn(`Nieznany poziom trudności: ${difficulty}. Używam 'easy'.`); config = this.difficulties.easy; } // Opóźnienie reakcji: bot aktualizuje "cel" co N klatek (żeby nie był nadludzki) this.reactionCounter++; if (this.reactionCounter >= config.reactionDelay || this._cachedTargetY === null) { this.reactionCounter = 0; this._cachedTargetY = this.computeTargetY(bot, ball, config, canvasHeight); } const botCenter = bot.y + bot.height / 2; const desiredCenter = this._cachedTargetY; // Sterowanie płynne: prędkość zależy od dystansu const error = desiredCenter - botCenter; const deadzone = Number.isFinite(config.deadzone) ? config.deadzone : 6; if (Math.abs(error) <= deadzone) { return; } // Gain: jak agresywnie goni cel; większe na trudniejszych const gain = Number.isFinite(config.gain) ? config.gain : (config.predictionEnabled ? 0.22 : 0.18); // Mały boost prędkości, gdy piłka jest już blisko bota (na wyższych trudnościach) let maxSpeed = config.maxSpeed; if (ball && typeof ball.dx === 'number' && ball.dx > 0) { const distanceToBotX = (bot.x - ball.x); if (Number.isFinite(distanceToBotX) && distanceToBotX < 220) { const boost = Number.isFinite(config.closeRangeBoost) ? config.closeRangeBoost : 1.0; maxSpeed = maxSpeed * boost; } } const step = this.clamp(error * gain, -maxSpeed, maxSpeed); bot.y += step; // Bezpieczny clamp w canvas (uwzględnia rozmiar paletki) if (bot.y < 0) bot.y = 0; if (bot.y + bot.height > canvasHeight) bot.y = canvasHeight - bot.height; } computeTargetY(bot, ball, config, canvasHeight) { const radius = ball.radius || 0; const minY = radius; const maxY = Math.max(minY, canvasHeight - radius); // Gdy piłka leci od bota (w lewo), bot wraca w okolice środka if (ball.dx <= 0) { if (config.returnToCenter) { const center = canvasHeight / 2; return this.clamp(center, minY, maxY); } return this.clamp(ball.y, minY, maxY); } // Predykcja: gdzie piłka przetnie linię bota (z odbiciami) let targetY = ball.y; if (config.predictionEnabled) { targetY = this.predictBallPosition(ball, bot, canvasHeight, config.predictionStrength); } // Strategia: na trudniejszych poziomach lekko celuje w krawędzie (żeby wybijać pod kątem) if (config.aimStrength && config.aimStrength > 0) { const direction = ball.dy >= 0 ? 1 : -1; targetY += direction * bot.height * config.aimStrength; } // Realistyczny błąd (skalowany paletką, nie stałe 100px) const maxError = (1 - config.accuracy) * bot.height * 0.9; targetY += (Math.random() - 0.5) * 2 * maxError; return this.clamp(targetY, minY, maxY); } /** * Przewiduje pozycję piłki gdy dotrze do bota * @param {Object} ball - Obiekt piłki * @param {Object} bot - Obiekt bota * @param {Number} strength - Siła predykcji (0-1) * @returns {Number} Przewidywana pozycja Y */ predictBallPosition(ball, bot, canvasHeight, strength) { // Czas dotarcia do osi bota (dx > 0 gwarantowane wyżej) const dx = ball.dx; if (dx <= 0) return ball.y; const distanceToBotX = (bot.x - ball.x); const timeToReach = distanceToBotX / dx; if (!Number.isFinite(timeToReach) || timeToReach <= 0) return ball.y; const radius = ball.radius || 0; const top = radius; const bottom = Math.max(top, canvasHeight - radius); let predictedY = ball.y + (ball.dy * timeToReach); // Odbicia od ścian: odbijaj w przedziale [top, bottom] let guard = 0; while (predictedY < top || predictedY > bottom) { if (predictedY < top) { predictedY = top + (top - predictedY); } else if (predictedY > bottom) { predictedY = bottom - (predictedY - bottom); } guard++; if (guard > 20) break; } // Mieszaj predykcję z bieżącą pozycją (żeby łatwiejsze poziomy nie były "laserem") const mixed = ball.y * (1 - strength) + predictedY * strength; return this.clamp(mixed, top, bottom); } clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } /** * Ustawia niestandardową konfigurację dla poziomu trudności * @param {String} difficulty - Nazwa poziomu trudności * @param {Object} config - Konfiguracja */ setCustomDifficulty(difficulty, config) { this.difficulties[difficulty] = { ...this.difficulties.easy, ...config }; } /** * Pobiera konfigurację dla poziomu trudności * @param {String} difficulty - Nazwa poziomu trudności * @returns {Object} Konfiguracja */ getDifficultyConfig(difficulty) { return this.difficulties[difficulty] || this.difficulties.easy; } } // Eksportuj klasę if (typeof window !== 'undefined') { window.BotAI = BotAI; // Utwórz globalną instancję window.botAI = new BotAI(); // Freeze object to prevent modifications Object.freeze(window.botAI.difficulties); } })(); // End of IIFE