feat(02-01): add AI module and extend Physics for paddle2 movement and collision

- Add AI object with init(), update(), _predictBallY() methods
- AI uses predictive ray-cast with wall bounce simulation to intercept ball
- AI respects difficulty config (speed, reactionDelay, errorMargin) from GameConfig
- Add Physics._checkPaddle2Collision() with 5-zone deflection, ball goes LEFT
- Extend Physics.update() with paddle2 movement branch (2p/ai modes)
- Add AI.init() to initialization block
This commit is contained in:
Dabit
2026-03-10 21:06:07 +01:00
parent a2f0bc391b
commit 398bc4a20e

View File

@@ -197,6 +197,16 @@
paddle.y += dir * paddle.speed * deltaTime;
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
// --- Move paddle2 (human or AI) ---
const paddle2 = GameState.paddle2;
if (GameState.mode === '2p') {
const dir2 = Input.getVerticalInput2();
paddle2.y += dir2 * paddle2.speed * deltaTime;
paddle2.y = Math.max(0, Math.min(this.height - paddle2.height, paddle2.y));
} else if (GameState.mode === 'ai') {
AI.update(deltaTime);
}
// --- Move ball ---
ball.x += ball.vx * deltaTime;
ball.y += ball.vy * deltaTime;
@@ -221,6 +231,11 @@
if (ball.vx < 0) {
this._checkPaddleCollision(ball, paddle);
}
// --- Paddle2 collision (only when ball moving right toward paddle2) ---
if (ball.vx > 0) {
this._checkPaddle2Collision(ball, GameState.paddle2);
}
},
_checkPaddleCollision(ball, paddle) {
@@ -261,6 +276,99 @@
// Push ball out of paddle to prevent double-collision next frame
ball.x = paddle.x + paddle.width + ball.radius + 1;
},
_checkPaddle2Collision(ball, paddle) {
const inX = ball.x + ball.radius > paddle.x &&
ball.x - ball.radius < paddle.x + paddle.width;
const inY = ball.y + ball.radius > paddle.y &&
ball.y - ball.radius < paddle.y + paddle.height;
if (!inX || !inY) return;
const relativeHitPos = (ball.y - paddle.y) / paddle.height;
const hitZone = Math.max(0, Math.min(4, Math.floor(relativeHitPos * 5)));
const anglesDeg = [-60, -30, 5, 30, 60];
const angleRad = anglesDeg[hitZone] * Math.PI / 180;
ball.speed += GameConfig.speedIncrement;
// Ball leaves paddle2 going LEFT (negative vx)
ball.vx = -Math.cos(angleRad) * ball.speed;
ball.vy = Math.sin(angleRad) * ball.speed;
// Push ball left of paddle2 to prevent double-collision
ball.x = paddle.x - ball.radius - 1;
}
};
const AI = {
reactionElapsed: 0,
init() {
this.reactionElapsed = 0;
},
update(deltaTime) {
const difficulty = GameState.difficulty;
const config = GameConfig['AI_' + difficulty.toUpperCase()];
const ball = GameState.ball;
const paddle2 = GameState.paddle2;
// Reset reaction timer when ball is moving away (toward player side)
if (ball.vx < 0) {
this.reactionElapsed = 0;
return; // Ball heading toward player — AI waits
}
this.reactionElapsed += deltaTime;
if (this.reactionElapsed < config.reactionDelay) {
return; // Still in reaction delay window
}
// Predict ball y-position when it reaches paddle2 x
const targetY = this._predictBallY(ball, paddle2.x);
// Add difficulty-based error margin (random jitter each frame)
const error = (Math.random() - 0.5) * config.errorMargin * 2;
const aimY = targetY + error;
// Move paddle toward aim Y (clamped to field)
const centerY = paddle2.y + paddle2.height / 2;
const diff = aimY - centerY;
if (Math.abs(diff) > 3) { // Deadzone prevents jitter
const dir = Math.sign(diff);
paddle2.y += dir * config.speed * deltaTime;
paddle2.y = Math.max(0, Math.min(Physics.height - paddle2.height, paddle2.y));
}
},
_predictBallY(ball, targetX) {
let x = ball.x, y = ball.y, vx = ball.vx, vy = ball.vy;
const maxSteps = 500;
let steps = 0;
while (steps < maxSteps) {
if (Math.abs(x - targetX) < 1) break;
const timeToTarget = (targetX - x) / vx;
const timeStep = Math.min(0.016, Math.abs(timeToTarget));
x += vx * timeStep;
y += vy * timeStep;
// Wall bounces during prediction
if (y - ball.radius < 0) {
y = ball.radius;
vy = Math.abs(vy);
} else if (y + ball.radius > Physics.height) {
y = Physics.height - ball.radius;
vy = -Math.abs(vy);
}
steps++;
}
return y;
}
};
@@ -304,6 +412,7 @@
Renderer.init();
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
Input.init();
AI.init();
GameLoop.start();
</script>
</body>