Friday, March 6, 2015

Flood-It (Python / Pygame)

Flood-It is a game where you're given a 14x14 grid of blocks randomly colored from a palette of 6 colors.



The objective is to get the whole grid to be one color. You start at the top-left corner, red/orange here, and progress your way across by clicking the palette on the right. My next move was to click green, which earned me two more pieces. For an added challenge the player must do this in 25 (or fewer) turns.

It's difficult but it can be done:

I keep track of tiles the user gains in a list called "blob." The tiles adjacent to ones in blob are called "neighbors." When the user chooses a color, I iterate over each tile in blob and find all its neighbors (if valid: corners, for example, only have two neighbors). Each neighboring tile gets added to the blob if it's the same color; otherwise it is added to the neighbor list.

I toyed with Pygame subsurfaces to make the panel. (Compare this to my 15 puzzle, where the score and restart button were added "on the same layer" as the game.) The code is a little messy here, but it's a nifty enough method of placing buttons that I'll work on cleaning it up in future projects.

Here's the code:

'''Flood It'''
import pygame as pyg, random

pyg.font.init()

YELLOW = (243,246,29)
PURPLE = (96,92,168)
GREEN = (126,157,30)
PINK = (237,112,161)
BLUE = (70,177,226)
RED = (220,74,32)
colors = [YELLOW, PURPLE, GREEN, PINK, BLUE, RED]

m,n = 14,14
pad = 15
size = 25
most = 25
area = (size,size)
mid = round(len(colors)/2)
p_wid = mid*(pad+size)+pad   # panel (non-game area) width
height,width = m*size,n*size+p_wid
window = pyg.display.set_mode( (width,height) )
pyg.display.set_caption("Flood-It!")

class Tile:
    board = window.subsurface(pyg.Rect(0,0,n*size,m*size))
    def __init__(self,i):
        self.i = i
        self.color = random.choice(colors)
        self.rect = pyg.Rect( ((i%m)*size,(i//n)*size), area)
        self.draw()
    def __repr__(self):
        return str(self.i)
    def draw(self):
        pyg.draw.rect(Tile.board,self.color,self.rect)

class Button:
    def __init__(self,i,color):
        gap = lambda x: x * (size+pad)
        self.color = color
        self.drect = pyg.Rect( (gap(i%mid),gap(i//mid)), area)
        self.rect = self.drect.move(*Pane.bpane.get_abs_offset())
        self.draw()
    def draw(self):
        pyg.draw.rect(Pane.bpane,self.color,self.drect)

class Board:    
    def __init__(self):
        '''
        A Board object has three lists:
          1) Board - a collection of ordered tiles
          2) Blob - tiles the player accrues
          3) Neighbors - tiles that may be added to blob
        '''
        self.board = [Tile(i) for i in range(m*n)]
        self.blob = [self.board[0]]
        self.neighbors = []
        self.make_blob(self.blob[0].color)
        
    def __getitem__(self,i):
        '''Board[i] gets ith item from Board.board'''
        return self.board[i]
    
    def get_neighbors(self,tile):
        '''For a given tile: fetches the ones beside, above,
           and below it'''
        i = tile.i
        dirs = [-n,1,n,-1] # north,east,south,west
        return [self[i+d] for d in dirs if self.is_valid(i+d,d)]

    def is_valid(self,i,j):
        '''Checks if index belongs to a valid neighbor'''
        if 0 <= i < m*n:
            # without these conditions, blob wraps around corners
            if j == 1:
                return i%m >= (i-j)%m
            elif j == -1:
                return i%m <= (i-j)%m 
            return True
        
    def make_blob(self,color):
        '''Adds more tiles to blob'''
        for cell in self.blob:
            cell.color = color
            for neighbor in self.get_neighbors(cell):
                if neighbor.color == cell.color:
                    if neighbor not in self.blob:
                        self.blob.append(self[neighbor.i])
                elif neighbor not in self.neighbors:
                    self.neighbors.append(self[neighbor.i])

class Text:
    '''A text block, optionally clickable'''
    def __init__(self,text,surf,font_size=25):
        surf.fill( (0,0,0) )
        font = pyg.font.SysFont('Arial',font_size)
        font_surf = font.render(text,font_size,(255,255,255))
        self.rect = font_surf.get_rect().move(*surf.get_abs_offset())
        surf.blit(font_surf, (0,0))
        
class Pane:
    a = size+pad
    b = p_wid-pad
    pane = window.subsurface(pyg.Rect(n*size,0,p_wid,height))
    bpane = pane.subsurface(pyg.Rect(pad,pad,b,2*a))
    click = pane.subsurface(pyg.Rect(pad,3*(size+pad),b,2*a))
    replay = pane.subsurface(pyg.Rect(pad,5*(size+pad),b,a))
    end = pane.subsurface(pyg.Rect(pad,7*(size+pad),b,a))
    def __init__(self):
        self.buttons = [Button(i,c) for i,c in enumerate(colors)]
        self.restart = Text('Replay',Pane.replay)
        self.end = Text('Quit',Pane.end)
        self.update()
    def update(self,count=0):
        self.click = Text('Clicks: %d/%d' % (count,most),Pane.click)
        

def flood_it():
    board = Board()
    pane = Pane()
    clicks = 0
    play = True
    while True:
        for event in pyg.event.get():
            if event.type == pyg.QUIT:
                pyg.quit()
                quit()
            if event.type == pyg.MOUSEBUTTONDOWN:                
                for button in pane.buttons:
                    if button.rect.collidepoint(event.pos):
                        if play:
                            board.make_blob(button.color)
                            [tile.draw() for tile in board.board]
                            clicks += 1
                            pane.update(clicks)
                        if len(board.blob) == m*n or clicks >= most:
                            play = False
                    elif pane.restart.rect.collidepoint(event.pos):
                        flood_it()
                    elif pane.end.rect.collidepoint(event.pos):
                        pyg.quit()
                        quit()
        pyg.display.update()

flood_it()