Skip to content

Instantly share code, notes, and snippets.

@ZhanruiLiang
Last active December 13, 2015 18:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ZhanruiLiang/4958754 to your computer and use it in GitHub Desktop.
Save ZhanruiLiang/4958754 to your computer and use it in GitHub Desktop.
break out game. Written in python 3 with Pygame and PIL.
import pygame as pg
import bz2
import itertools
import Image
import ImageDraw
import pickle
from random import randint
from math import pi,sin,cos,sqrt,atan2
norm = lambda c:c/abs(c)
times = lambda c,d:(c*d.conjugate()).real
cross = lambda c,d:(c*d.conjugate()).imag
class Poly(pg.sprite.Sprite):
def __init__(self, ps, c, color=0):
assert isinstance(c, complex)
for p in ps: assert isinstance(p, complex)
self.c = c
self.ps = ps
self.boundR = max(abs(p) for p in ps)
self.ns = [norm(ps[i]-ps[i-1])*1j for i in range(len(ps))]
self.color = color
W = abs(phy2scr(self.boundR+0j)[0]-phy2scr(0+0j)[0]) * 2
self.rect = pg.Rect(0, 0, W, W)
self.image = pg.Surface(self.rect.size).convert_alpha()
self._drawn = False
def update(self, *args):
x, y = phy2scr(self.c)
if not self._drawn:
self.image.fill((0, 0, 0, 0))
ps = []
for p in self.ps:
x1, y1 = phyvec2scr(p)
ps.append((self.rect.width/2+x1, self.rect.height/2+y1))
# pg.draw.aalines(self.image, self.color, True, ps, 1)
pg.draw.polygon(self.image, self.color, ps)
self._drawn = True
self.rect.x = x - self.rect.width / 2
self.rect.y = y - self.rect.height / 2
class Ball(Poly):
def __init__(self, r, c, v):
n = int(1.5*r)
d = 2*pi/n
ps = [r * (cos(i*d)+sin(i*d)*1j) for i in range(n)]
super().__init__(ps, c)
self.v = v
self.r = r
self.color = ColorBall
class Player(Poly):
def __init__(self, ps, c, color=0):
super().__init__(ps, c, color)
self.v = 0+0j
class SpeedBar(pg.sprite.Sprite):
BarColor = pg.Color(0, 0x5f, 0, 0xff)
TextColor = pg.Color(0xff, 0xff, 0xff, 0xff)
BgColor = pg.Color(0x5f, 0xff, 0x5f, 0x88)
BarWidth = 90
def __init__(self, thick=20):
# speed bar will be put on top side of screen
self.rect = pg.Rect(0, 0, W, thick)
self.image = pg.Surface(self.rect.size).convert_alpha()
self.barPos = 0
self.font = font = pg.font.SysFont('', int(thick * 1.5), True)
def update_speed(self, speed):
self.speed = speed
k = (speed - MinSpeed)/float(MaxSpeed - MinSpeed)
self.barPos = int((self.rect.width - self.BarWidth) * k)
def update(self, *args):
img = self.image
img.fill(self.BgColor)
pg.draw.rect(img, self.BarColor, (self.barPos, 0, self.BarWidth, self.rect.height))
text = self.font.render(str('{0:.2f}X'.format(self.speed)), 1, self.TextColor)
img.blit(text, (5+self.barPos, 2))
def rect(w, h):
w2, h2 = w/2, h/2
return [w2+h2*1j, -w2+h2*1j, -w2-h2*1j, w2-h2*1j]
pg.display.init()
pg.font.init()
W, H = 900, 700
ColorBG = pg.Color(0xffffffff)
ColorBall = pg.Color(0x615ea6ff)
ColorBrick = pg.Color(0x555566ff)
FPS = 40
BallR, BallV = 10, 0+440j
MinSpeed, MaxSpeed = 0.05, 2 * BallR * FPS / abs(BallV)
print('maxspeed:', MaxSpeed)
PlayerV = 500
DEBUG = 0
PROFILE = 1
Bc, Bi, Bj = W/2+H*1j, 1+0j, -1j
scr = pg.display.set_mode((W, H), 0, 32)
def phy2scr(p):
p = Bc + p.real * Bi + p.imag * Bj
return round(p.real), round(p.imag)
def phyvec2scr(v):
p = v.real * Bi + v.imag * Bj
return round(p.real), round(p.imag)
def hittest(dt, b, plr, Ps)->'(dt, P) or None':
t0, t1, t2 = 0, dt, dt
moveon(dt, b, plr)
if not existhit(b, Ps): return None
while t1 - t0 > 1e-2:
t3 = (t0 + t1)/2
moveon(t3 - t2, b, plr)
if existhit(b, Ps): t1 = t3
else: t0 = t3
t2 = t3
moveon(t1 - t2, b, plr)
P = next(P for P in Ps if collide(b, P))
moveon(t0 - t1, b, plr)
assert not existhit(b, Ps)
return (t1, P)
def existhit(b, Ps)->'bool':
return any(collide(b, P) for P in Ps)
def inside(p, P)->'bool':
return all(times(p-q-P.c, n)>=0 for q,n in zip(P.ps,P.ns))
def collide(P1, P2)->'bool':
if abs(P1.c - P2.c) > P1.boundR + P2.boundR + 1:
return False
return any(inside(P1.c + p, P2) for p in P1.ps) \
or any(inside(P2.c + p, P1) for p in P2.ps)
def moveon(dt, *ps):
for p in ps:
p.c += p.v * dt
def hithandle(b, P):
hp, n = hitwhere(b, P)
if DEBUG: pg.draw.line(debugSur, pg.Color(0xff0000ff), phy2scr(hp), phy2scr(hp+n*50), 2)
b.v -= 2 * n * times(b.v, n)
def hitwhere(b, P)->'(hitpoint, norm)':
c = P.c
for p in P.ps:
if abs(b.c - p - c) < b.r + 1e-1:
return (p+c, norm(b.c - p - c))
minD = 100
for p, n in zip(P.ps, P.ns):
d = abs(times(b.c - p - c, -n) - b.r)
if d < minD:
minD, minN = d, n
n = minN
return (b.c + n * b.r, -n)
def draw_norms(P):
if not isinstance(P, Ball):
for p, n in zip(P.ps, P.ns):
pg.draw.line(debugSur, (0, 0xff, 0), phy2scr(P.c + p), phy2scr(P.c + p + n*20), 2)
def flood_fill(img, bgcolor):
dat = img.load()
w, h = img.size
mark = set()
blocks = []
for x0 in range(w):
for y0 in range(h):
if (x0, y0) in mark: continue
color = dat[x0, y0]
if color == bgcolor: continue
mark.add((x0, y0))
stk = [(x0, y0)]
block = []
while stk:
x, y = stk.pop()
for p1 in ((x-1,y),(x,y-1),(x+1,y), (x,y+1)):
x1, y1 = p1
if x1 < 0 or x1 >= w or y1 < 0 or y1 >= h: continue
if dat[p1] == color and p1 not in mark:
mark.add(p1)
block.append(p1)
stk.append(p1)
block1 = []
vis = set(block)
for x, y in block:
neig = sum(1 for p1 in ((x-1,y),(x,y-1),(x+1,y), (x,y+1)) if p1 in vis)
if neig < 4: block1.append((x, y))
if len(block1) >= 4: blocks.append((dat[x0, y0], block1))
return blocks
def place_ball(b, plr):
c = plr.c
hl, hr = 0+0j, 0+300j
# assume:
# when b.c = c + hl, the ball overlaps player
# when b.c = c + hr, the ball do not overlaps player
while abs(hr - hl) > 1:
hm = (hl + hr) / 2
b.c = c + hm
if collide(b, plr): hl = hm
else: hr = hm
b.c = c + hr
def pixels2convex(pixels):
"""
convert a list of pixels into a convex polygon using Gramham Scan.
"""
c = pixels[0]
for p in pixels:
if c[1] > p[1]:
c = p
ts = [(atan2(p[1]-c[1], p[0]-c[0]), p) for p in pixels]
ts.sort()
stk = []
for x, y in ts:
while len(stk) >= 2:
y2, y1 = complex(*stk[-1]), complex(*stk[-2])
if cross(y1 - y2, complex(*y) - y1) > 0:
break
stk.pop()
stk.append(y)
if len(stk) < 3: return None
stk.reverse()
return stk
def img2data(path) -> "dumps((ball, player, brckis, walls))":
"""
Extract polygons from the image in path.
The lowest(with largest y value in image) polygon will be
the player.
"""
ColorWall = (0, 0, 0)
ColorBG = (255, 255, 255)
img = Image.open(path)
w, h = img.size
blocks = flood_fill(img, ColorBG)
brckis = []
walls = []
player = None
def convert(x, y):
return x * W / float(w) - W/2 + (H - y * H / float(h))*1j
for color, block in blocks:
conv = []
conv = pixels2convex(block)
if conv is None: continue
conv = [convert(x, y) for x, y in conv]
center = sum(conv) / len(conv)
p = Poly([c-center for c in conv], center, color)
if color == ColorWall:
walls.append(p)
else:
brckis.append(p)
if player is None or player.c.imag > center.imag:
player = p
ball = Ball(BallR, player.c, BallV)
print('Parsed image:\n {0} polygons,\n {1} vertices.'.format(
len(walls) + len(brckis),
sum(len(P.ps) for P in itertools.chain(brckis, walls))))
print('Ball: {0} vertices, radius={1}'.format(len(ball.ps), ball.r))
brckis.remove(player)
player = Player(player.ps, player.c, player.color)
place_ball(ball, player)
return ball, player, brckis, walls
def play_img(path):
data = img2data(path)
if PROFILE:
import cProfile
cProfile.runctx("play(data)", globals(), locals(), 'prof.out')
else:
play(data)
def play(data):
global debugSur
scr.fill(0)
if DEBUG:
debugSur = scr.copy().convert_alpha()
debugSur.fill((0, 0, 0, 0))
sur1 = scr.copy()
quit = False
tm = pg.time.Clock()
ball, player, brckis, walls = data
thick = 20
polys = walls + brckis + [player]
inputD = None
speed = 1
life = 3
playerInitPos = player.c
# init sprites
speedBar = SpeedBar()
while not quit:
for e in pg.event.get():
if e.type == pg.QUIT or e.type == pg.KEYDOWN and e.key == pg.K_q:
quit = True
keys = pg.key.get_pressed()
# control directions of player
if keys[pg.K_LEFT]: inputD = -1
elif keys[pg.K_RIGHT]: inputD = 1
else: inputD = 0
if inputD is not None:
player.v = PlayerV * inputD + 0j
# control time scale
if keys[pg.K_UP]: speed = min(MaxSpeed, speed + 0.04)
elif keys[pg.K_DOWN]: speed = max(MinSpeed, speed - 0.04)
speedBar.update_speed(speed)
dt = 1. / FPS * speed
while dt > 0:
r = hittest(dt, ball, player, polys)
if not r: break
ddt, P = r
dt -= ddt
hithandle(ball, P)
if P in brckis:
polys.remove(P)
brckis.remove(P)
# constraint player movement
#TODO
# test game over
if ball.c.imag < 0:
if life == 0:
print('game over')
quit = True
life -= 1
player.c = playerInitPos
ball.v = BallV
place_ball(ball, player)
# test win
if not brckis: print('you win');quit = True
# render
scr.fill(ColorBG)
for P in itertools.chain(walls, brckis, [player, ball]):
P.update()
scr.blit(P.image, P.rect)
speedBar.update()
scr.blit(speedBar.image, speedBar.rect)
if DEBUG:
scr.blit(debugSur, (0, 0))
pg.display.flip()
tm.tick(FPS)
pg.quit()
def test1(path):
" test flood_fill "
blocks = flood_fill(Image.open(path), bgcolor=(255,255,255))
print(blocks)
def test2(path):
"test pixels2convex, pixel data comes from floodfill"
src = Image.open(path)
blocks = flood_fill(src, bgcolor=(255,255,255))
ps = [(c, pixels2convex(p)) for c, p in blocks]
print(ps)
img = Image.new("RGB", src.size)
draw = ImageDraw.Draw(img)
draw.rectangle((0, 0, img.size[0], img.size[1]), fill=(255, 255, 255))
for c, p in ps:
draw.polygon(p, outline=c)
for c, p in ps:
for q in p:
draw.point(q, fill=(255, 0, 0))
img.save('test2.png')
def test3(path):
" test img2data, assume flood_fill and pixels2convex is tested."
ball, player, brckis, walls = img2data(path)
scr = pg.display.set_mode((W, H), 0, 32)
scr.fill(ColorBG)
for p in itertools.chain(walls, brckis, [player, ball]):
draw(scr, p)
pg.display.flip()
while 1:
for e in pg.event.get():
if e.type == pg.QUIT:
return
def test4():
" test place_ball "
ball = Ball(10, 0+0j, 0+0j)
player = Player(rect(100, 20), 10+10j)
place_ball(ball, player)
print(ball.c)
def test5():
" test inside "
datas = [(0+0j, False), (1+0j, True), (2+0j, True), (3+0j, True),
(0+1j, False), (1+1j, False), (2+1j, True), (3+1j, False),
(0+2j, False), (1+2j, False), (2+2j, True), (3+2j, False),
(4+0j, False)]
P = Poly([-1+0j, 1+0j, 0+4j], 2+0j)
for p, ans in datas:
got = bool(inside(p, P))
print(p, 'ans:', ans, 'got:', got)
assert ans == got
if __name__ == '__main__':
import sys
# test4(); exit(0)
# test5(); exit(0)
if '-g' in sys.argv:
print(img2data(sys.argv[2]))
elif '-i' in sys.argv:
play_img(sys.argv[2])
else:
play(open(sys.argv[1]).read())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment