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:

  1. http://jsfiddle.net/ukva5/6/ more playable
  2. http://jsfiddle.net/ukva5/7/ random bricks, faster ball
  3. http://jsfiddle.net/ukva5/8/ random brick health
  4. 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);
}