Breakout in half an hour
Python 3 with Pygame and PIL / many hours
After playing other answers, that has simple level config, an idea came to me: The level can be initialized be an image.
So I make this one. To play it, you draw an image that contains many convex polygons, like this: image example http://imgbin.org/images/11634.png
The black color will be parsed as walls and the lowest block will be the player.
Save it as break-windows.png
and then run with command:
python3 brcki.py break-windows.png
Another feature is that the collision detection is very precise. I use binary-search to do it. When two objects collide, time goes back until nothing collide so that the exact collide time and point will be detected. With this tecnique, the ball can go very fast while the bounce effect is still realistic.
Here is brcki.py
. I know it's a bit too long to be a golf. But isn't that insteresting playing on your own image?
import pygame as pg
import itertools
import Image
from random import randint
from math import pi,sin,cos,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:
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
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
pg.display.init()
W, H = 600, 700
ColorBG = pg.Color(0xffffffff)
ColorBall = pg.Color(0x615ea6ff)
ColorBrick = pg.Color(0x555566ff)
FPS = 40
BallR, BallV = 15, 120+640j
PlayerV = 300
Bc, Bi, Bj = W/2+H*1j, 1+0j, -1j
def phy2scr(p):
p = Bc + p.real * Bi + p.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)
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(sur, P):
pg.draw.polygon(sur, P.color, [phy2scr(p + P.c) for p in P.ps])
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) -> "(ball, player, brckis, walls)":
"""
Extract polygons from the image in path.
The lowest(with largest y value in image) polygon will be
the player. All polygon will be converted into convex.
"""
ColorWall = (0, 0, 0)
ColorBG = (255, 255, 255)
print('Start parsing image...')
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(config):
scr = pg.display.set_mode((W, H), 0, 32)
quit = False
tm = pg.time.Clock()
ball, player, brckis, walls = config
polys = walls + brckis + [player]
inputD = None
while not quit:
dt = 1. / FPS
for e in pg.event.get():
if e.type == pg.KEYDOWN:
inputD = {pg.K_LEFT:-1, pg.K_RIGHT:1}.get(e.key, inputD)
elif e.type == pg.KEYUP:
inputD = 0
elif e.type == pg.QUIT:
quit = True
if inputD is not None:
player.v = PlayerV * inputD + 0j
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)
if ball.c.imag < 0: print('game over');quit = True
if not brckis: print('you win');quit = True
scr.fill(ColorBG)
for p in itertools.chain(walls, brckis, [player, ball]):
draw(scr, p)
pg.display.flip()
tm.tick(FPS)
if __name__ == '__main__':
import sys
play(img2data(sys.argv[1]))
Javascript/KineticJS
Here's a "working" version in 28 minutes, but it's not exactly playable (the collision logic is too slow). http://jsfiddle.net/ukva5/5/
(function (con, wid, hei, brk) {
var stage = new Kinetic.Stage({
container: con,
width: wid,
height: hei
}),
bricks = new Kinetic.Layer(),
brks = brk.bricks,
bX = wid / 2 - brk.width * brks[0].length / 2,
bY = brk.height,
mov = new Kinetic.Layer(),
ball = new Kinetic.Circle({
x: wid / 4,
y: hei / 2,
radius: 10,
fill: 'black'
}),
paddle = new Kinetic.Rect({
x:wid/2-30,
y:hei*9/10,
width:60,
height:10,
fill:'black'
}),
left = false,
right = false;
mov.add(ball);
mov.add(paddle);
stage.add(mov);
paddle.velocity = 5;
ball.angle = Math.PI/4;
ball.velocity = 3;
ball.bottom = function(){
var x = ball.getX();
var y = ball.getY();
return {x:x, y:y+ball.getRadius()};
};
ball.top = function(){
var x = ball.getX();
var y = ball.getY();
return {x:x, y:y-ball.getRadius()};
};
ball.left = function(){
var x = ball.getX();
var y = ball.getY();
return {x:x-ball.getRadius(), y:y};
};
ball.right = function(){
var x = ball.getX();
var y = ball.getY();
return {x:x+ball.getRadius(), y:y};
};
ball.update = function(){
ball.setX(ball.getX() + Math.cos(ball.angle)*ball.velocity);
ball.setY(ball.getY() + Math.sin(ball.angle)*ball.velocity);
};
paddle.update = function(){
var x = paddle.getX();
if (left) x-=paddle.velocity;
if (right) x+= paddle.velocity;
paddle.setX(x);
};
for (var i = 0; i < brks.length; i++) {
for (var j = 0; j < brks[i].length; j++) {
if (brks[i][j]) {
bricks.add(new Kinetic.Rect({
x: bX + j * brk.width + .5,
y: bY + i * brk.height + .5,
width: brk.width,
height: brk.height,
stroke: 'black',
strokeWidth: 1,
fill: 'gold'
}));
}
}
}
stage.add(bricks);
$(window).keydown(function(e){
switch(e.keyCode){
case 37:
left = true;
break;
case 39:
right = true;
break;
}
}).keyup(function(e){
switch(e.keyCode){
case 37:
left = false;
break;
case 39:
right = false;
break;
}
});
(function loop(){
ball.update();
paddle.update();
if (paddle.intersects(ball.bottom())){
ball.setY(paddle.getY()-ball.getRadius());
ball.angle = -ball.angle;
}
if (ball.right().x > wid){
ball.setX(wid - ball.getRadius());
ball.angle = Math.PI - ball.angle;
}
if (ball.left().x < 0){
ball.setX(ball.getRadius());
ball.angle = Math.PI - ball.angle;
}
if (ball.top().y < 0){
ball.setY(ball.getRadius());
ball.angle = -ball.angle;
}
for(var i = bricks.children.length; i--;){
var b = bricks.children[i];
if (b.intersects(ball.top()) || b.intersects(ball.bottom())){
ball.angle = -ball.angle;
b.destroy();
}
else if (b.intersects(ball.left()) || b.intersects(ball.right())){
ball.angle = Math.PI-ball.angle;
b.destroy();
}
}
stage.draw();
webkitRequestAnimationFrame(loop);
})()
})('b', 640, 480, {
width: 80,
height: 20,
bricks: [
[1, 1, 1, 1, 1, 1],
[1, 1, 0, 0, 1, 1],
[1, 1, 1, 1, 1, 1]
]
});
I'll work on making the game snappier now. :)
Note: Works only in webkit browsers right now. I'll add a shim so it will work in any HTML5 ready browser eventually.
Updates:
- http://jsfiddle.net/ukva5/6/ more playable
- http://jsfiddle.net/ukva5/7/ random bricks, faster ball
- http://jsfiddle.net/ukva5/8/ random brick health
- http://jsfiddle.net/ukva5/12/show/light/ stopping point for the day.
Processing, 400 characters
Didn't manage to do it in 30min, but here's a surf'd version. The ball is square and if you press any other key than left/right it jumps fully right, but it was shorter and the spec didn't say anything against it ;).
int x,y=999,u,v,p=300,q=580,i,l,t;long g;void setup(){size(720,600);}void draw(){background(0);if(keyPressed)p=min(max(p+8*(keyCode&2)-8,0),630);rect(p,q,90,20);for(i=0;i<64;i++)if((g>>i&1)<1){l=i%8*90;t=i/8*20+40;if(x+20>l&&x<l+90&&y+20>t&&y<t+20){v=-v;g|=1l<<i;}rect(l,t,90,20);}if(x+20>p&&x<p+90&&y+20>q)v=-abs(v);if(y<0)v=-v;if(x<0||x>700)u=-u;if(y>600){x=y=400;u=v=3;}x+=u;y+=v;rect(x,y,20,20);}
With proper names & some whitespace for clarity, that's:
int ballX, ballY=999, speedX, speedY, paddleX=300, paddleY=580;
long bricks;
void setup() {
size(720, 600);
}
void draw() {
background(0);
if(keyPressed) paddleX = min(max(paddleX+8*(keyCode^37)-8,0),630);
rect(paddleX, paddleY, 90, 20);
for(int i=0; i<64; i++) {
if((bricks>>i&1)<1) {
int brickX=i%8*90, brickY=i/8*20+40;
if(ballX+20>brickX && ballX<brickX+90 && ballY+20>brickY && ballY<brickY+20) {
speedY = -speedY;
bricks |= 1l<<i;
}
rect(brickX, brickY, 90, 20);
}
}
if(ballX+20>paddleX && ballX<paddleX+90 && ballY+20>paddleY) speedY = -abs(speedY);
if(ballY<0) speedY = -speedY;
if(ballX<0 || ballX>700) speedX = -speedX;
if(ballY>600) {
ballX = ballY = 400;
speedX = speedY = 3;
}
ballX += speedX; ballY += speedY;
rect(ballX, ballY, 20, 20);
}