Sunday, February 22, 2015

15 Puzzle (Python / Pygame)

In the 15 Puzzle you are given a permutation of 15 numbers in 16 slots. The puzzle is solved when the numbers are put in order with an empty spot in the 16th. Some papers I read generalized this to be "The (n**2 - 1) Puzzle." This code allows for that, though some resizing of globals may be in order to ensure that the game fits on the screen.

Not every permutation is solvable. The check in Board.permute()is found on WolframAlpha's page for the 15 puzzle.The idea is: For each tile, read left-to-right and top-to-bottom, you count the number of tiles smaller than it. Then you add the row the blank is on. If this number is even, we have a solvable permutation.

My last addition allows Multi-Tile Moves (MTM), which means that you can move more than one tile with one click if they are in a straight line.

Here is an example for n = 4. I have found a way to center the numbers in their tiles since Memory






A brief example of the WolframAlpha method:
  • 12 has no numbers before it, so we add 0
  • 1 has no numbers before it that are less than 1, so we add 0
  • 2 has 1 before it, so we add 1
  • 7 has 1 and 2 before it, so we add 2. Our tally is 3
  • ...
  • 13 has the numbers 1-12 before it, so we add 12 to our tally
  • The blank is on the fourth row, so we add 4 to our tally
  • The tally must be divisible by 2, because Python chose this as a valid permutation
As a demonstration of the MTM, note that the 13 block in the following image was clicked to move both the 9 and the 13 to the left. The `Click` tally is therefore 1 instead of 2, because only one tile was clicked even though two were moved.



I solved the above in 58 clicks using a method similar to the one shown on Wikihow. I would like to make the Game Won screen a little less anticlimactic: At the moment it looks like a regular screen. The user is expected to press Restart if they want to play again or the window exit button if they wish to quit.



Here is the code:
# 16-blocks

import pygame, random

pygame.init()
pygame.font.init()

n = 4
pad = 50
toolbar = 100
box_size = 150
side = n*box_size + (n + 1)*pad 
window = pygame.display.set_mode( (side, side+toolbar) )
pygame.display.set_caption("15-Blox")
font = pygame.font.SysFont('Verdana', 60)

WHITE = (255,255,255)
BLACK = (0,0,0)

clicks = 0

class Tile:
    def __init__(self, i, j):
        self.x = (i+1)*pad + i*box_size
        self.y = (j+1)*pad + j*box_size 
        self.id = i + j * n
        self.val = self.id
        self.color = WHITE
        self.surf = pygame.Surface( (box_size, box_size) )
        self.rect = pygame.Rect((self.x, self.y),(box_size,box_size))
        self.draw()
    def draw(self):
        if self.val != 0:            
            pygame.draw.rect(window, WHITE, self.rect)
            num_surf = font.render(str(self.val), True, BLACK)
            num_rect = num_surf.get_rect()
            num_rect.center = self.rect.center
            window.blit(num_surf, (num_rect.x, num_rect.y) )
        else:
            pygame.draw.rect(window, BLACK, self.rect)
    def __repr__(self):
        return str(self.val)

class Board:
    win_state = [i for i in range(1,n*n)] + [0]
    def __init__(self):
        # tiles in order 0-15
        self.board = [Tile(i,j) for j in range(n) for i in range(n)]
        self.vals = [i for i in range(n*n)]
        self.permute()
    def draw(self):
        window.fill(BLACK)
        for tile in self.board:
            tile.draw()
    def swap(self, clicked, index):
        global clicks
        if clicked.val != 0:
            way = 0
            if self.zero//n == index//n: # same row
                way = 1 if self.zero < index else -1
            elif self.zero%n == index%n: # same col
                way = n if self.zero < index else -n
            if way:
                for i in range(self.zero, index+way, way):
                    self.vals[self.zero] = self.vals[i]
                    self.vals[i] = 0
                    self.update()
                clicks += 1
    def update(self):
        self.zero = self.vals.index(0)
        for i,tile in enumerate(self.board):
            tile.val = self.vals[i]
    def permute(self):
        while True:
            random.shuffle(self.vals)
            self.update()
            e = self.vals.index(0) // n # row of zero
            counts = []
            for i,tile in enumerate(self.board):
                for j in range(i):
                    if 0 < self.vals[j] < tile.val:
                        counts.append(1)
            if (sum(counts) + e) % 2 == 0:
                return
    
def blox():
    global clicks
    clicks = 0
    board = Board()
    while True:
        board.draw()
        
        # stats pane
        surf = font.render('Clicks: ' + str(clicks), True, WHITE)
        rect = surf.get_rect()
        rect.bottomleft = (0, side+toolbar)
        window.blit(surf, (rect.x, rect.y) )
        
        # restart button
        restart_surf = font.render('Restart', True, WHITE)
        restart_rect = restart_surf.get_rect()
        restart_rect.bottomright = (side, side+toolbar)
        window.blit(restart_surf, (restart_rect.x, restart_rect.y) )

        # event loop
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                pygame.font.quit()
                quit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                if restart_rect.collidepoint(event.pos):
                    blox()
                for index,tile in enumerate(board.board):
                    if tile.rect.collidepoint(event.pos):
                        board.swap(tile, index)
                    if board.vals == Board.win_state:
                        break

        pygame.display.update()

blox()


No comments:

Post a Comment