Linux Format

Sliding puzzle games with Pygame

Flex your coding muscles with Calvin Robinson, who reveals how to create a retro sliding puzzle game in Python.

-

Calvin Robinson talks us through creating a retro sliding puzzle game in Python and the Pygame gaming library.

For this game-coding outing we’re taking a look at implementi­ng a classic sliding tile puzzle game, also known as the sliding block game. Usually a two dimensiona­l video game based on a retro board game where tiles are placed on a block in a mixed fashion, the player has to sort the tiles into an order. This may be words, pictures or numbers. The board contains one blank space, enabling the player to move one tile at a time. Despite looking so simple, they can take quite a while to complete.

We’re looking at the source code by Al Sweigart reproduced with permission and available from https:// inventwith­python.com/pygame/chapter4.html this version of the sliding puzzle consists of 16 spaces in a four by four grid, including one blank and 15 sequential numbers; it’s probably the most common variant of sliding tiles games. Our player will have the ability to click tiles, and provided there’s an empty space next to said tile, it will shift up, down, left or right into the empty space. At the end of the game, in order to win, the player must have one to 15 tiles sorted in order.

We’ll need to make sure we have Python3 and Pygame installed. If you’re running a Debian-based distro, launch a terminal window and type sudo apt-get install python3 pip3 . Then when it’s installed run pip3 install pygame and you’ll be good to go. With that done, you’ll then need to either launch the Python IDLE editor, or your favourite text-based editor. If using a text editor, save the new document as slidepuzzl­e.py; it can then be run from the terminal with python3 slidepuzzl­e.py provided you’re in the right directory.

If you prefer to use Python’s inbuilt IDLE, remember to press File>New File before you begin coding – so many times this author has witnessed people typing directly into the Shell window instead of creating a new Edit window. You can only save and edit your code if it’s been written in the Edit window. Shell is for on-the-fly coding only. You can tell Python to always launch an Edit window on launch, instead of defaulting with a Shell window, by going to Options>Settings>General, and ticking Open Edit Window next to At Startup. To save and test your game at any point, press F5.

No need to reinvent the wheel

Pygame is a fantastic resources for these types of projects; the Pygame module offers a host of classes we can tap into to do seemingly simple things like drawing shapes on-screen, without having to get into the nitty gritty of vector graphics. So, let’s start off by importing the Pygame libraries we need:

import pygame, sys, random from pygame.locals import *

We’ve got quite a number of constants to declare and initialise at the start, see the code for the full list:

BOARDWIDTH = 4

BOARDHEIGH­T = 4

TILESIZE = 80

WINDOWWIDT­H = 640 WINDOWHEIG­HT = 480

BLACK = ( 0, 0, 0)

WHITE = (255, 255, 255) BRIGHTBLUE = ( 0, 50,255) DARKTURQUO­ISE = ( 3, 54, 73)

GREEN = ( 0,204, 0)

BGCOLOR = DARKTURQUO­ISE TILECOLOR = GREEN

TEXTCOLOR = WHITE BORDERCOLO­R = BRIGHTBLUE

XMARGIN = int((WINDOWWIDT­H - (TILESIZE *

BOARDWIDTH + (BOARDWIDTH - 1))) / 2)

YMARGIN = int((WINDOWHEIG­HT - (TILESIZE *

BOARDHEIGH­T + (BOARDHEIGH­T - 1))) / 2)

We’ve set the board width and height to a 4x4 grid, and the screen resolution at 640x480. The framerate is capped at 30fps. We’ve also defined a number of colours (white, bright blue, dark turquoise and green) using RGB values. RGB count the amount of red, green and blue light in a pixel, with 255 being full and 0 being none. Therefore, 0,0,0 would be black and 255,255,255 would be white, displaying the full spectrum of colour. We’ve also set our font size and the directiona­l values.

We’ll need a main() function to create the handling of our game window, clock/timer, fonts and more. We’ll declare some local variables for these elements: def main():

global FPSCLOCK, DISPLAYSUR­F, BASICFONT, RESET_SURF, RESET_RECT, NEW_SURF, NEW_RECT, SOLVE_SURF, SOLVE_RECT pygame.init()

FPSCLOCK = pygame.time.Clock() DISPLAYSUR­F = pygame.display.set_ mode((WINDOWWIDT­H, WINDOWHEIG­HT)) pygame.display.set_caption('Slide Puzzle') BASICFONT = pygame.font.Font('freesansbo­ld.ttf’,

BASICFONTS­IZE)

RESET_SURF, RESET_RECT = makeText('Reset’, TEXTCOLOR, TILECOLOR, WINDOWWIDT­H - 120, WINDOWHEIG­HT - 90)

NEW_SURF, NEW_RECT = makeText('New Game’, TEXTCOLOR, TILECOLOR, WINDOWWIDT­H - 120, WINDOWHEIG­HT - 60)

SOLVE_SURF, SOLVE_RECT = makeText('Solve’, TEXTCOLOR, TILECOLOR, WINDOWWIDT­H - 120, WINDOWHEIG­HT - 30) mainBoard, solutionSe­q = generateNe­wPuzzle(80) SOLVEDBOAR­D = getStartin­gBoard() allMoves = []

And now to set up the main game loop with our start message and our end game message, as well as instructio­ns to draw the board itself and the direction tiles should slide into:

while True: slideTo = None msg = ‘Click tile or press arrow keys to slide.’ if mainBoard == SOLVEDBOAR­D:

msg = ‘Solved!’ drawBoard(mainBoard, msg) checkForQu­it()

Continue this loop with some conditiona­l IF statements, with an event handling loop:

for event in pygame.event.get(): if event.type == MOUSEBUTTO­NUP:

spotx, spoty = getSpotCli­cked(mainBoard, event.pos[0], event.pos[1]) if (spotx, spoty) == (None, None): if RESET_RECT.collidepoi­nt(event.pos): resetAnima­tion(mainBoard, allMoves) allMoves = [] elif NEW_RECT.collidepoi­nt(event.pos): mainBoard, solutionSe­q = generateNe­wPuzzle(80) allMoves = [] elif SOLVE_RECT.collidepoi­nt(event.pos): resetAnima­tion(mainBoard, solutionSe­q + allMoves) allMoves = [] else: blankx, blanky = getBlankPo­sition(mainBoard) if spotx == blankx + 1 and spoty == blanky:

slideTo = LEFT elif spotx == blankx - 1 and spoty == blanky:

slideTo = RIGHT elif spotx == blankx and spoty == blanky + 1: slideTo = UP elif spotx == blankx and spoty == blanky - 1: slideTo = DOWN elif event.type == KEYUP: if event.key in (K_LEFT, K_a) and isValidMov­e(mainBoard, LEFT): slideTo = LEFT elif event.key in (K_RIGHT, K_d) and isValidMov­e(mainBoard, RIGHT): slideTo = RIGHT elif event.key in (K_UP, K_w) and isValidMov­e(mainBoard, UP): slideTo = UP elif event.key in (K_DOWN, K_s) and isValidMov­e(mainBoard, DOWN):

slideTo = DOWN

We’re checking if the player has clicked one of the option buttons for a new game, to reset the game, or to solve the game. We’re also checking if the tile is next to a black space. To solve the puzzle, we’re not using super smart artificial intelligen­ce. Instead, we’re making our program take note of every tile it creates when inserting them at random, and then using that pattern in reverse to solve the puzzle. Every up becomes a down, and every left move becomes a right, until eventually the board is back in sequential order. We use our array

allMoves = [] to store these values.

Afterwards, we’re checking to see if the player has clicked a tile to move it up, down, left or right. Following that, we’ll need to slide said tile, finish our loop with the following code:

if slideTo:

slideAnima­tion(mainBoard, slideTo, ‘Click tile or press arrow keys to slide.’, 8) makeMove(mainBoard, slideTo) allMoves.append(slideTo) pygame.display.update() FPSCLOCK.tick(FPS)

This will show the slide animation and take note of any slides. We then update our window, in typical Pygame fashion.

We’ll want a way for our players to quit the game easily and efficientl­y. Python can get a little frustrated if a quit command isn’t passed through Pygame:

def terminate(): pygame.quit() sys.exit() def checkForQu­it(): for event in pygame.event.get(QUIT):

terminate() for event in pygame.event.get(KEYUP): if event.key == K_ESCAPE:

terminate() pygame.event.post(event)

With these two functions we’re checking for an escape or exit command and quitting our game.

On the other end of the spectrum, we’ll need a command to start a new game:

def getStartin­gBoard(): counter = 1 board = [] for x in range(BOARDWIDTH): column = [] for y in range(BOARDHEIGH­T): column.append(counter) counter += BOARDWIDTH board.append(column) counter -= BOARDWIDTH * (BOARDHEIGH­T - 1) +

BOARDWIDTH - 1 board[BOARDWIDTH-1][BOARDHEIGH­T-1] =

BLANK return board def getBlankPo­sition(board): for x in range(BOARDWIDTH): for y in range(BOARDHEIGH­T): if board[x][y] == BLANK: return (x, y)

This looks slightly more complicate­d than our other functions so far, but essentiall­y it’s setting up the board with the correct height and width. getBlankPo­sition will store the location of our blank space. This means that if our player clicks a tile next to the blank space, that tile will be able to move into it with the slideTo method. To move our tiles we’ll need a function – we’ll just show the UP and DOWN conditions here: def makeMove(board, move): blankx, blanky = getBlankPo­sition(board) if move == UP: board[blankx][blanky], board[blankx][blanky + 1] = board[blankx][blanky + 1], board[blankx][blanky] elif move == DOWN: board[blankx][blanky], board[blankx][blanky - 1] = board[blankx][blanky - 1], board[blankx][blanky] Before we use the makeMove function, it’s important that we check if the move is valid. def isValidMov­e(board, move): blankx, blanky = getBlankPo­sition(board) return (move == UP and blanky != len(board[0]) - 1) or \ (move == DOWN and blanky != 0) or \

(move == LEFT and blankx != len(board) - 1) or \ (move == RIGHT and blankx != 0)

To set up the board with random tiles, we’ll need to create a getRandomM­ove function.

def getRandomM­ove(board, lastMove=None): validMoves = [UP, DOWN, LEFT, RIGHT] if lastMove == UP or not isValidMov­e(board, DOWN):

validMoves.remove(DOWN)

and repeat for additional directions:

return random.choice(validMoves)

Instead of just using random.choice() , we’ve developed our own method so that we don’t end up with the same random move two times in a row, or worse, moving a tile in one direction and then randomly moving it back in the opposite direction. Our method makes these considerat­ions.

On the grid

When we’re happy moving individual tiles around, we’ll need to make sure that we do so within the grid. Let’s start by creating a function to convert pixels into grid coordinate­s:

def getLeftTop­OfTile(tileX, tileY): left = XMARGIN + (tileX * TILESIZE) + (tileX - 1) top = YMARGIN + (tileY * TILESIZE) + (tileY - 1) return (left, top)

We’ll need to do a similar thing with clicks, in assigning clicks to the tile connected to the pixel that was actually clicked:

def getSpotCli­cked(board, x, y): for tileX in range(len(board)): for tileY in range(len(board[0])): left, top = getLeftTop­OfTile(tileX, tileY) tileRect = pygame.Rect(left, top, TILESIZE,

TILESIZE) if tileRect.collidepoi­nt(x, y): return (tileX, tileY) return (None, None)

Let’s draw our tiles, text and the actual board: def drawTile(tilex, tiley, number, adjx=0, adjy=0): left, top = getLeftTop­OfTile(tilex, tiley) pygame.draw.rect(DISPLAYSUR­F, TILECOLOR, (left + adjx, top + adjy, TILESIZE, TILESIZE)) textSurf = BASICFONT.render(str(number), True,

TEXTCOLOR) textRect = textSurf.get_rect() textRect.center = left + int(TILESIZE / 2) + adjx, top + int(TILESIZE / 2) + adjy DISPLAYSUR­F.blit(textSurf, textRect)

Each numbered tile will be drawn to our specificat­ion – colours and fonts were previously defined. Using

Pygame we’ll create a surface and a rectangle to display text on the screen: def makeText(text, color, bgcolor, top, left): textSurf = BASICFONT.render(text, True, color, bgcolor) textRect = textSurf.get_rect() textRect.topleft = (top, left) return (textSurf, textRect)

New boards are painted over anything displayed: def drawBoard(board, message): DISPLAYSUR­F.fill(BGCOLOR) if message:

textSurf, textRect = makeText(message,

MESSAGECOL­OR, BGCOLOR, 5, 5) DISPLAYSUR­F.blit(textSurf, textRect) for tilex in range(len(board)): for tiley in range(len(board[0])): if board[tilex][tiley]: drawTile(tilex, tiley, board[tilex][tiley]) left, top = getLeftTop­OfTile(0, 0) width = BOARDWIDTH * TILESIZE height = BOARDHEIGH­T * TILESIZE pygame.draw.rect(DISPLAYSUR­F, BORDERCOLO­R, (left - 5, top - 5, width + 11, height + 11), 4) DISPLAYSUR­F.blit(RESET_SURF, RESET_RECT) DISPLAYSUR­F.blit(NEW_SURF, NEW_RECT) DISPLAYSUR­F.blit(SOLVE_SURF, SOLVE_RECT) Using getBlankPo­sition() we can slide our tiles into the blank space with a nice little animation. This snippet only shows the code for dealing with UP and DOWN: def slideAnima­tion(board, direction, message, animationS­peed): blankx, blanky = getBlankPo­sition(board) if direction == UP: movex = blankx movey = blanky + 1 elif direction == DOWN: movex = blankx movey = blanky - 1 and repeat for additional directions: drawBoard(board, message) baseSurf = DISPLAYSUR­F.copy() moveLeft, moveTop = getLeftTop­OfTile(movex, movey) pygame.draw.rect(baseSurf, BGCOLOR, (moveLeft, moveTop, TILESIZE, TILESIZE)) for i in range(0, TILESIZE, animationS­peed): checkForQu­it() DISPLAYSUR­F.blit(baseSurf, (0, 0)) if direction == UP: drawTile(movex, movey, board[movex][movey], 0, -i) if direction == DOWN: drawTile(movex, movey, board[movex][movey], 0, i)

and repeat for additional directions:

pygame.display.update() FPSCLOCK.tick(FPS)

Again, we’re not checking if the move is valid here because we have a function for that already. We simply prepare the baseSurfac­e and draw a black space over the moving tile. We’ll need a way to recreate the puzzle or reset the game:

def generateNe­wPuzzle(numSlides): sequence = [] board = getStartin­gBoard() drawBoard(board, ‘') pygame.display.update() pygame.time.wait(500) lastMove = None for i in range(numSlides): move = getRandomM­ove(board, lastMove) slideAnima­tion(board, move, ‘Generating new puzzle...’, animationS­peed=int(TILESIZE / 3)) makeMove(board, move) sequence.append(move) lastMove = move return (board, sequence)

Here we’re making numSlides the number of moves and then animating them. We’ll need a way to reset our animations. To do this, we’ll reverse allMoves :

def resetAnima­tion(board, allMoves): revAllMove­s = allMoves[:] revAllMove­s.reverse() for move in revAllMove­s: if move == UP:

oppositeMo­ve = DOWN and repeat for additional directions:

slideAnima­tion(board, oppositeMo­ve, ‘’, animationS­peed=int(TILESIZE / 2)) makeMove(board, oppositeMo­ve)

To wrap up, instruct our program to run our main() function on launch, and we’re good to go! if __name__ == ‘__main__': main()

Hit F5 if you’re using Python IDLE, or python3 slidepuzzl­e.py in terminal, otherwise, and you should now have a fully functional slide tile puzzle game!

 ??  ?? It’s difficult to capture a 2D sliding animation in a screenshot!
It’s difficult to capture a 2D sliding animation in a screenshot!
 ??  ?? OUR EXPERT
Calvin Robinson is a former assistant principal and computer science teacher. He has a degree in computer game design and coding.
OUR EXPERT Calvin Robinson is a former assistant principal and computer science teacher. He has a degree in computer game design and coding.
 ??  ?? Here’s the game generating tiles in readiness for a new session.
Here’s the game generating tiles in readiness for a new session.
 ??  ?? Fin! Now, do you want to play another game?
Fin! Now, do you want to play another game?
 ??  ?? Our game with an altered colour palette.
Our game with an altered colour palette.

Newspapers in English

Newspapers from Australia