I’m currently developing a Pong game in JavaScript for a school project. One rule I have is that the AI can only check the game state once every second.
Currently, the AI operates on two simple rules:
- (A): If the ball is moving away from the AI, it returns to the center of the board.
- (B): If the ball is coming toward the AI, it tries to catch it and then returns to the center.
This strategy works well when the ball is slow because the AI almost always gets to look at the game state when the ball is still in the player’s half of the board and coming towards the AI and thus we have time to apply rule B.
My problem is that when the ball is moving at a normal speed, the AI often gets to look at the game state when the ball is the player’s half of the board and moving away from the AI and thus we apply rule A. By the time the AI gets to look at the game again it is often too late since the ball is too close to react and we don’t have enough time to move to the ball’s location.
I would like your help to improve rule A, for exemple by trying to predict in which area of the board the ball will end up when the player send it back to us.
Here is the code for the AI:
import * as g from './global.js';
import * as game from './game.js';
import * as physics from './physics.js';
/*
* The behaves according to the following rules:
*
* Since we can look at the state of the game once per second, it means
* that we have a 'budget' of 60 ticks to use to make our actions.
*
* The budget is divided as follow :
* | move towards the ball | wait for collision | move back towards the center | wait for the remainder of the budget |
*
*/
export class AIManager {
constructor() {
this.last_looked = 0.0;
this.game = new game.Game();
this.collision_point = new physics.Vector(0, 0);
this.inputs = [];
this.nput_movement = g.INPUT_NEUTRAL;
this.input_center = g.INPUT_NEUTRAL;
this.tick_budget = 60; /* The number of ticks we have before the next time we update our view */
this.tick_collision = 0; /* The number of ticks before the ball collide with the paddle */
this.tick_movement = 0; /* The minimum number of ticks before the paddle reaches the collision point */
this.tick_center = 0; /* The number of ticks need to go back to the center of the board */
this.intersect_area = new physics.Rectangle(0, 2 * g.BOARD_WALL, g.BOARD_WIDTH, g.BALL_SIDE, 0, 0);
}
reset() {
this.last_looked = performance.now();
this.game = new game.Game();
this.collision_point = new physics.Vector(0, 0);
this.inputs = [];
this.nput_movement = g.INPUT_NEUTRAL;
this.input_center = g.INPUT_NEUTRAL;
this.tick_budget = 60; /* The number of ticks we have before the next time we update our view */
this.tick_collision = 0; /* The number of ticks before the ball collide with the paddle */
this.tick_movement = 0; /* The minimum number of ticks before the paddle reaches the collision point */
this.tick_center = 0; /* The number of ticks need to go back to the center of the board */
this.intersect_area = new physics.Rectangle(0, 2 * g.BOARD_WALL, g.BOARD_WIDTH, g.BALL_SIDE, 0, 0);
}
refresh(game, dt) {
const elapsed = performance.now() - this.last_looked;
if (elapsed >= 1000) {
this.reset();
/* Load the game state into the AI's 'memory' */
this.load_state(game);
if (this.game.ball.velocity.y >= 0) {
this.tick_movement = 0;
this.tick_collision = 0;
this.handle_reset(dt);
} else {
this.handle_collision(dt);
this.handle_movement(dt);
this.handle_reset(dt);
}
this.queue_moves();
}
return this.inputs.shift();
}
handle_collision(dt) {
let tick = 0;
let collision = null;
let collision_happened = false;
while (!collision_happened && tick < 120) {
tick += 1;
collision = physics.aabb_continuous_detection(this.game.ball, this.intersect_area, dt);
if (collision.time > 0 && collision.time <= 1.0) {
collision_happened = true;
} else {
this.game.ball.position.x += this.game.ball.velocity.x * dt;
this.game.ball.position.y += this.game.ball.velocity.y * dt;
}
if (this.game.ball.position.x <= g.BOARD_WALL || this.game.ball.position.x + this.game.ball.size.x >= g.BOARD_WIDTH - g.BOARD_WALL) {
/* Left and right walls */
this.game.ball.position.x = this.game.ball.position.x <= g.BOARD_WALL ? g.BOARD_WALL : g.BOARD_WIDTH - this.game.ball.size.x - g.BOARD_WALL;
this.game.ball.velocity.x *= -1;
}
}
this.tick_collision = tick;
this.collision_point.x = collision.point.x;
this.collision_point.y = collision.point.y;
}
handle_movement(dt) {
let tick = 0;
const paddle = this.game.player2;
const paddle_center = this.game.player2.position.x + (this.game.player2.size.x / 2);
if (this.collision_point.x < paddle_center) {
this.game.player2.velocity.x = -g.PADDLE_SPEED;
this.nput_movement = g.INPUT_LEFT;
} else {
this.game.player2.velocity.x = g.PADDLE_SPEED;
this.nput_movement = g.INPUT_RIGHT;
}
while (!(paddle.position.x < this.collision_point.x && paddle.position.x + paddle.size.x >= this.collision_point.x) && tick < 60) {
tick += 1;
if (paddle.position.x + paddle.velocity.x * dt > g.BOARD_CORRIDOR && paddle.position.x + paddle.size.x + paddle.velocity.x * dt < g.BOARD_WIDTH - g.BOARD_CORRIDOR) {
paddle.position.x += paddle.velocity.x * dt;
}
}
this.tick_movement = tick;
}
handle_reset(dt) {
const board_center = g.BOARD_WIDTH / 2;
const paddle_center = this.game.player2.position.x + (this.game.player2.size.x / 2);
const distance_from_center = board_center - paddle_center;
const velocity = board_center <= paddle_center ? -g.PADDLE_SPEED : g.PADDLE_SPEED;
const delta_move = velocity * dt;
if (distance_from_center < 0) {
this.input_center = g.INPUT_LEFT;
} else if (distance_from_center > 0) {
this.input_center = g.INPUT_RIGHT;
}
this.tick_center = Math.abs(Math.floor(distance_from_center / delta_move));
}
queue_moves() {
for (let i = 0; i < this.tick_movement && this.tick_budget > 0; i++) {
this.inputs.push(this.nput_movement);
this.tick_budget -= 1;
}
for (let i = 0; i < this.tick_collision && this.tick_budget > 0; i++) {
this.inputs.push(g.INPUT_NEUTRAL);
this.tick_budget -= 1;
}
for (let i = 0; i < this.tick_center && this.tick_budget > 0; i++) {
this.inputs.push(this.input_center);
this.tick_budget -= 1;
}
for (let i = 0; this.tick_budget > 0; i++) {
this.inputs.push(g.INPUT_NEUTRAL);
this.tick_budget -= 1;
}
}
load_state(game) {
this.game.status = game.status;
this.game.who_serves = game.who_serves;
this.game.ball.position.x = game.ball.position.x;
this.game.ball.position.y = game.ball.position.y;
this.game.ball.velocity.x = game.ball.velocity.x;
this.game.ball.velocity.y = game.ball.velocity.y;
this.game.player1.position.x = game.player1.position.x;
this.game.player2.position.x = game.player2.position.x;
}
}
2
Answers
you can predict the position of the ball so your AI can react quickly even with checking the game state only once every second…
Im not sure about your games infrastructure and how everything is calculated but this should give you a vague idea of what im trying to do…
This will result in quicker reactions
funtion to predict the ball location:
Use of the predict function in your code:
Keep in mind that for precise predictions, you’ll need to tweak the collision and bounce logic to match your game’s rules. Basically, the better your prediction mimics your game’s physics, the more accurate it will be.
Unless you are creating a Pong version that includes special physics you could use simple maths instead of a complete simulation…
The movement of the ball is linear and constant, it will just bounce off the walls but bounces only create a change in the horizontal direction. This can be expressed using a simple Triangle Wave.
With this, we can substitute some variables already.
Now we also need to calculate the frequency, at what rate will the ball bounce… For this we calculate how much Y will it have to traverse to travel a full board’s width. We can also simplify by just using the ratio they produce and divide them by the scale.
Last but not least we need to change the phase to get the origin perfectly aligned with the position of the ball. This will be between -1 and 1 depending on the direction and we’ll convert to between 0 and 2.
The final result, in one formula by inverting X and Y:
Now to use it, we just need to use the Y distance between the ball and the paddle and we’ll receive the X where the ball will end.
EDIT:
I was writing this answer while you changed your question. Since the ball’s X will not change direction when a paddle is hit, it is very easy to figure out no matter the number of paddle hits it will have. Basically, you just sum the total distance and pass that to the formula. So if the board is 100 long and the balls is 10 away from the player’s paddle, simple calculate for a distance of 110.
Here’s the graph I played with to see it in action.
https://www.desmos.com/calculator/zr3g9wumrn