From 398bc4a20e0598e09da86437c2e0eab9bd089050 Mon Sep 17 00:00:00 2001 From: Dabit Date: Tue, 10 Mar 2026 21:06:07 +0100 Subject: [PATCH] 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 --- index.html | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/index.html b/index.html index 815cea5..905eefb 100644 --- a/index.html +++ b/index.html @@ -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();