Last active
December 13, 2015 18:58
-
-
Save ZhanruiLiang/4958754 to your computer and use it in GitHub Desktop.
break out game. Written in python 3 with Pygame and PIL.
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
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