Note
Go to the end to download the full example code
Classes for running simulations of the classic game Pong¶
Run this example as a Jupyter notebook:
See our guide for more information and troubleshooting.
The Class GameOfPong contains all necessary functionality for running simple simulations of Pong games. See [1] for details.
See Also¶
References¶
- Authors:
T Wunderlich, Electronic Vision(s), J Gille
import numpy as np
LEFT_SCORE = -1
RIGHT_SCORE = +1
GAME_CONTINUES = 0
MOVE_DOWN = -1
MOVE_UP = +1
DONT_MOVE = 0
class GameObject:
def __init__(self, game, x_pos=0.5, y_pos=0.5, velocity=0.2, direction=(0, 0)):
"""Base class for Ball and Paddle that contains basic functionality for
an object inside a game.
Args:
game (GameOfPong): Instance of Pong game.
x_pos (float, optional): Initial x position. Defaults to 0.5.
y_pos (float, optional): Initial y position. Defaults to 0.5.
velocity (float, optional): Change in position per iteration.
Defaults to 0.2.
direction (list, optional): direction vector. Defaults to [0,0].
"""
self.x_pos = x_pos
self.y_pos = y_pos
self.velocity = velocity
self.direction = direction
self.game = game
self.update_cell()
def get_cell(self):
return self.cell
def get_pos(self):
return self.x_pos, self.y_pos
def update_cell(self):
"""Updates the cell in the game grid based on position."""
x_cell = int(np.floor((self.x_pos / self.game.x_length) * self.game.x_grid))
y_cell = int(np.floor((self.y_pos / self.game.y_length) * self.game.y_grid))
self.cell = [x_cell, y_cell]
class Ball(GameObject):
"""Class representing the ball.
Args:
radius (float): Radius of ball in unit length.
For other args, see :class:`GameObject`.
"""
def __init__(self, game, x_pos=0.8, y_pos=0.5, velocity=0.025, direction=(-1 / 2.0, 1 / 2.0), radius=0.025):
super().__init__(game, x_pos, y_pos, velocity, direction)
self.ball_radius = radius # Unit length
self.update_cell()
class Paddle(GameObject):
"""Class representing the paddles on either end of the playing field.
Args:
direction (int, optional): Either -1, 0, or 1 for downward, neutral
or upwards motion, respectively. Defaults to 0.
left (boolean): If True, paddle is placed on the left side of the
board, otherwise on the right side.
For other args, see :class:`GameObject`.
"""
length = 0.2 # Paddle length in the scale of GameOfPong.y_length
def __init__(self, game, left, y_pos=0.5, velocity=0.05, direction=0):
x_pos = 0.0 if left else game.x_length
super().__init__(game, x_pos, y_pos, velocity, direction)
self.update_cell()
def move_up(self):
self.direction = MOVE_UP
def move_down(self):
self.direction = MOVE_DOWN
def dont_move(self):
self.direction = DONT_MOVE
class GameOfPong(object):
"""Class representing a game of Pong. Playing field is 1.6 by 1.0 units
in size, discretized into x_grid*y_grid cells.
"""
x_grid = 32
y_grid = 20
x_length = 1.6
y_length = 1.0
def __init__(self):
self.r_paddle = Paddle(self, False)
self.l_paddle = Paddle(self, True)
self.reset_ball()
self.result = 0
def reset_ball(self, towards_left=False):
"""Resets the ball position to the center of the field after a goal.
Args:
towards_left (bool, optional): if True, ball direction is
initialized towards the left side of the field, otherwise towards
the right. Defaults to False.
"""
initial_vx = 0.5 + 0.5 * np.random.random()
initial_vy = 1.0 - initial_vx
if towards_left:
initial_vx *= -1
initial_vy *= np.random.choice([-1.0, 1.0])
self.ball = Ball(self, direction=[initial_vx, initial_vy])
self.ball.y_pos = np.random.random() * self.y_length
def update_ball_direction(self):
"""In case of a collision, updates the direction of the ball. Also
determines if the ball is in either player's net.
Returns:
Either GAME_CONTINUES, LEFT_SCORE or RIGHT_SCORE depending on ball
and paddle position.
"""
if self.ball.y_pos + self.ball.ball_radius >= self.y_length:
# Ball on upper edge
self.ball.direction[1] = -1 * abs(self.ball.direction[1])
elif self.ball.y_pos - self.ball.ball_radius <= 0:
# Ball on lower edge
self.ball.direction[1] = abs(self.ball.direction[1])
if self.ball.x_pos - self.ball.ball_radius <= 0:
# Ball on left edge
if abs(self.l_paddle.y_pos - self.ball.y_pos) <= Paddle.length / 2:
# Ball hits left paddle
self.ball.direction[0] = abs(self.ball.direction[0])
else:
return RIGHT_SCORE
elif self.ball.x_pos + self.ball.ball_radius >= self.x_length:
# Ball on right edge
if abs(self.r_paddle.y_pos - self.ball.y_pos) <= Paddle.length / 2:
# Ball hits right paddle
self.ball.direction[0] = -1 * abs(self.ball.direction[0])
else:
return LEFT_SCORE
return GAME_CONTINUES
def propagate_ball_and_paddles(self):
"""Updates ball and paddle coordinates based on direction and velocity."""
for paddle in [self.r_paddle, self.l_paddle]:
paddle.y_pos += paddle.direction * paddle.velocity
paddle.y_pos = min(max(0, paddle.y_pos), self.y_length)
paddle.update_cell()
self.ball.y_pos += self.ball.velocity * self.ball.direction[1]
self.ball.x_pos += self.ball.velocity * self.ball.direction[0]
self.ball.update_cell()
def get_ball_cell(self):
return self.ball.get_cell()
def step(self):
"""Performs one game step by handling collisions, propagating all game
objects and returning the new game state.
Returns:
Either GAME_CONTINUES, LEFT_SCORE or RIGHT_SCORE depending on ball
and paddle position. see update_ball_direction()
"""
ball_status = self.update_ball_direction()
self.propagate_ball_and_paddles()
self.result = ball_status
return ball_status