#! /usr/bin/env python """ A simple puzzle game. Push pieces around to reassemble the image. The image comes from a file that you specify, which is divided in N times M blocks, shuffled around randomly. Usage: puzzle.py [-b] [imagefilename] [NxM] [PxQ] NxM: number of divisions, defaults to 2x3 PxQ: board size, defaults to two more than NxM -b : flag to forbid blocks from touching the border of the board. W 8 Keys: Q E or 7 9 A D 1 3 S 2 Backspace to cancel recent moves Escape to quit """ import pygame import pygame.event, pygame.key, pygame.transform from pygame.locals import * import random, time CHEAT_MOUSE_CLICK = False CHEAT_SOLUTION_DB = False #FULLSCREEN_RES = None FULLSCREEN_RES = (1024, 768) # hexagonal positions (example for h=3, w=3): # # 1,0 3,0 # 0,1 2,1 4,1 # 1,2 3,2 # 0,3 2,3 4,3 # 1,4 3,4 # __ __ # __/ \__/ \__ # / \__/ \__/ \ # \__/ \__/ \__/ # / \__/ \__/ \ # \__/ \__/ \__/ # \__/ \__/ def enumerate_positions(rect): if len(rect) == 2: x1 = y1 = 0 w, h = rect else: x1, y1, w, h = rect for y in range(y1*2, (y1+h)*2-1): for x in range(x1*2 + ((y&1)^1), (x1+w)*2-1, 2): yield (x, y) def in_bounds((x, y), w, h): assert (x^y) & 1 == 1 return 0 <= x < w*2-1 and 0 <= y < h*2-1 DIRS = [(1, -1), # NE (0, -2), # N (-1, -1), # NW (-1, 1), # SW (0, 2), # S (1, 1)] # SE KEY2DIR = { ord('e'): (1, -1), ord('w'): (0, -2), ord('q'): (-1, -1), ord('a'): (-1, 1), ord('s'): (0, 2), ord('d'): (1, 1), ord('9'): (1, -1), ord('8'): (0, -2), ord('7'): (-1, -1), ord('1'): (-1, 1), ord('2'): (0, 2), ord('3'): (1, 1), } def go((x, y), (dx, dy)): return (x+dx, y+dy) def square_layout_size(img_w, img_h, w, h): # ____ # / \ # / \ # \ / | cy # \____/ | # L __ # S S = L/2 # ______ # cx cx = L+S cx = (img_w*3) // (w*6-2) cy = img_h // (2*h) return cx, cy ystartlist = None ystoplist = None yNWlist = None ySWlist = None def init_tiles(cx, cy): global ystartlist, ystoplist, yNWlist, ySWlist if ystartlist is None: S = cx // 3 ystartlist = [(cy*(S-i-1)) // S for i in range(S)] ystoplist = [cy + (cy*i) // S for i in range(S)] yNWlist = [] ySWlist = [] for i in range(cy): f = float(S*(cy-i-1)) / cy yNWlist.append((i, int(f), f-int(f))) ySWlist.append((cy+cy-i-1, int(f)+1, f-int(f))) def extract_tile(img, (cx, cy), (x, y)): init_tiles(cx, cy) origx = cx * x origy = cy * y S = cx // 3 rx = cx + S ry = 2*cy result = pygame.Surface((rx, ry)) result.fill((0, 0, 0)) result.set_colorkey((0, 0, 0)) result = result.convert_alpha() result.blit(img, (S, 0), (S+origx, origy, cx-S, ry)) for i in range(S): ystart = ystartlist[i] ystop = ystoplist[i] result.blit(img, (i, ystart), (i + origx, ystart + origy, 1, ystop-ystart)) result.blit(img, (rx-i-1, ystart), (rx-i-1 + origx, ystart + origy, 1, ystop-ystart)) return result def draw_plain_tile(screen, (cx, cy), (screen_x, screen_y), color): init_tiles(cx, cy) S = cx // 3 rx = cx + S ry = 2*cy screen.fill(color, (screen_x+S, screen_y, cx-S, ry)) for i in range(S): ystart = ystartlist[i] ystop = ystoplist[i] screen.fill(color, (screen_x+i, screen_y+ystart, 1, ystop-ystart)) screen.fill(color, (screen_x+rx-i-1, screen_y+ystart, 1, ystop-ystart)) def draw_border(screen, (cx, cy), (screen_x, screen_y), (r, g, b), dirs=DIRS): S = cx // 3 rx = cx + S ry = 2*cy col1 = (r*5//4, g*5//4, b*5//4) col2 = (r*9//8, g*9//8, b*9//8) col25 = (r, g, b) col3 = (r//2, g//2, b//2) col4 = (r*3//4, g*3//4, b*3//4) col5 = (r//3, g//3, b//3) col6 = (0, 0, 0) def interm(f1, (r1, g1, b1), col2, maxcol=None): f2 = 1.0 - f1 r2 = col2[0] g2 = col2[1] b2 = col2[2] res = (r1*f1 + r2*f2, g1*f1 + g2*f2, b1*f1 + b2*f2) if maxcol is not None and res[0] > maxcol[0]: res = maxcol return res if (-1, -1) in dirs: # NW screen.lock() for j, i, s0 in yNWlist: maxcol = None if j < 3: if j == 2: maxcol = col1 if j == 1: maxcol = col2 if j == 0: maxcol = col3 try: rgb = screen.get_at((screen_x+i+2, screen_y+j)) screen.set_at((screen_x+i+2, screen_y+j), interm(s0, col1, rgb, maxcol)) except IndexError: pass screen.set_at((screen_x+i+1, screen_y+j), interm(s0, col2, col1, maxcol)) screen.set_at((screen_x+i+0, screen_y+j), interm(s0, col3, col2, maxcol)) screen.unlock() if (-1, 1) in dirs: # SW screen.lock() for j, i, s0 in ySWlist: try: rgb = screen.get_at((screen_x+i+2, screen_y+j)) screen.set_at((screen_x+i+2, screen_y+j), interm(s0, col2, rgb)) except IndexError: pass screen.set_at((screen_x+i+1, screen_y+j), interm(s0, col25, col2)) screen.set_at((screen_x+i+0, screen_y+j), interm(s0, col3, col25)) screen.unlock() if (1, -1) in dirs: # NE screen.lock() max_x = screen_x + rx - 1 for j, i, s0 in yNWlist: try: rgb = screen.get_at((max_x-i-2, screen_y+j)) screen.set_at((max_x-i-2, screen_y+j), interm(s0, col4, rgb)) except IndexError: pass screen.set_at((max_x-i-1, screen_y+j), interm(s0, col5, col4)) screen.set_at((max_x-i-0, screen_y+j), interm(s0, col6, col5)) screen.unlock() if (0, 2) in dirs: # S screen.fill(col4, (screen_x+S, screen_y+ry-2, cx-S, 1)) screen.fill(col5, (screen_x+S, screen_y+ry-1, cx-S, 1)) if (1, 1) in dirs: # SE screen.lock() y0 = cy max_x = screen_x + rx - 1 for j, i, s0 in ySWlist: try: rgb = screen.get_at((max_x-i-2, screen_y+j)) screen.set_at((max_x-i-2, screen_y+j), interm(s0, col3, rgb)) except IndexError: pass screen.set_at((max_x-i-1, screen_y+j), interm(s0, col5, col3)) screen.set_at((max_x-i-0, screen_y+j), interm(s0, col6, col5)) screen.unlock() if (0, -2) in dirs: # N screen.fill(col1, (screen_x+S, screen_y+2, cx-S, 1)) screen.fill(col2, (screen_x+S, screen_y+1, cx-S, 1)) screen.fill(col3, (screen_x+S, screen_y+0, cx-S, 1)) ## LINE_ENDS = { ## (1, -1): (cx+S-2, cy, cx-1, 1, col4), ## (0, -2): (cx-1, 1, S+1, 1, col1), ## (-1, -1): (S+1, 1, 2, cy, col1), ## (-1, 1): (2, cy, S+1, cy*2-1, col2), ## (0, 2): (S+1, cy*2-1, cx-1, cy*2-1, col3), ## (1, 1): (cx-1, cy*2-1, cx+S-2, cy, col3), ## } ## for dir in dirs: ## x1, y1, x2, y2, col = LINE_ENDS[dir] ## pygame.draw.line(screen, col, (screen_x + x1, screen_y + y1), ## (screen_x + x2, screen_y + y2), ## 3) # ____________________________________________________________ class Game: COLOR = {' ': (28, 0, 0), '@': (192, 0, 0), '#': (128, 128, 0), '*': (64, 64, 0), #'.': (28, 0, 0), } def __init__(self, bwidth, bheight, cw, ch): self.bwidth = bwidth self.bheight = bheight self.cw = cw self.ch = ch pygame.display.init() pygame.key.set_repeat(400, 30) if FULLSCREEN_RES: winstyle = FULLSCREEN width, height = FULLSCREEN_RES else: winstyle = 0 width = int((bwidth*2)*cw) height = int((bheight*2+0.6)*ch) self.screen = pygame.display.set_mode((width, height), winstyle) self.screen.fill((96, 96, 0)) self.img_tiles = {} targetcenterx = cw*bwidth - cw//3 targetcentery = ch*bheight self.deltax = targetcenterx - width // 2 self.deltay = targetcentery - height // 2 self.board = {} self.history = None ## xmin, ymin = self.screen2xy((0, 0)) ## xmax, ymax = self.screen2xy((width-1, height-1)) ## for y in range(ymin, ymax+1): ## for x in range(xmin, xmax+1): ## if not (0 <= x < bwidth and 0 <= y < bheight): ## self.set(x, y, '*') ## self.board.clear() self.regular_positions = {} for x, y in enumerate_positions((bwidth, bheight)): self.set(x, y, ' ') self.regular_positions[x, y] = True for x, y in enumerate_positions((-1, -1, bwidth+2, bheight+2)): if (x, y) not in self.regular_positions: self.set(x, y, '#') def screen2xy(self, (mx, my)): cw = self.cw ch = self.ch mx += self.deltax my += self.deltay return mx/cw, my/ch def xy2screen(self, (x, y)): cw = self.cw ch = self.ch x1 = x*cw - self.deltax y1 = y*ch - self.deltay return x1, y1 def set(self, x, y, piece, rec=True): assert (x^y) & 1 == 1 cw = self.cw ch = self.ch x1, y1 = self.xy2screen((x, y)) s = self.screen if self.history is not None and (x, y) not in self.history: self.history[x, y] = self.board[x, y] self.board[x, y] = piece ## border = 15 if isinstance(piece, tuple): color = 160, 80, 128 px, py = piece s.blit(self.img_tiles[px, py], (x1, y1)) ## if self.board[x, y-1] == (px, py-1): border &= ~1 ## if self.board[x-1, y] == (px-1, py): border &= ~2 ## if self.board[x, y+1] == (px, py+1): border &= ~4 ## if self.board[x+1, y] == (px+1, py): border &= ~8 borders = [] for dir in DIRS: nx, ny = go((x, y), dir) if self.board[nx, ny] == go((px, py), dir): if rec: self.set(nx, ny, self.board[nx, ny], rec=False) else: borders.append(dir) else: color = self.COLOR[piece] draw_plain_tile(s, (cw, ch), (x1, y1), color) borders = DIRS draw_border(s, (cw, ch), (x1, y1), color, borders) ## if piece == ' ': ## return ## gap = 2 * bool(border&8) ## if border&1: s.fill((r*5//4, g*5//4, b*5//4), (x1, y1, cw-gap, 2)) #top ## gap = 2 * bool(border&4) ## if border&2: s.fill((r*5//4, g*5//4, b*5//4), (x1, y1, 2, ch-gap)) #lft ## gap = 2 * bool(border&2) ## if border&4: s.fill((r//2, g//2, b//2), (x1+gap, y1+ch-2, cw-gap, 2))#bot ## gap = 2 * bool(border&1) ## if border&8: s.fill((r//2, g//2, b//2), (x1+cw-2, y1+gap, 2, ch-gap))#rgt ## if rec: ## do = [] ## if not (border&1): do.append((x,y-1)) ## if not (border&2): do.append((x-1,y)) ## if not (border&4): do.append((x,y+1)) ## if not (border&8): do.append((x+1,y)) ## for x, y in do: ## self.set(x, y, self.board[x, y], rec=False) def flip(self): pygame.display.flip() def insert_piece(self, piece): #, (piecesw, piecesh), no_border=False): #xstart = 1 + (piece[0] == piecesw-1 or no_border) #xstop = self.bwidth-1 - (piece[0] == 0 or no_border) #ystart = 1 + (piece[1] == piecesh-1 or no_border) #ystop = self.bheight-1 - (piece[1] == 0 or no_border) lst = self.regular_positions.keys() k = 0.0 while True: #nx = random.randrange(xstart, xstop) #ny = random.randrange(ystart, ystop) nx, ny = random.choice(lst) if self.board[nx, ny] != ' ': continue count = 0 for dx, dy in DIRS: count += self.board[nx+dx, ny+dy] != ' ' if count <= k: break k += 0.02 self.set(nx, ny, piece) def bigpiece(self, x0, y0): result = {} if not isinstance(self.board[x0, y0], tuple): return result px0, py0 = self.board[x0, y0] pending = [(x0, y0)] for x, y in pending: if (x, y) in result: continue if self.board[x, y] != (px0 + x-x0, py0 + y-y0): continue result[x, y] = True for dx, dy in DIRS: pending.append((x+dx, y+dy)) return result def finished(self, nb_pieces): for x, y in self.board.keys(): bp = self.bigpiece(x, y) if len(bp) == nb_pieces: x, y = min(bp) return x, y-1 if bp: break return None def nextevent(): e = pygame.event.wait() if e.type == QUIT or (e.type == KEYDOWN and e.key == K_ESCAPE): raise SystemExit return e def puzzle((piecesw, piecesh), (boardw, boardh), img, largeimg, bumpyborder=False): assert not bumpyborder # XXX reimplement? img_w, img_h = img.get_size() cw, ch = square_layout_size(img_w, img_h, piecesw, piecesh) game = Game(boardw, boardh, cw, ch) lst = list(enumerate_positions((piecesw, piecesh))) for x, y in lst: game.img_tiles[x, y] = extract_tile(img, (cw, ch), (x, y)) px, py = 0, 1 game.set(px, py, '@') nb_pieces = len(lst) #pieces_max = (piecesw, piecesh) try: preset = PRESET except NameError: random.shuffle(lst) for piece in lst: game.insert_piece(piece) #, pieces_max, bumpyborder) print game.board else: for (x, y), piece in PRESET.items(): game.set(x, y, piece) if boardw * boardh <= (piecesw+2) * (piecesh+2): bumpyborder = False if bumpyborder: for x in range(1, game.bwidth-1): if x > 1: game.set(x, 1, '.') game.set(x, game.bheight-2, '.') for y in range(2, game.bheight-2): game.set(1, y, '.') game.set(game.bwidth-2, y, '.') undolist = [] game.history = {} lastundolistlen = None while not game.finished(nb_pieces): game.flip() if CHEAT_SOLUTION_DB and lastundolistlen != len(undolist): prevcolor = game.COLOR['@'] game.COLOR = game.__class__.COLOR.copy() if len(undolist) > 0: import db if not db.exists(game.board): title = "Impossible" game.COLOR['@'] = (80, 0, 0) lastundolistlen = len(undolist) if prevcolor != game.COLOR['@']: game.set(px, py, '@') game.flip() e = nextevent() if e.type == KEYDOWN: if e.key in KEY2DIR: dx, dy = KEY2DIR[e.key] if bumpyborder and (px == 1 or px == game.bwidth-2 or py == 1 or py == game.bheight-2): game.set(px, py, '.') else: game.set(px, py, ' ') if game.board[px+dx, py+dy] not in (' ', '.'): blocked = [(px, py)] bp = {} while bp is not None and blocked: for bx, by in blocked: if not isinstance(game.board[bx+dx, by+dy], tuple): bp = None break bp.update(game.bigpiece(bx+dx, by+dy)) else: del blocked[:] for x, y in bp: if (x+dx, y+dy) not in bp: if game.board[x+dx, y+dy] != ' ': blocked.append((x, y)) if bp is not None: undolist.append((px, py, game.history)) game.history = {} for x, y in bp.keys(): bp[x, y] = game.board[x, y] game.set(x, y, ' ') for (x, y), piece in bp.items(): game.set(x+dx, y+dy, piece) if game.board[px+dx, py+dy] in (' ', '.'): px += dx py += dy game.set(px, py, '@') elif e.key == K_BACKSPACE and undolist: bp = game.history.items() for (x, y), piece in bp: game.set(x, y, ' ') for (x, y), piece in bp: game.set(x, y, piece) px, py, game.history = undolist.pop() game.set(px, py, '@') elif e.type == MOUSEBUTTONDOWN and CHEAT_MOUSE_CLICK: try: bp = game.bigpiece(*game.screen2xy(e.pos)) except KeyError: bp = None if bp: undolist.append((px, py, game.history)) game.history = {} bp = list(bp) random.shuffle(bp) for x, y in bp: piece = game.board[x, y] game.set(x, y, '#') game.insert_piece(piece) #, pieces_max) game.set(x, y, ' ') ending(game, img, largeimg, game.finished(nb_pieces)) def ending(game, smallimg, largeimg, origin): # ending animation largeimg = largeimg.convert() starttime = time.time() pygame.display.flip() x1, y1 = game.xy2screen(origin) elapsed = 0.0 smallimgw, smallimgh = smallimg.get_size() largeimgw, largeimgh = largeimg.get_size() factor0 = float(smallimgw+smallimgh)/(largeimgw+largeimgh) targetx, targety = game.screen.get_size() targetx = (targetx - largeimgw) // 2 targety = (targety - largeimgh) // 2 while elapsed < 1.0: elapsed = min(1.0, time.time() - starttime) factor = factor0 + (1.0-factor0) * elapsed nimage = pygame.transform.scale(largeimg, (int(factor*largeimgw), int(factor*largeimgh))) for (x, y), value in game.board.items(): if isinstance(value, tuple): value = ' ' game.set(x, y, value) game.screen.blit(nimage, (int(elapsed*targetx + (1.0-elapsed)*x1), int(elapsed*targety + (1.0-elapsed)*y1))) pygame.display.flip() while True: nextevent() # ____________________________________________________________ def loadfile(filename, rescaled=None): if rescaled: import os, tempfile tmpname = tempfile.mktemp('.bmp') try: rw, rh = rescaled os.system("convert '%s' -geometry %dx%d '%s'" % ( filename, int(rw), int(rh), tmpname)) try: f = open(tmpname, 'rb') except (IOError, OSError): pass else: try: return pygame.image.load(f) finally: f.close() finally: try: os.unlink(tmpname) except OSError: pass f = open(filename, 'rb') try: s = pygame.image.load(f) finally: f.close() if rescaled: rw, rh = rescaled sw, sh = s.get_size() ratio = min(float(rw)/sw, float(rh)/sh) rw = int(sw * ratio) rh = int(sh * ratio) s = pygame.transform.scale(s, (rw, rh)) return s def main(argv): import sys, getopt opts, args = getopt.getopt(argv, "bh", ["help"]) bumpyborder = ('-b', '') in opts if bumpyborder: opts.remove(('-b', '')) if not (1 <= len(args) <= 3): print >> sys.stderr, __doc__ sys.exit(2) filename = args[0] if len(args) <= 1: pw, ph = 2, 3 else: pw, ph = args[1].split('x') pw, ph = int(pw), int(ph) if len(args) <= 2: bw, bh = pw+1, ph+2 else: bw, bh = args[2].split('x') bw, bh = int(bw), int(bh) largeimg = loadfile(filename) tw, th = largeimg.get_size() if FULLSCREEN_RES: rw, rh = tw, th rescale = 1.0 wmax = rw / (pw+pw-2.0/3) * (bw*2-0.4) rescale = min(rescale, FULLSCREEN_RES[0] / wmax) hmax = rh / float(ph) * (bh+0.2) rescale = min(rescale, FULLSCREEN_RES[1] / hmax) rw *= rescale rh *= rescale else: f = min(float(pw*6-2) / (bw*6), (2*ph) / (bh*2+0.6)) rw = tw * f rh = th * f rw = int(rw) rh = int(rh) smallimg = loadfile(filename, (rw, rh)) return (pw, ph), (bw, bh), smallimg, largeimg, bumpyborder if __name__ == '__main__': import sys puzzle(*main(sys.argv[1:]))