togethere.cloud/private_html/disciplines/ping-pong/js/bot-ai.js

230 lines
8.1 KiB
JavaScript

/**
* 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