Last active
June 8, 2021 12:50
-
-
Save xjcl/8ce64008710128f3a076 to your computer and use it in GitHub Desktop.
A simple pong game written in python and pyglet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Play a simple game of pong in a window. | |
Player 0 controls: (W, S) Player 1 controls: (O, L). Quit: (Q) | |
The ball bounces differently if you move while hitting it. | |
This also includes a slight factor of randomness. | |
Requires: pyglet, 10x10 image in directory: "white_square.png" | |
""" | |
# native libs | |
import math | |
import random | |
# non-native libs | |
import pyglet | |
class Ball(object): | |
def __init__(self): | |
self.debug = 0 | |
# x and y are the _center_ of the ball, not topleft corner | |
self.TO_SIDE = 5 | |
self.x = 50.0 + self.TO_SIDE | |
self.y = float( random.randint(0, 450) ) | |
self.x_old = self.x # coordinates in the last frame | |
self.y_old = self.y | |
self.vec_x = 2**0.5 / 2 # sqrt(2)/2 | |
self.vec_y = random.choice([-1, 1]) * 2**0.5 / 2 | |
#(assert self.vec_x * self.vex_y == 1.0) at all times | |
class Player(object): | |
def __init__(self, NUMBER, screen_WIDTH=800): | |
"""NUMBER must be 0 (left player) or 1 (right player).""" | |
self.NUMBER = NUMBER | |
self.x = 50.0 + (screen_WIDTH - 100) * NUMBER | |
self.y = 50.0 | |
self.last_movements = [0]*4 # short movement history | |
# used for bounce calculation | |
self.up_key, self.down_key = None, None | |
if NUMBER == 0: | |
self.up_key = pyglet.window.key.W | |
self.down_key = pyglet.window.key.S | |
elif NUMBER == 1: | |
self.up_key = pyglet.window.key.O | |
self.down_key = pyglet.window.key.L | |
class Model(object): | |
"""Model of the entire game. Has two players and one ball.""" | |
def __init__(self, DIMENSIONS=(800, 450)): | |
"""DIMENSIONS is a tuple (WIDTH, HEIGHT) of the field.""" | |
# OBJECTS | |
WIDTH = DIMENSIONS[0] | |
self.players = [Player(0, WIDTH), Player(1, WIDTH)] | |
self.ball = Ball() | |
# DATA | |
self.pressed_keys = set() # set has no duplicates | |
self.quit_key = pyglet.window.key.Q | |
self.speed = 6 # in pixels per frame | |
self.ball_speed = self.speed * 2.5 | |
self.WIDTH, self.HEIGHT = DIMENSIONS | |
# STATE VARS | |
self.paused = False | |
self.i = 0 # "frame count" for debug | |
def reset_ball(self, who_scored): | |
"""Place the ball anew on the loser's side.""" | |
if debug: print(str(who_scored)+" scored. reset.") | |
self.ball.y = float( random.randint(0, self.HEIGHT) ) | |
self.ball.vec_y = random.choice([-1, 1]) * 2**0.5 / 2 | |
if who_scored == 0: | |
self.ball.x = self.WIDTH - 50.0 - self.ball.TO_SIDE | |
self.ball.vec_x = - 2**0.5 / 2 | |
elif who_scored == 1: | |
self.ball.x = 50.0 + self.ball.TO_SIDE | |
self.ball.vec_x = + 2**0.5 / 2 | |
elif who_scored == "debug": | |
self.ball.x = 70 # in paddle atm -> usage: hold f | |
self.ball.y = self.ball.debug | |
self.ball.vec_x = -1 | |
self.ball.vec_y = 0 | |
self.ball.debug += 0.2 | |
if self.ball.debug > 100: | |
self.ball.debug = 0 | |
def check_if_oob_top_bottom(self): | |
"""Called by update_ball to recalc. a ball above/below the screen.""" | |
# bounces. if -- bounce on top of screen. elif -- bounce on bottom. | |
b = self.ball | |
if b.y - b.TO_SIDE < 0: | |
illegal_movement = 0 - (b.y - b.TO_SIDE) | |
b.y = 0 + b.TO_SIDE + illegal_movement | |
b.vec_y *= -1 | |
elif b.y + b.TO_SIDE > self.HEIGHT: | |
illegal_movement = self.HEIGHT - (b.y + b.TO_SIDE) | |
b.y = self.HEIGHT - b.TO_SIDE + illegal_movement | |
b.vec_y *= -1 | |
def check_if_oob_sides(self): | |
"""Called by update_ball to reset a ball left/right of the screen.""" | |
b = self.ball | |
if b.x + b.TO_SIDE < 0: # leave on left | |
self.reset_ball(1) | |
elif b.x - b.TO_SIDE > self.WIDTH: # leave on right | |
self.reset_ball(0) | |
def check_if_paddled(self): | |
"""Called by update_ball to recalc. a ball hit with a player paddle.""" | |
# TODO remove duplicate code? | |
# TODO also retrace the original line to see if it touches paddle | |
b = self.ball | |
p0, p1 = self.players[0], self.players[1] | |
angle = math.acos(b.vec_y) # vector mult: (0, 1) * (b.vec_x, b.vec_y) | |
factor = random.randint(5, 15) # low: strong player influence | |
# `cross` is true for player n if the ball was in front of their | |
# paddle the frame before and is behind them in this frame | |
cross0 = (b.x < p0.x + 2*b.TO_SIDE) and (b.x_old >= p0.x + 2*b.TO_SIDE) | |
cross1 = (b.x > p1.x - 2*b.TO_SIDE) and (b.x_old <= p1.x - 2*b.TO_SIDE) | |
if cross0 and -25 < b.y - p0.y < 25: | |
if debug: print("hit at "+str(self.i)) | |
illegal_movement = p0.x + 2*b.TO_SIDE - b.x | |
b.x = p0.x + 2*b.TO_SIDE + illegal_movement | |
angle -= sum(p0.last_movements) / factor / self.ball_speed | |
b.vec_y = math.cos(angle) | |
b.vec_x = (1**2 - b.vec_y**2) ** 0.5 | |
elif cross1 and -25 < b.y - p1.y < 25: | |
if debug: print("hit at "+str(self.i)) | |
illegal_movement = p1.x - 2*b.TO_SIDE - b.x | |
b.x = p1.x - 2*b.TO_SIDE + illegal_movement | |
angle -= sum(p1.last_movements) / factor / self.ball_speed | |
b.vec_y = math.cos(angle) | |
b.vec_x = - (1**2 - b.vec_y**2) ** 0.5 | |
def update_ball(self): | |
""" | |
Update ball position with post-collision detection. | |
I.e. Let the ball move out of bounds and calculate | |
where it should have been within bounds. | |
When bouncing off a paddle, take player velocity into | |
consideration as well. Add a small factor of random too. | |
""" | |
self.i += 1 # "debug" | |
b = self.ball | |
b.x_old, b.y_old = b.x, b.y | |
b.x += b.vec_x * self.ball_speed | |
b.y += b.vec_y * self.ball_speed | |
self.check_if_oob_top_bottom() # oob: out of bounds | |
self.check_if_oob_sides() | |
self.check_if_paddled() | |
def update(self): | |
"""Work through all pressed keys, update and call update_ball.""" | |
pks = self.pressed_keys | |
if self.quit_key in pks: | |
exit(0) | |
if pyglet.window.key.R in pks and debug: | |
self.reset_ball(1) | |
if pyglet.window.key.F in pks and debug: | |
self.reset_ball("debug") | |
for p in self.players: | |
p.last_movements.pop(0) | |
if p.up_key in pks and p.down_key not in pks: | |
p.y -= self.speed | |
p.last_movements.append(-self.speed) | |
elif p.up_key not in pks and p.down_key in pks: | |
p.y += self.speed | |
p.last_movements.append(+self.speed) | |
else: | |
# notice how we popped from _place_ zero, | |
# but append _a number_ zero here. it's not the same. | |
p.last_movements.append(0) | |
self.update_ball() | |
class Controller(object): | |
def __init__(self, model): | |
self.m = model | |
def on_key_press(self, symbol, modifiers): | |
# `a |= b`: mathematical or. add to set a if in set a or b. | |
# equivalent to `a = a | b`. | |
# XXX p0 holds down both keys => p1 controls break # PYGLET!? D: | |
self.m.pressed_keys |= set([symbol]) | |
def on_key_release(self, symbol, modifiers): | |
if symbol in self.m.pressed_keys: | |
self.m.pressed_keys.remove(symbol) | |
def update(self): | |
self.m.update() | |
class View(object): | |
def __init__(self, window, model): | |
self.w = window | |
self.m = model | |
# ------------------ IMAGES --------------------# | |
# "white_square.png" is a 10x10 white image | |
lplayer = pyglet.resource.image("white_square.png") | |
# TODO stack the same graphic together three times? | |
self.player_spr = pyglet.sprite.Sprite(lplayer) | |
def redraw(self): | |
# ------------------ PLAYERS --------------------# | |
TO_SIDE = self.m.ball.TO_SIDE | |
for p in self.m.players: | |
self.player_spr.x = p.x//1 - TO_SIDE | |
# oh god! pyglet's (0, 0) is bottom right! madness. | |
self.player_spr.y = self.w.height - (p.y//1 + TO_SIDE) | |
self.player_spr.draw() # these 3 lines: pretend-paddle | |
self.player_spr.y -= 2*TO_SIDE; self.player_spr.draw() | |
self.player_spr.y += 4*TO_SIDE; self.player_spr.draw() | |
# ------------------ BALL --------------------# | |
self.player_spr.x = self.m.ball.x//1 - TO_SIDE | |
self.player_spr.y = self.w.height - (self.m.ball.y//1 + TO_SIDE) | |
self.player_spr.draw() | |
# TODO draw scores (either as label or 3-by-5 pixel graphic) | |
class Window(pyglet.window.Window): | |
def __init__(self, *args, **kwargs): | |
DIM = (800, 450) # DIMENSIONS | |
super(Window, self).__init__(width=DIM[0], height=DIM[1], | |
*args, **kwargs) | |
# ------------------ MVC --------------------# | |
the_window = self | |
self.model = Model(DIM) | |
self.view = View(the_window, self.model) | |
self.controller = Controller(self.model) | |
# ------------------ CLOCK --------------------# | |
fps = 30.0 | |
pyglet.clock.schedule_interval(self.update, 1.0/fps) | |
pyglet.clock.set_fps_limit(fps) | |
def on_key_release(self, symbol, modifiers): | |
self.controller.on_key_release(symbol, modifiers) | |
def on_key_press(self, symbol, modifiers): | |
self.controller.on_key_press(symbol, modifiers) | |
def update(self, *args, **kwargs): | |
# XXX make more efficient (save last position, draw black square | |
# over that and the new square, don't redraw _entire_ frame.) | |
self.clear() | |
self.controller.update() | |
self.view.redraw() | |
def main(): | |
if debug: print("init window...") | |
window = Window() | |
if debug: print("done! init app...") | |
pyglet.app.run() | |
debug = 1 | |
if __name__ == "__main__": | |
main() |
Maybe you could try including a "constants" or "settings" module which includes all the constants.
You could also divide up the event handler classes into different modules, it might be easier to manage.
that code doesn't work :-(
@Leolelo2203 Did you do these 3 steps?:
- Install
pyglet
viapython-pip
- Provide a 10x10 png file called
white_square.png
in the same folder as the script - Comment out the line
pyglet.clock.set_fps_limit(fps)
(seems to be a removed feature)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
hello
how can i change the speed of the ball and players???