Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
4241982b6This file is pinned to the selected release. Download the original to open it in your design tool.
"""
Based on: https://gist.github.com/xjcl/8ce64008710128f3a076
Modified by PedroLopes for openEMSstim but credit remains with author
HOW TO SETUP:
1. Connect the openEMSstim via USB to your laptop
2. Setup the serial_port variable (e.g., serial_port = "/dev/tty.wchusbserial1410")
3. Connect Player 1 to EMS Channel 1 and Player 2 to EMS Channel 2
4. Calibrate openEMSstim (read plopes.org/ems to learn how to safely execute this)
5. Start the python game: >python ems-pong.py
HOW TO PLAY:
Player 0 controls: UP (W) DOWN (S)
Player 1 controls: UP (O) DOWN (L).
Quit: (Q)
HOW TO INSTALL:
Requires: pyglet, openEMSstim python lib (openems)
and a 10x10 image in directory: "white_square.png"
"""
#native imports
import math
import random
import pyglet
import sys
#import openEMSstim modules
sys.path.append("../")
from pyEMS import openEMSstim
from pyEMS.EMSCommand import ems_command
serial_port = "/dev/tty.wchusbserial1410"
class Ball(object):
def __init__(self):
self.debug = 0
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
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
self.ems_device = openEMSstim.openEMSstim(serial_port,19200)
self.left_player_lost_stimulation = ems_command(1,100,2000)
self.right_player_lost_stimulation = ems_command(2,100,2000)
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)
self.ems_device.send(self.left_player_lost_stimulation)
elif b.x - b.TO_SIDE > self.WIDTH: # leave on right
self.reset_ball(0)
self.ems_device.send(self.right_player_lost_stimulation)
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()