Chapter 10 – Four Extra Games

Included in this chapter is the source code for four extra games. Unfortunately, only the source code (including comments) is in this chapter without any detailed explanation of the code. By now, you can play these games and figure out how the code works by looking at the source code and comments.

The games are:

·         Flippy – An “Othello” clone where the player tries to flip the computer AI player’s tiles.

·         Ink Spill – A “Flood It” clone that makes use of the flood fill algorithm.

·         Four in a Row – A “Connect Four” clone against the computer AI player.

·         Gemgem – A “Bejeweled” clone where the player swaps gems to try to get three identical gems in a row.

If you have any questions about the source code in this book, feel free to email the author at [email protected]

Buggy versions of these programs are also available if you want to practice fixing bugs:

·         http://invpy.com/buggy/flippy

·         http://invpy.com/buggy/inkspill

·         http://invpy.com/buggy/fourinarow

·         http://invpy.com/buggy/gemgem


 

Flippy, an “Othello” Clone

    

Othello, also known by the generic name Reversi, has an 8 x 8 board with tiles that are black on one side and white on the other. The starting board looks like Figure 10-1. Each player takes turn placing down a new tile of their color. Any of the opponent's tiles that are between the new tile and the other tiles of that color is flipped. The goal of the game is to have as many of the tiles with your color as possible. For example, Figure 10-2 is what it looks like if the white player places a new white tile on space 5, 6.

Description: C:\book1svn\4thed\images\15-1.png

Description: C:\book1svn\4thed\images\15-2.png

The starting Reversi board has two white tiles and two black tiles.

White places a new tile.

 

The black tile at 5, 5 is in between the new white tile and the existing white tile at 5, 4. That black tile is flipped over and becomes a new white tile, making the board look like Figure 10-3. Black makes a similar move next, placing a black tile on 4, 6 which flips the white tile at 4, 5. This results in a board that looks like Figure 10-4.

Description: C:\book1svn\4thed\images\15-3.png

Description: C:\book1svn\4thed\images\15-4.png

White's move will flip over one of black's tiles.

Black places a new tile, which flips over one of white's tiles.

 

Tiles in all directions are flipped as long as they are in between the player's new tile and existing tile. In Figure 10-5, the white player places a tile at 3, 6 and flips black tiles in both directions (marked by the lines). The result is in Figure 10-6.

Description: C:\book1svn\4thed\images\15-5.png

Description: C:\book1svn\4thed\images\15-6.png

White's second move at 3, 6 will flip two of black's tiles.

The board after white's second move.

 

As you can see, each player can quickly grab a majority of the tiles on the board in just one or two moves. Players must always make a move that captures at least one tile. The game ends when a player either cannot make a move, or the board is completely full. The player with the most tiles of their color wins.

You can learn more about Reversi from Wikipedia: http://en.wikipedia.org/wiki/Reversi

A text version of this game that uses print() and input() instead of Pygame is featured in Chapter 15 of “Invent Your Own Computer Games with Python”. You can read that chapter for details about how the computer AI’s algorithm was put together. http://inventwithpython.com/chapter15.html

The computer AI for this game is pretty good, because it is easy for a computer to simulate every possible move and take the one that flips over the most tiles. It usually beats me whenever I play it.

Source Code for Flippy

This source code can be downloaded from http://invpy.com/flippy.py.

The image files that Flippy uses can be downloaded from http://invpy.com/flippyimages.zip.

  1. # Flippy (an Othello or Reversi clone)

  2. # By Al Sweigart [email protected]

  3. # http://inventwithpython.com/pygame

  4. # Released under a "Simplified BSD" license

  5.

  6. # Based on the "reversi.py" code that originally appeared in "Invent

  7. # Your Own Computer Games with Python", chapter 15:

  8. #   http://inventwithpython.com/chapter15.html

  9.

 10. import random, sys, pygame, time, copy

 11. from pygame.locals import *

 12.

 13. FPS = 10 # frames per second to update the screen

 14. WINDOWWIDTH = 640 # width of the program's window, in pixels

 15. WINDOWHEIGHT = 480 # height in pixels

 16. SPACESIZE = 50 # width & height of each space on the board, in pixels

 17. BOARDWIDTH = 8 # how many columns of spaces on the game board

 18. BOARDHEIGHT = 8 # how many rows of spaces on the game board

 19. WHITE_TILE = 'WHITE_TILE' # an arbitrary but unique value

 20. BLACK_TILE = 'BLACK_TILE' # an arbitrary but unique value

 21. EMPTY_SPACE = 'EMPTY_SPACE' # an arbitrary but unique value

 22. HINT_TILE = 'HINT_TILE' # an arbitrary but unique value

 23. ANIMATIONSPEED = 25 # integer from 1 to 100, higher is faster animation

 24.

 25. # Amount of space on the left & right side (XMARGIN) or above and below

 26. # (YMARGIN) the game board, in pixels.

 27. XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * SPACESIZE)) / 2)

 28. YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * SPACESIZE)) / 2)

 29.

 30. #              R    G    B

 31. WHITE      = (255, 255, 255)

 32. BLACK      = (  0,   0,   0)

 33. GREEN      = (  0, 155,   0)

 34. BRIGHTBLUE = (  0,  50, 255)

 35. BROWN      = (174,  94,   0)

 36.

 37. TEXTBGCOLOR1 = BRIGHTBLUE

 38. TEXTBGCOLOR2 = GREEN

 39. GRIDLINECOLOR = BLACK

 40. TEXTCOLOR = WHITE

 41. HINTCOLOR = BROWN

 42.

 43.

 44. def main():

 45.     global MAINCLOCK, DISPLAYSURF, FONT, BIGFONT, BGIMAGE

 46.

 47.     pygame.init()

 48.     MAINCLOCK = pygame.time.Clock()

 49.     DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

 50.     pygame.display.set_caption('Flippy')

 51.     FONT = pygame.font.Font('freesansbold.ttf', 16)

 52.     BIGFONT = pygame.font.Font('freesansbold.ttf', 32)

 53.

 54.     # Set up the background image.

 55.     boardImage = pygame.image.load('flippyboard.png')

 56.     # Use smoothscale() to stretch the board image to fit the entire board:

 57.     boardImage = pygame.transform.smoothscale(boardImage, (BOARDWIDTH * SPACESIZE, BOARDHEIGHT * SPACESIZE))

 58.     boardImageRect = boardImage.get_rect()

 59.     boardImageRect.topleft = (XMARGIN, YMARGIN)

 60.     BGIMAGE = pygame.image.load('flippybackground.png')

 61.     # Use smoothscale() to stretch the background image to fit the entire window:

 62.     BGIMAGE = pygame.transform.smoothscale(BGIMAGE, (WINDOWWIDTH, WINDOWHEIGHT))

 63.     BGIMAGE.blit(boardImage, boardImageRect)

 64.

 65.     # Run the main game.

 66.     while True:

 67.         if runGame() == False:

 68.             break

 69.

 70.

 71. def runGame():

 72.     # Plays a single game of reversi each time this function is called.

 73.

 74.     # Reset the board and game.

 75.     mainBoard = getNewBoard()

 76.     resetBoard(mainBoard)

 77.     showHints = False

 78.     turn = random.choice(['computer', 'player'])

 79.

 80.     # Draw the starting board and ask the player what color they want.

 81.     drawBoard(mainBoard)

 82.     playerTile, computerTile = enterPlayerTile()

 83.

 84.     # Make the Surface and Rect objects for the "New Game" and "Hints" buttons

 85.     newGameSurf = FONT.render('New Game', True, TEXTCOLOR, TEXTBGCOLOR2)

 86.     newGameRect = newGameSurf.get_rect()

 87.     newGameRect.topright = (WINDOWWIDTH - 8, 10)

 88.     hintsSurf = FONT.render('Hints', True, TEXTCOLOR, TEXTBGCOLOR2)

 89.     hintsRect = hintsSurf.get_rect()

 90.     hintsRect.topright = (WINDOWWIDTH - 8, 40)

 91.

 92.     while True: # main game loop

 93.         # Keep looping for player and computer's turns.

 94.         if turn == 'player':

 95.             # Player's turn:

 96.             if getValidMoves(mainBoard, playerTile) == []:

 97.                 # If it's the player's turn but they

 98.                 # can't move, then end the game.

 99.                 break

100.             movexy = None

101.             while movexy == None:

102.                 # Keep looping until the player clicks on a valid space.

103.

104.                 # Determine which board data structure to use for display.

105.                 if showHints:

106.                     boardToDraw = getBoardWithValidMoves(mainBoard, playerTile)

107.                 else:

108.                     boardToDraw = mainBoard

109.

110.                 checkForQuit()

111.                 for event in pygame.event.get(): # event handling loop

112.                     if event.type == MOUSEBUTTONUP:

113.                         # Handle mouse click events

114.                         mousex, mousey = event.pos

115.                         if newGameRect.collidepoint( (mousex, mousey) ):

116.                             # Start a new game

117.                             return True

118.                         elif hintsRect.collidepoint( (mousex, mousey) ):

119.                             # Toggle hints mode

120.                             showHints = not showHints

121.                         # movexy is set to a two-item tuple XY coordinate, or None value

122.                         movexy = getSpaceClicked(mousex, mousey)

123.                         if movexy != None and not isValidMove(mainBoard, playerTile, movexy[0], movexy[1]):

124.                             movexy = None

125.

126.                 # Draw the game board.

127.                 drawBoard(boardToDraw)

128.                 drawInfo(boardToDraw, playerTile, computerTile, turn)

129.

130.                 # Draw the "New Game" and "Hints" buttons.

131.                 DISPLAYSURF.blit(newGameSurf, newGameRect)

132.                 DISPLAYSURF.blit(hintsSurf, hintsRect)

133.

134.                 MAINCLOCK.tick(FPS)

135.                 pygame.display.update()

136.

137.             # Make the move and end the turn.

138.             makeMove(mainBoard, playerTile, movexy[0], movexy[1], True)

139.             if getValidMoves(mainBoard, computerTile) != []:

140.                 # Only set for the computer's turn if it can make a move.

141.                 turn = 'computer'

142.

143.         else:

144.             # Computer's turn:

145.             if getValidMoves(mainBoard, computerTile) == []:

146.                 # If it was set to be the computer's turn but

147.                 # they can't move, then end the game.

148.                 break

149.

150.             # Draw the board.

151.             drawBoard(mainBoard)

152.             drawInfo(mainBoard, playerTile, computerTile, turn)

153.

154.             # Draw the "New Game" and "Hints" buttons.

155.             DISPLAYSURF.blit(newGameSurf, newGameRect)

156.             DISPLAYSURF.blit(hintsSurf, hintsRect)

157.

158.             # Make it look like the computer is thinking by pausing a bit.

159.             pauseUntil = time.time() + random.randint(5, 15) * 0.1

160.             while time.time() < pauseUntil:

161.                 pygame.display.update()

162.

163.             # Make the move and end the turn.

164.             x, y = getComputerMove(mainBoard, computerTile)

165.             makeMove(mainBoard, computerTile, x, y, True)

166.             if getValidMoves(mainBoard, playerTile) != []:

167.                 # Only set for the player's turn if they can make a move.

168.                 turn = 'player'

169.

170.     # Display the final score.

171.     drawBoard(mainBoard)

172.     scores = getScoreOfBoard(mainBoard)

173.

174.     # Determine the text of the message to display.

175.     if scores[playerTile] > scores[computerTile]:

176.         text = 'You beat the computer by %s points! Congratulations!' % \

177.                (scores[playerTile] - scores[computerTile])

178.     elif scores[playerTile] < scores[computerTile]:

179.         text = 'You lost. The computer beat you by %s points.' % \

180.                (scores[computerTile] - scores[playerTile])

181.     else:

182.         text = 'The game was a tie!'

183.

184.     textSurf = FONT.render(text, True, TEXTCOLOR, TEXTBGCOLOR1)

185.     textRect = textSurf.get_rect()

186.     textRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))

187.     DISPLAYSURF.blit(textSurf, textRect)

188.

189.     # Display the "Play again?" text with Yes and No buttons.

190.     text2Surf = BIGFONT.render('Play again?', True, TEXTCOLOR, TEXTBGCOLOR1)

191.     text2Rect = text2Surf.get_rect()

192.     text2Rect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 50)

193.

194.     # Make "Yes" button.

195.     yesSurf = BIGFONT.render('Yes', True, TEXTCOLOR, TEXTBGCOLOR1)

196.     yesRect = yesSurf.get_rect()

197.     yesRect.center = (int(WINDOWWIDTH / 2) - 60, int(WINDOWHEIGHT / 2) + 90)

198.

199.     # Make "No" button.

200.     noSurf = BIGFONT.render('No', True, TEXTCOLOR, TEXTBGCOLOR1)

201.     noRect = noSurf.get_rect()

202.     noRect.center = (int(WINDOWWIDTH / 2) + 60, int(WINDOWHEIGHT / 2) + 90)

203.

204.     while True:

205.         # Process events until the user clicks on Yes or No.

206.         checkForQuit()

207.         for event in pygame.event.get(): # event handling loop

208.             if event.type == MOUSEBUTTONUP:

209.                 mousex, mousey = event.pos

210.                 if yesRect.collidepoint( (mousex, mousey) ):

211.                     return True

212.                 elif noRect.collidepoint( (mousex, mousey) ):

213.                     return False

214.         DISPLAYSURF.blit(textSurf, textRect)

215.         DISPLAYSURF.blit(text2Surf, text2Rect)

216.         DISPLAYSURF.blit(yesSurf, yesRect)

217.         DISPLAYSURF.blit(noSurf, noRect)

218.         pygame.display.update()

219.         MAINCLOCK.tick(FPS)

220.

221.

222. def translateBoardToPixelCoord(x, y):

223.     return XMARGIN + x * SPACESIZE + int(SPACESIZE / 2), YMARGIN + y * SPACESIZE + int(SPACESIZE / 2)

224.

225.

226. def animateTileChange(tilesToFlip, tileColor, additionalTile):

227.     # Draw the additional tile that was just laid down. (Otherwise we'd

228.     # have to completely redraw the board & the board info.)

229.     if tileColor == WHITE_TILE:

230.         additionalTileColor = WHITE

231.     else:

232.         additionalTileColor = BLACK

233.     additionalTileX, additionalTileY = translateBoardToPixelCoord(additionalTile[0], additionalTile[1])

234.     pygame.draw.circle(DISPLAYSURF, additionalTileColor, (additionalTileX, additionalTileY), int(SPACESIZE / 2) - 4)

235.     pygame.display.update()

236.

237.     for rgbValues in range(0, 255, int(ANIMATIONSPEED * 2.55)):

238.         if rgbValues > 255:

239.             rgbValues = 255

240.         elif rgbValues < 0:

241.             rgbValues = 0

242.

243.         if tileColor == WHITE_TILE:

244.             color = tuple([rgbValues] * 3) # rgbValues goes from 0 to 255

245.         elif tileColor == BLACK_TILE:

246.             color = tuple([255 - rgbValues] * 3) # rgbValues goes from 255 to 0

247.

248.         for x, y in tilesToFlip:

249.             centerx, centery = translateBoardToPixelCoord(x, y)

250.             pygame.draw.circle(DISPLAYSURF, color, (centerx, centery), int(SPACESIZE / 2) - 4)

251.         pygame.display.update()

252.         MAINCLOCK.tick(FPS)

253.         checkForQuit()

254.

255.

256. def drawBoard(board):

257.     # Draw background of board.

258.     DISPLAYSURF.blit(BGIMAGE, BGIMAGE.get_rect())

259.

260.     # Draw grid lines of the board.

261.     for x in range(BOARDWIDTH + 1):

262.         # Draw the horizontal lines.

263.         startx = (x * SPACESIZE) + XMARGIN

264.         starty = YMARGIN

265.         endx = (x * SPACESIZE) + XMARGIN

266.         endy = YMARGIN + (BOARDHEIGHT * SPACESIZE)

267.         pygame.draw.line(DISPLAYSURF, GRIDLINECOLOR, (startx, starty), (endx, endy))

268.     for y in range(BOARDHEIGHT + 1):

269.         # Draw the vertical lines.

270.         startx = XMARGIN

271.         starty = (y * SPACESIZE) + YMARGIN

272.         endx = XMARGIN + (BOARDWIDTH * SPACESIZE)

273.         endy = (y * SPACESIZE) + YMARGIN

274.         pygame.draw.line(DISPLAYSURF, GRIDLINECOLOR, (startx, starty), (endx, endy))

275.

276.     # Draw the black & white tiles or hint spots.

277.     for x in range(BOARDWIDTH):

278.         for y in range(BOARDHEIGHT):

279.             centerx, centery = translateBoardToPixelCoord(x, y)

280.             if board[x][y] == WHITE_TILE or board[x][y] == BLACK_TILE:

281.                 if board[x][y] == WHITE_TILE:

282.                     tileColor = WHITE

283.                 else:

284.                     tileColor = BLACK

285.                 pygame.draw.circle(DISPLAYSURF, tileColor, (centerx, centery), int(SPACESIZE / 2) - 4)

286.             if board[x][y] == HINT_TILE:

287.                 pygame.draw.rect(DISPLAYSURF, HINTCOLOR, (centerx - 4, centery - 4, 8, 8))

288.

289.

290. def getSpaceClicked(mousex, mousey):

291.     # Return a tuple of two integers of the board space coordinates where

292.     # the mouse was clicked. (Or returns None not in any space.)

293.     for x in range(BOARDWIDTH):

294.         for y in range(BOARDHEIGHT):

295.             if mousex > x * SPACESIZE + XMARGIN and \

296.                mousex < (x + 1) * SPACESIZE + XMARGIN and \

297.                mousey > y * SPACESIZE + YMARGIN and \

298.                mousey < (y + 1) * SPACESIZE + YMARGIN:

299.                 return (x, y)

300.     return None

301.

302.

303. def drawInfo(board, playerTile, computerTile, turn):

304.     # Draws scores and whose turn it is at the bottom of the screen.

305.     scores = getScoreOfBoard(board)

306.     scoreSurf = FONT.render("Player Score: %s    Computer Score: %s    %s's Turn" % (str(scores[playerTile]), str(scores[computerTile]), turn.title()), True, TEXTCOLOR)

307.     scoreRect = scoreSurf.get_rect()

308.     scoreRect.bottomleft = (10, WINDOWHEIGHT - 5)

309.     DISPLAYSURF.blit(scoreSurf, scoreRect)

310.

311.

312. def resetBoard(board):

313.     # Blanks out the board it is passed, and sets up starting tiles.

314.     for x in range(BOARDWIDTH):

315.         for y in range(BOARDHEIGHT):

316.             board[x][y] = EMPTY_SPACE

317.

318.     # Add starting pieces to the center

319.     board[3][3] = WHITE_TILE

320.     board[3][4] = BLACK_TILE

321.     board[4][3] = BLACK_TILE

322.     board[4][4] = WHITE_TILE

323.

324.

325. def getNewBoard():

326.     # Creates a brand new, empty board data structure.

327.     board = []

328.     for i in range(BOARDWIDTH):

329.         board.append([EMPTY_SPACE] * BOARDHEIGHT)

330.

331.     return board

332.

333.

334. def isValidMove(board, tile, xstart, ystart):

335.     # Returns False if the player's move is invalid. If it is a valid

336.     # move, returns a list of spaces of the captured pieces.

337.     if board[xstart][ystart] != EMPTY_SPACE or not isOnBoard(xstart, ystart):

338.         return False

339.

340.     board[xstart][ystart] = tile # temporarily set the tile on the board.

341.

342.     if tile == WHITE_TILE:

343.         otherTile = BLACK_TILE

344.     else:

345.         otherTile = WHITE_TILE

346.

347.     tilesToFlip = []

348.     # check each of the eight directions:

349.     for xdirection, ydirection in [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]]:

350.         x, y = xstart, ystart

351.         x += xdirection

352.         y += ydirection

353.         if isOnBoard(x, y) and board[x][y] == otherTile:

354.             # The piece belongs to the other player next to our piece.

355.             x += xdirection

356.             y += ydirection

357.             if not isOnBoard(x, y):

358.                 continue

359.             while board[x][y] == otherTile:

360.                 x += xdirection

361.                 y += ydirection

362.                 if not isOnBoard(x, y):

363.                     break # break out of while loop, continue in for loop

364.             if not isOnBoard(x, y):

365.                 continue

366.             if board[x][y] == tile:

367.                 # There are pieces to flip over. Go in the reverse

368.                 # direction until we reach the original space, noting all

369.                 # the tiles along the way.

370.                 while True:

371.                     x -= xdirection

372.                     y -= ydirection

373.                     if x == xstart and y == ystart:

374.                         break

375.                     tilesToFlip.append([x, y])

376.

377.     board[xstart][ystart] = EMPTY_SPACE # make space empty

378.     if len(tilesToFlip) == 0: # If no tiles flipped, this move is invalid

379.         return False

380.     return tilesToFlip

381.

382.

383. def isOnBoard(x, y):

384.     # Returns True if the coordinates are located on the board.

385.     return x >= 0 and x < BOARDWIDTH and y >= 0 and y < BOARDHEIGHT

386.

387.

388. def getBoardWithValidMoves(board, tile):

389.     # Returns a new board with hint markings.

390.     dupeBoard = copy.deepcopy(board)

391.

392.     for x, y in getValidMoves(dupeBoard, tile):

393.         dupeBoard[x][y] = HINT_TILE

394.     return dupeBoard

395.

396.

397. def getValidMoves(board, tile):

398.     # Returns a list of (x,y) tuples of all valid moves.

399.     validMoves = []

400.

401.     for x in range(BOARDWIDTH):

402.         for y in range(BOARDHEIGHT):

403.             if isValidMove(board, tile, x, y) != False:

404.                 validMoves.append((x, y))

405.     return validMoves

406.

407.

408. def getScoreOfBoard(board):

409.     # Determine the score by counting the tiles.

410.     xscore = 0

411.     oscore = 0

412.     for x in range(BOARDWIDTH):

413.         for y in range(BOARDHEIGHT):

414.             if board[x][y] == WHITE_TILE:

415.                 xscore += 1

416.             if board[x][y] == BLACK_TILE:

417.                 oscore += 1

418.     return {WHITE_TILE:xscore, BLACK_TILE:oscore}

419.

420.

421. def enterPlayerTile():

422.     # Draws the text and handles the mouse click events for letting

423.     # the player choose which color they want to be.  Returns

424.     # [WHITE_TILE, BLACK_TILE] if the player chooses to be White,

425.     # [BLACK_TILE, WHITE_TILE] if Black.

426.

427.     # Create the text.

428.     textSurf = FONT.render('Do you want to be white or black?', True, TEXTCOLOR, TEXTBGCOLOR1)

429.     textRect = textSurf.get_rect()

430.     textRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))

431.

432.     xSurf = BIGFONT.render('White', True, TEXTCOLOR, TEXTBGCOLOR1)

433.     xRect = xSurf.get_rect()

434.     xRect.center = (int(WINDOWWIDTH / 2) - 60, int(WINDOWHEIGHT / 2) + 40)

435.

436.     oSurf = BIGFONT.render('Black', True, TEXTCOLOR, TEXTBGCOLOR1)

437.     oRect = oSurf.get_rect()

438.     oRect.center = (int(WINDOWWIDTH / 2) + 60, int(WINDOWHEIGHT / 2) + 40)

439.

440.     while True:

441.         # Keep looping until the player has clicked on a color.

442.         checkForQuit()

443.         for event in pygame.event.get(): # event handling loop

444.             if event.type == MOUSEBUTTONUP:

445.                 mousex, mousey = event.pos

446.                 if xRect.collidepoint( (mousex, mousey) ):

447.                     return [WHITE_TILE, BLACK_TILE]

448.                 elif oRect.collidepoint( (mousex, mousey) ):

449.                     return [BLACK_TILE, WHITE_TILE]

450.

451.         # Draw the screen.

452.         DISPLAYSURF.blit(textSurf, textRect)

453.         DISPLAYSURF.blit(xSurf, xRect)

454.         DISPLAYSURF.blit(oSurf, oRect)

455.         pygame.display.update()

456.         MAINCLOCK.tick(FPS)

457.

458.

459. def makeMove(board, tile, xstart, ystart, realMove=False):

460.     # Place the tile on the board at xstart, ystart, and flip tiles

461.     # Returns False if this is an invalid move, True if it is valid.

462.     tilesToFlip = isValidMove(board, tile, xstart, ystart)

463.

464.     if tilesToFlip == False:

465.         return False

466.

467.     board[xstart][ystart] = tile

468.

469.     if realMove:

470.         animateTileChange(tilesToFlip, tile, (xstart, ystart))

471.

472.     for x, y in tilesToFlip:

473.         board[x][y] = tile

474.     return True

475.

476.

477. def isOnCorner(x, y):

478.     # Returns True if the position is in one of the four corners.

479.     return (x == 0 and y == 0) or \

480.            (x == BOARDWIDTH and y == 0) or \

481.            (x == 0 and y == BOARDHEIGHT) or \

482.            (x == BOARDWIDTH and y == BOARDHEIGHT)

483.

484.

485. def getComputerMove(board, computerTile):

486.     # Given a board and the computer's tile, determine where to

487.     # move and return that move as a [x, y] list.

488.     possibleMoves = getValidMoves(board, computerTile)

489.

490.     # randomize the order of the possible moves

491.     random.shuffle(possibleMoves)

492.

493.     # always go for a corner if available.

494.     for x, y in possibleMoves:

495.         if isOnCorner(x, y):

496.             return [x, y]

497.

498.     # Go through all possible moves and remember the best scoring move

499.     bestScore = -1

500.     for x, y in possibleMoves:

501.         dupeBoard = copy.deepcopy(board)

502.         makeMove(dupeBoard, computerTile, x, y)

503.         score = getScoreOfBoard(dupeBoard)[computerTile]

504.         if score > bestScore:

505.             bestMove = [x, y]

506.             bestScore = score

507.     return bestMove

508.

509.

510. def checkForQuit():

511.     for event in pygame.event.get((QUIT, KEYUP)): # event handling loop

512.         if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):

513.             pygame.quit()

514.             sys.exit()

515.

516.

517. if __name__ == '__main__':

518.     main()

Ink Spill, a “Flood It” Clone

     

The game “Flood It” begins with a board filled with colored tiles. On each turn the player chooses a new color to paint the top left tile and any tiles adjacent to it of that same color. This game makes use of the flood fill algorithm (described in the Star Pusher chapter). The goal of the game is to turn the entire board into a single color before running out of turns.

This game also has a Settings screen where the player can change the size of the board and the difficulty of the game. If the player gets board of the colors, there are a few other color schemes they can switch to as well.

Source Code for Ink Spill

This source code can be downloaded from http://invpy.com/inkspill.py.

The image files that Flippy uses can be downloaded from http://invpy.com/inkspillimages.zip.

  1. # Ink Spill (a Flood It clone)

  2. # http://inventwithpython.com/pygame

  3. # By Al Sweigart [email protected]

  4. # Released under a "Simplified BSD" license

  5.

  6. import random, sys, webbrowser, copy, pygame

  7. from pygame.locals import *

  8.

  9. # There are different box sizes, number of boxes, and

 10. # life depending on the "board size" setting selected.

 11. SMALLBOXSIZE  = 60 # size is in pixels

 12. MEDIUMBOXSIZE = 20

 13. LARGEBOXSIZE  = 11

 14.

 15. SMALLBOARDSIZE  = 6 # size is in boxes

 16. MEDIUMBOARDSIZE = 17

 17. LARGEBOARDSIZE  = 30

 18.

 19. SMALLMAXLIFE  = 10 # number of turns

 20. MEDIUMMAXLIFE = 30

 21. LARGEMAXLIFE  = 64

 22.

 23. FPS = 30

 24. WINDOWWIDTH = 640

 25. WINDOWHEIGHT = 480

 26. boxSize = MEDIUMBOXSIZE

 27. PALETTEGAPSIZE = 10

 28. PALETTESIZE = 45

 29. EASY = 0   # arbitrary but unique value

 30. MEDIUM = 1 # arbitrary but unique value

 31. HARD = 2   # arbitrary but unique value

 32.

 33. difficulty = MEDIUM # game starts in "medium" mode

 34. maxLife = MEDIUMMAXLIFE

 35. boardWidth = MEDIUMBOARDSIZE

 36. boardHeight = MEDIUMBOARDSIZE

 37.

 38.

 39. #            R    G    B

 40. WHITE    = (255, 255, 255)

 41. DARKGRAY = ( 70,  70,  70)

 42. BLACK    = (  0,   0,   0)

 43. RED      = (255,   0,   0)

 44. GREEN    = (  0, 255,   0)

 45. BLUE     = (  0,   0, 255)

 46. YELLOW   = (255, 255,   0)

 47. ORANGE   = (255, 128,   0)

 48. PURPLE   = (255,   0, 255)

 49.

 50. # The first color in each scheme is the background color, the next six are the palette colors.

 51. COLORSCHEMES = (((150, 200, 255), RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE),

 52.                 ((0, 155, 104),  (97, 215, 164),  (228, 0, 69),  (0, 125, 50),   (204, 246, 0),   (148, 0, 45),    (241, 109, 149)),

 53.                 ((195, 179, 0),  (255, 239, 115), (255, 226, 0), (147, 3, 167),  (24, 38, 176),   (166, 147, 0),   (197, 97, 211)),

 54.                 ((85, 0, 0),     (155, 39, 102),  (0, 201, 13),  (255, 118, 0),  (206, 0, 113),   (0, 130, 9),     (255, 180, 115)),

 55.                 ((191, 159, 64), (183, 182, 208), (4, 31, 183),  (167, 184, 45), (122, 128, 212), (37, 204, 7),    (88, 155, 213)),

 56.                 ((200, 33, 205), (116, 252, 185), (68, 56, 56),  (52, 238, 83),  (23, 149, 195),  (222, 157, 227), (212, 86, 185)))

 57. for i in range(len(COLORSCHEMES)):

 58.     assert len(COLORSCHEMES[i]) == 7, 'Color scheme %s does not have exactly 7 colors.' % (i)

 59. bgColor = COLORSCHEMES[0][0]

 60. paletteColors =  COLORSCHEMES[0][1:]

 61.

 62. def main():

 63.     global FPSCLOCK, DISPLAYSURF, LOGOIMAGE, SPOTIMAGE, SETTINGSIMAGE, SETTINGSBUTTONIMAGE, RESETBUTTONIMAGE

 64.

 65.     pygame.init()

 66.     FPSCLOCK = pygame.time.Clock()

 67.     DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

 68.

 69.     # Load images

 70.     LOGOIMAGE = pygame.image.load('inkspilllogo.png')

 71.     SPOTIMAGE = pygame.image.load('inkspillspot.png')

 72.     SETTINGSIMAGE = pygame.image.load('inkspillsettings.png')

 73.     SETTINGSBUTTONIMAGE = pygame.image.load('inkspillsettingsbutton.png')

 74.     RESETBUTTONIMAGE = pygame.image.load('inkspillresetbutton.png')

 75.

 76.     pygame.display.set_caption('Ink Spill')

 77.     mousex = 0

 78.     mousey = 0

 79.     mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty)

 80.     life = maxLife

 81.     lastPaletteClicked = None

 82.

 83.     while True: # main game loop

 84.         paletteClicked = None

 85.         resetGame = False

 86.

 87.         # Draw the screen.

 88.         DISPLAYSURF.fill(bgColor)

 89.         drawLogoAndButtons()

 90.         drawBoard(mainBoard)

 91.         drawLifeMeter(life)

 92.         drawPalettes()

 93.

 94.         checkForQuit()

 95.         for event in pygame.event.get(): # event handling loop

 96.             if event.type == MOUSEBUTTONUP:

 97.                 mousex, mousey = event.pos

 98.                 if pygame.Rect(WINDOWWIDTH - SETTINGSBUTTONIMAGE.get_width(),

 99.                                WINDOWHEIGHT - SETTINGSBUTTONIMAGE.get_height(),

100.                                SETTINGSBUTTONIMAGE.get_width(),

101.                                SETTINGSBUTTONIMAGE.get_height()).collidepoint(mousex, mousey):

102.                     resetGame = showSettingsScreen() # clicked on Settings button

103.                 elif pygame.Rect(WINDOWWIDTH - RESETBUTTONIMAGE.get_width(),

104.                                  WINDOWHEIGHT - SETTINGSBUTTONIMAGE.get_height() - RESETBUTTONIMAGE.get_height(),

105.                                  RESETBUTTONIMAGE.get_width(),

106.                                  RESETBUTTONIMAGE.get_height()).collidepoint(mousex, mousey):

107.                     resetGame = True # clicked on Reset button

108.                 else:

109.                     # check if a palette button was clicked

110.                     paletteClicked = getColorOfPaletteAt(mousex, mousey)

111.

112.         if paletteClicked != None and paletteClicked != lastPaletteClicked:

113.             # a palette button was clicked that is different from the

114.             # last palette button clicked (this check prevents the player

115.             # from accidentally clicking the same palette twice)

116.             lastPaletteClicked = paletteClicked

117.             floodAnimation(mainBoard, paletteClicked)

118.             life -= 1

119.

120.             resetGame = False

121.             if hasWon(mainBoard):

122.                 for i in range(4): # flash border 4 times

123.                     flashBorderAnimation(WHITE, mainBoard)

124.                 resetGame = True

125.                 pygame.time.wait(2000) # pause so the player can bask in victory

126.             elif life == 0:

127.                 # life is zero, so player has lost

128.                 drawLifeMeter(0)

129.                 pygame.display.update()

130.                 pygame.time.wait(400)

131.                 for i in range(4):

132.                     flashBorderAnimation(BLACK, mainBoard)

133.                 resetGame = True

134.                 pygame.time.wait(2000) # pause so the player can suffer in their defeat

135.

136.         if resetGame:

137.             # start a new game

138.             mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty)

139.             life = maxLife

140.             lastPaletteClicked = None

141.

142.         pygame.display.update()

143.         FPSCLOCK.tick(FPS)

144.

145.

146. def checkForQuit():

147.     # Terminates the program if there are any QUIT or escape key events.

148.     for event in pygame.event.get(QUIT): # get all the QUIT events

149.         pygame.quit() # terminate if any QUIT events are present

150.         sys.exit()

151.     for event in pygame.event.get(KEYUP): # get all the KEYUP events

152.         if event.key == K_ESCAPE:

153.             pygame.quit() # terminate if the KEYUP event was for the Esc key

154.             sys.exit()

155.         pygame.event.post(event) # put the other KEYUP event objects back

156.

157.

158. def hasWon(board):

159.     # if the entire board is the same color, player has won

160.     for x in range(boardWidth):

161.         for y in range(boardHeight):

162.             if board[x][y] != board[0][0]:

163.                 return False # found a different color, player has not won

164.     return True

165.

166.

167. def showSettingsScreen():

168.     global difficulty, boxSize, boardWidth, boardHeight, maxLife, paletteColors, bgColor

169.

170.     # The pixel coordinates in this function were obtained by loading

171.     # the inkspillsettings.png image into a graphics editor and reading

172.     # the pixel coordinates from there. Handy trick.

173.

174.     origDifficulty = difficulty

175.     origBoxSize = boxSize

176.     screenNeedsRedraw = True

177.

178.     while True:

179.         if screenNeedsRedraw:

180.             DISPLAYSURF.fill(bgColor)

181.             DISPLAYSURF.blit(SETTINGSIMAGE, (0,0))

182.

183.             # place the ink spot marker next to the selected difficulty

184.             if difficulty == EASY:

185.                 DISPLAYSURF.blit(SPOTIMAGE, (30, 4))

186.             if difficulty == MEDIUM:

187.                 DISPLAYSURF.blit(SPOTIMAGE, (8, 41))

188.             if difficulty == HARD:

189.                 DISPLAYSURF.blit(SPOTIMAGE, (30, 76))

190.

191.             # place the ink spot marker next to the selected size

192.             if boxSize == SMALLBOXSIZE:

193.                 DISPLAYSURF.blit(SPOTIMAGE, (22, 150))

194.             if boxSize == MEDIUMBOXSIZE:

195.                 DISPLAYSURF.blit(SPOTIMAGE, (11, 185))

196.             if boxSize == LARGEBOXSIZE:

197.                 DISPLAYSURF.blit(SPOTIMAGE, (24, 220))

198.

199.             for i in range(len(COLORSCHEMES)):

200.                 drawColorSchemeBoxes(500, i * 60 + 30, i)

201.

202.             pygame.display.update()

203.

204.         screenNeedsRedraw = False # by default, don't redraw the screen

205.         for event in pygame.event.get(): # event handling loop

206.             if event.type == QUIT:

207.                 pygame.quit()

208.                 sys.exit()

209.             elif event.type == KEYUP:

210.                 if event.key == K_ESCAPE:

211.                     # Esc key on settings screen goes back to game

212.                     return not (origDifficulty == difficulty and origBoxSize == boxSize)

213.             elif event.type == MOUSEBUTTONUP:

214.                 screenNeedsRedraw = True # screen should be redrawn

215.                 mousex, mousey = event.pos # syntactic sugar

216.

217.                 # check for clicks on the difficulty buttons

218.                 if pygame.Rect(74, 16, 111, 30).collidepoint(mousex, mousey):

219.                     difficulty = EASY

220.                 elif pygame.Rect(53, 50, 104, 29).collidepoint(mousex, mousey):

221.                     difficulty = MEDIUM

222.                 elif pygame.Rect(72, 85, 65, 31).collidepoint(mousex, mousey):

223.                     difficulty = HARD

224.

225.                 # check for clicks on the size buttons

226.                 elif pygame.Rect(63, 156, 84, 31).collidepoint(mousex, mousey):

227.                     # small board size setting:

228.                     boxSize = SMALLBOXSIZE

229.                     boardWidth = SMALLBOARDSIZE

230.                     boardHeight = SMALLBOARDSIZE

231.                     maxLife = SMALLMAXLIFE

232.                 elif pygame.Rect(52, 192, 106,32).collidepoint(mousex, mousey):

233.                     # medium board size setting:

234.                     boxSize = MEDIUMBOXSIZE

235.                     boardWidth = MEDIUMBOARDSIZE

236.                     boardHeight = MEDIUMBOARDSIZE

237.                     maxLife = MEDIUMMAXLIFE

238.                 elif pygame.Rect(67, 228, 58, 37).collidepoint(mousex, mousey):

239.                     # large board size setting:

240.                     boxSize = LARGEBOXSIZE

241.                     boardWidth = LARGEBOARDSIZE

242.                     boardHeight = LARGEBOARDSIZE

243.                     maxLife = LARGEMAXLIFE

244.                 elif pygame.Rect(14, 299, 371, 97).collidepoint(mousex, mousey):

245.                     # clicked on the "learn programming" ad

246.                     webbrowser.open('http://inventwithpython.com') # opens a web browser

247.                 elif pygame.Rect(178, 418, 215, 34).collidepoint(mousex, mousey):

248.                     # clicked on the "back to game" button

249.                     return not (origDifficulty == difficulty and origBoxSize == boxSize)

250.

251.                 for i in range(len(COLORSCHEMES)):

252.                     # clicked on a color scheme button

253.                     if pygame.Rect(500, 30 + i * 60, MEDIUMBOXSIZE * 3, MEDIUMBOXSIZE * 2).collidepoint(mousex, mousey):

254.                         bgColor = COLORSCHEMES[i][0]

255.                         paletteColors  = COLORSCHEMES[i][1:]

256.

257.

258. def drawColorSchemeBoxes(x, y, schemeNum):

259.     # Draws the color scheme boxes that appear on the "Settings" screen.

260.     for boxy in range(2):

261.         for boxx in range(3):

262.             pygame.draw.rect(DISPLAYSURF, COLORSCHEMES[schemeNum][3 * boxy + boxx + 1], (x + MEDIUMBOXSIZE * boxx, y + MEDIUMBOXSIZE * boxy, MEDIUMBOXSIZE, MEDIUMBOXSIZE))

263.             if paletteColors == COLORSCHEMES[schemeNum][1:]:

264.                 # put the ink spot next to the selected color scheme

265.                 DISPLAYSURF.blit(SPOTIMAGE, (x - 50, y))

266.

267.

268. def flashBorderAnimation(color, board, animationSpeed=30):

269.     origSurf = DISPLAYSURF.copy()

270.     flashSurf = pygame.Surface(DISPLAYSURF.get_size())

271.     flashSurf = flashSurf.convert_alpha()

272.     for start, end, step in ((0, 256, 1), (255, 0, -1)):

273.         # the first iteration on the outer loop will set the inner loop

274.         # to have transparency go from 0 to 255, the second iteration will

275.         # have it go from 255 to 0. This is the "flash".

276.         for transparency in range(start, end, animationSpeed * step):

277.             DISPLAYSURF.blit(origSurf, (0, 0))

278.             r, g, b = color

279.             flashSurf.fill((r, g, b, transparency))

280.             DISPLAYSURF.blit(flashSurf, (0, 0))

281.             drawBoard(board) # draw board ON TOP OF the transparency layer

282.             pygame.display.update()

283.             FPSCLOCK.tick(FPS)

284.     DISPLAYSURF.blit(origSurf, (0, 0)) # redraw the original surface

285.

286.

287. def floodAnimation(board, paletteClicked, animationSpeed=25):

288.     origBoard = copy.deepcopy(board)

289.     floodFill(board, board[0][0], paletteClicked, 0, 0)

290.

291.     for transparency in range(0, 255, animationSpeed):

292.         # The "new" board slowly become opaque over the original board.

293.         drawBoard(origBoard)

294.         drawBoard(board, transparency)

295.         pygame.display.update()

296.         FPSCLOCK.tick(FPS)

297.

298.

299. def generateRandomBoard(width, height, difficulty=MEDIUM):

300.     # Creates a board data structure with random colors for each box.

301.     board = []

302.     for x in range(width):

303.         column = []

304.         for y in range(height):

305.             column.append(random.randint(0, len(paletteColors) - 1))

306.         board.append(column)

307.

308.     # Make board easier by setting some boxes to same color as a neighbor.

309.

310.     # Determine how many boxes to change.

311.     if difficulty == EASY:

312.         if boxSize == SMALLBOXSIZE:

313.             boxesToChange = 100

314.         else:

315.             boxesToChange = 1500

316.     elif difficulty == MEDIUM:

317.         if boxSize == SMALLBOXSIZE:

318.             boxesToChange = 5

319.         else:

320.             boxesToChange = 200

321.     else:

322.         boxesToChange = 0

323.

324.     # Change neighbor's colors:

325.     for i in range(boxesToChange):

326.         # Randomly choose a box whose color to copy

327.         x = random.randint(1, width-2)

328.         y = random.randint(1, height-2)

329.

330.         # Randomly choose neighbors to change.

331.         direction = random.randint(0, 3)

332.         if direction == 0: # change left and up neighbor

333.             board[x-1][y] == board[x][y]

334.             board[x][y-1] == board[x][y]

335.         elif direction == 1: # change right and down neighbor

336.             board[x+1][y] == board[x][y]

337.             board[x][y+1] == board[x][y]

338.         elif direction == 2: # change right and up neighbor

339.             board[x][y-1] == board[x][y]

340.             board[x+1][y] == board[x][y]

341.         else: # change left and down neighbor

342.             board[x][y+1] == board[x][y]

343.             board[x-1][y] == board[x][y]

344.     return board

345.

346.

347. def drawLogoAndButtons():

348.     # draw the Ink Spill logo and Settings and Reset buttons.

349.     DISPLAYSURF.blit(LOGOIMAGE, (WINDOWWIDTH - LOGOIMAGE.get_width(), 0))

350.     DISPLAYSURF.blit(SETTINGSBUTTONIMAGE, (WINDOWWIDTH - SETTINGSBUTTONIMAGE.get_width(), WINDOWHEIGHT - SETTINGSBUTTONIMAGE.get_height()))

351.     DISPLAYSURF.blit(RESETBUTTONIMAGE, (WINDOWWIDTH - RESETBUTTONIMAGE.get_width(), WINDOWHEIGHT - SETTINGSBUTTONIMAGE.get_height() - RESETBUTTONIMAGE.get_height()))

352.

353.

354. def drawBoard(board, transparency=255):

355.     # The colored squares are drawn to a temporary surface which is then

356.     # drawn to the DISPLAYSURF surface. This is done so we can draw the

357.     # squares with transparency on top of DISPLAYSURF as it currently is.

358.     tempSurf = pygame.Surface(DISPLAYSURF.get_size())

359.     tempSurf = tempSurf.convert_alpha()

360.     tempSurf.fill((0, 0, 0, 0))

361.

362.     for x in range(boardWidth):

363.         for y in range(boardHeight):

364.             left, top = leftTopPixelCoordOfBox(x, y)

365.             r, g, b = paletteColors[board[x][y]]

366.             pygame.draw.rect(tempSurf, (r, g, b, transparency), (left, top, boxSize, boxSize))

367.     left, top = leftTopPixelCoordOfBox(0, 0)

368.     pygame.draw.rect(tempSurf, BLACK, (left-1, top-1, boxSize * boardWidth + 1, boxSize * boardHeight + 1), 1)

369.     DISPLAYSURF.blit(tempSurf, (0, 0))

370.

371.

372. def drawPalettes():

373.     # Draws the six color palettes at the bottom of the screen.

374.     numColors = len(paletteColors)

375.     xmargin = int((WINDOWWIDTH - ((PALETTESIZE * numColors) + (PALETTEGAPSIZE * (numColors - 1)))) / 2)

376.     for i in range(numColors):

377.         left = xmargin + (i * PALETTESIZE) + (i * PALETTEGAPSIZE)

378.         top = WINDOWHEIGHT - PALETTESIZE - 10

379.         pygame.draw.rect(DISPLAYSURF, paletteColors[i], (left, top, PALETTESIZE, PALETTESIZE))

380.         pygame.draw.rect(DISPLAYSURF, bgColor,   (left + 2, top + 2, PALETTESIZE - 4, PALETTESIZE - 4), 2)

381.

382.

383. def drawLifeMeter(currentLife):

384.     lifeBoxSize = int((WINDOWHEIGHT - 40) / maxLife)

385.

386.     # Draw background color of life meter.

387.     pygame.draw.rect(DISPLAYSURF, bgColor, (20, 20, 20, 20 + (maxLife * lifeBoxSize)))

388.

389.     for i in range(maxLife):

390.         if currentLife >= (maxLife - i): # draw a solid red box

391.             pygame.draw.rect(DISPLAYSURF, RED, (20, 20 + (i * lifeBoxSize), 20, lifeBoxSize))

392.         pygame.draw.rect(DISPLAYSURF, WHITE, (20, 20 + (i * lifeBoxSize), 20, lifeBoxSize), 1) # draw white outline

393.

394.

395. def getColorOfPaletteAt(x, y):

396.     # Returns the index of the color in paletteColors that the x and y parameters

397.     # are over. Returns None if x and y are not over any palette.

398.     numColors = len(paletteColors)

399.     xmargin = int((WINDOWWIDTH - ((PALETTESIZE * numColors) + (PALETTEGAPSIZE * (numColors - 1)))) / 2)

400.     top = WINDOWHEIGHT - PALETTESIZE - 10

401.     for i in range(numColors):

402.         # Find out if the mouse click is inside any of the palettes.

403.         left = xmargin + (i * PALETTESIZE) + (i * PALETTEGAPSIZE)

404.         r = pygame.Rect(left, top, PALETTESIZE, PALETTESIZE)

405.         if r.collidepoint(x, y):

406.             return i

407.     return None # no palette exists at these x, y coordinates

408.

409.

410. def floodFill(board, oldColor, newColor, x, y):

411.     # This is the flood fill algorithm.

412.     if oldColor == newColor or board[x][y] != oldColor:

413.         return

414.

415.     board[x][y] = newColor # change the color of the current box

416.

417.     # Make the recursive call for any neighboring boxes:

418.     if x > 0:

419.         floodFill(board, oldColor, newColor, x - 1, y) # on box to the left

420.     if x < boardWidth - 1:

421.         floodFill(board, oldColor, newColor, x + 1, y) # on box to the right

422.     if y > 0:

423.         floodFill(board, oldColor, newColor, x, y - 1) # on box to up

424.     if y < boardHeight - 1:

425.         floodFill(board, oldColor, newColor, x, y + 1) # on box to down

426.

427.

428. def leftTopPixelCoordOfBox(boxx, boxy):

429.     # Returns the x and y of the left-topmost pixel of the xth & yth box.

430.     xmargin = int((WINDOWWIDTH - (boardWidth * boxSize)) / 2)

431.     ymargin = int((WINDOWHEIGHT - (boardHeight * boxSize)) / 2)

432.     return (boxx * boxSize + xmargin, boxy * boxSize + ymargin)

433.

434.

435. if __name__ == '__main__':

436.     main()

Four-In-A-Row, a “Connect Four” Clone

     

The game “Connect Four” has a 7 x 6 board where the players take turns dropping tokens from the top of the board. The tokens will fall from the top of each column and come to rest on the bottom of the board or on top of the topmost token in that column. A player wins when four of their tokens line up in a row either horizontally, vertically, or diagonally.

The AI for this game is pretty good. It simulates every possible move it can make, then simulates every possible move the human player can make in response to each of those moves, and then simulates every possible move it can make in response to that, and then simulates every possible move the human player could make in response to each of those moves! After all that thinking, the computer determines which move is most likely to lead to it winning.

So the computer is kind of tough to beat. I usually lose to it.

Since there are seven possible moves you can make on your turn (unless some columns are full), and seven possible moves the opponent could make, and seven moves in response to that, and seven moves in response to that, that means that on each turn the computer is considering 7 x 7 x 7 x 7 = 2,401 possible moves. You can make the computer consider the game even further by setting the DIFFICULTY constant to a higher number, but when I set to a value larger than 2, the computer takes a long time to calculate its turn.

You can also make the computer easier by setting DIFFICULTY to 1. Then the computer only considers each of its moves and the player’s possible responses to those moves. If you set the DIFFICULTY to 0, then the computer loses all intelligence and simply makes random moves.

Source Code for Four-In-A-Row

This source code can be downloaded from http://invpy.com/fourinarow.py.

The image files that Flippy uses can be downloaded from http://invpy.com/fourinarowimages.zip.

  1. # Four-In-A-Row (a Connect Four clone)

  2. # By Al Sweigart [email protected]

  3. # http://inventwithpython.com/pygame

  4. # Released under a "Simplified BSD" license

  5.

  6. import random, copy, sys, pygame

  7. from pygame.locals import *

  8.

  9. BOARDWIDTH = 7  # how many spaces wide the board is

 10. BOARDHEIGHT = 6 # how many spaces tall the board is

 11. assert BOARDWIDTH >= 4 and BOARDHEIGHT >= 4, 'Board must be at least 4x4.'

 12.

 13. DIFFICULTY = 2 # how many moves to look ahead. (>2 is usually too slow)

 14.

 15. SPACESIZE = 50 # size of the tokens and individual board spaces in pixels

 16.

 17. FPS = 30 # frames per second to update the screen

 18. WINDOWWIDTH = 640 # width of the program's window, in pixels

 19. WINDOWHEIGHT = 480 # height in pixels

 20.

 21. XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * SPACESIZE) / 2)

 22. YMARGIN = int((WINDOWHEIGHT - BOARDHEIGHT * SPACESIZE) / 2)

 23.

 24. BRIGHTBLUE = (0, 50, 255)

 25. WHITE = (255, 255, 255)

 26.

 27. BGCOLOR = BRIGHTBLUE

 28. TEXTCOLOR = WHITE

 29.

 30. RED = 'red'

 31. BLACK = 'black'

 32. EMPTY = None

 33. HUMAN = 'human'

 34. COMPUTER = 'computer'

 35.

 36.

 37. def main():

 38.     global FPSCLOCK, DISPLAYSURF, REDPILERECT, BLACKPILERECT, REDTOKENIMG

 39.     global BLACKTOKENIMG, BOARDIMG, ARROWIMG, ARROWRECT, HUMANWINNERIMG

 40.     global COMPUTERWINNERIMG, WINNERRECT, TIEWINNERIMG

 41.

 42.     pygame.init()

 43.     FPSCLOCK = pygame.time.Clock()

 44.     DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

 45.     pygame.display.set_caption('Four in a Row')

 46.

 47.     REDPILERECT = pygame.Rect(int(SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE)

 48.     BLACKPILERECT = pygame.Rect(WINDOWWIDTH - int(3 * SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE)

 49.     REDTOKENIMG = pygame.image.load('4row_red.png')

 50.     REDTOKENIMG = pygame.transform.smoothscale(REDTOKENIMG, (SPACESIZE, SPACESIZE))

 51.     BLACKTOKENIMG = pygame.image.load('4row_black.png')

 52.     BLACKTOKENIMG = pygame.transform.smoothscale(BLACKTOKENIMG, (SPACESIZE, SPACESIZE))

 53.     BOARDIMG = pygame.image.load('4row_board.png')

 54.     BOARDIMG = pygame.transform.smoothscale(BOARDIMG, (SPACESIZE, SPACESIZE))

 55.

 56.     HUMANWINNERIMG = pygame.image.load('4row_humanwinner.png')

 57.     COMPUTERWINNERIMG = pygame.image.load('4row_computerwinner.png')

 58.     TIEWINNERIMG = pygame.image.load('4row_tie.png')

 59.     WINNERRECT = HUMANWINNERIMG.get_rect()

 60.     WINNERRECT.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))

 61.

 62.     ARROWIMG = pygame.image.load('4row_arrow.png')

 63.     ARROWRECT = ARROWIMG.get_rect()

 64.     ARROWRECT.left = REDPILERECT.right + 10

 65.     ARROWRECT.centery = REDPILERECT.centery

 66.

 67.     isFirstGame = True

 68.

 69.     while True:

 70.         runGame(isFirstGame)

 71.         isFirstGame = False

 72.

 73.

 74. def runGame(isFirstGame):

 75.     if isFirstGame:

 76.         # Let the computer go first on the first game, so the player

 77.         # can see how the tokens are dragged from the token piles.

 78.         turn = COMPUTER

 79.         showHelp = True

 80.     else:

 81.         # Randomly choose who goes first.

 82.         if random.randint(0, 1) == 0:

 83.             turn = COMPUTER

 84.         else:

 85.             turn = HUMAN

 86.         showHelp = False

 87.

 88.     # Set up a blank board data structure.

 89.     mainBoard = getNewBoard()

 90.

 91.     while True: # main game loop

 92.         if turn == HUMAN:

 93.             # Human player's turn.

 94.             getHumanMove(mainBoard, showHelp)

 95.             if showHelp:

 96.                 # turn off help arrow after the first move

 97.                 showHelp = False

 98.             if isWinner(mainBoard, RED):

 99.                 winnerImg = HUMANWINNERIMG

100.                 break

101.             turn = COMPUTER # switch to other player's turn

102.         else:

103.             # Computer player's turn.

104.             column = getComputerMove(mainBoard)

105.             animateComputerMoving(mainBoard, column)

106.             makeMove(mainBoard, BLACK, column)

107.             if isWinner(mainBoard, BLACK):

108.                 winnerImg = COMPUTERWINNERIMG

109.                 break

110.             turn = HUMAN # switch to other player's turn

111.

112.         if isBoardFull(mainBoard):

113.             # A completely filled board means it's a tie.

114.             winnerImg = TIEWINNERIMG

115.             break

116.

117.     while True:

118.         # Keep looping until player clicks the mouse or quits.

119.         drawBoard(mainBoard)

120.         DISPLAYSURF.blit(winnerImg, WINNERRECT)

121.         pygame.display.update()

122.         FPSCLOCK.tick()

123.         for event in pygame.event.get(): # event handling loop

124.             if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):

125.                 pygame.quit()

126.                 sys.exit()

127.             elif event.type == MOUSEBUTTONUP:

128.                 return

129.

130.

131. def makeMove(board, player, column):

132.     lowest = getLowestEmptySpace(board, column)

133.     if lowest != -1:

134.         board[column][lowest] = player

135.

136.

137. def drawBoard(board, extraToken=None):

138.     DISPLAYSURF.fill(BGCOLOR)

139.

140.     # draw tokens

141.     spaceRect = pygame.Rect(0, 0, SPACESIZE, SPACESIZE)

142.     for x in range(BOARDWIDTH):

143.         for y in range(BOARDHEIGHT):

144.             spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE))

145.             if board[x][y] == RED:

146.                 DISPLAYSURF.blit(REDTOKENIMG, spaceRect)

147.             elif board[x][y] == BLACK:

148.                 DISPLAYSURF.blit(BLACKTOKENIMG, spaceRect)

149.

150.     # draw the extra token

151.     if extraToken != None:

152.         if extraToken['color'] == RED:

153.             DISPLAYSURF.blit(REDTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE))

154.         elif extraToken['color'] == BLACK:

155.             DISPLAYSURF.blit(BLACKTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE))

156.

157.     # draw board over the tokens

158.     for x in range(BOARDWIDTH):

159.         for y in range(BOARDHEIGHT):

160.             spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE))

161.             DISPLAYSURF.blit(BOARDIMG, spaceRect)

162.

163.     # draw the red and black tokens off to the side

164.     DISPLAYSURF.blit(REDTOKENIMG, REDPILERECT) # red on the left

165.     DISPLAYSURF.blit(BLACKTOKENIMG, BLACKPILERECT) # black on the right

166.

167.

168. def getNewBoard():

169.     board = []

170.     for x in range(BOARDWIDTH):

171.         board.append([EMPTY] * BOARDHEIGHT)

172.     return board

173.

174.

175. def getHumanMove(board, isFirstMove):

176.     draggingToken = False

177.     tokenx, tokeny = None, None

178.     while True:

179.         for event in pygame.event.get(): # event handling loop

180.             if event.type == QUIT:

181.                 pygame.quit()

182.                 sys.exit()

183.             elif event.type == MOUSEBUTTONDOWN and not draggingToken and REDPILERECT.collidepoint(event.pos):

184.                 # start of dragging on red token pile.

185.                 draggingToken = True

186.                 tokenx, tokeny = event.pos

187.             elif event.type == MOUSEMOTION and draggingToken:

188.                 # update the position of the red token being dragged

189.                 tokenx, tokeny = event.pos

190.             elif event.type == MOUSEBUTTONUP and draggingToken:

191.                 # let go of the token being dragged

192.                 if tokeny < YMARGIN and tokenx > XMARGIN and tokenx < WINDOWWIDTH - XMARGIN:

193.                     # let go at the top of the screen.

194.                     column = int((tokenx - XMARGIN) / SPACESIZE)

195.                     if isValidMove(board, column):

196.                         animateDroppingToken(board, column, RED)

197.                         board[column][getLowestEmptySpace(board, column)] = RED

198.                         drawBoard(board)

199.                         pygame.display.update()

200.                         return

201.                 tokenx, tokeny = None, None

202.                 draggingToken = False

203.         if tokenx != None and tokeny != None:

204.             drawBoard(board, {'x':tokenx - int(SPACESIZE / 2), 'y':tokeny - int(SPACESIZE / 2), 'color':RED})

205.         else:

206.             drawBoard(board)

207.

208.         if isFirstMove:

209.             # Show the help arrow for the player's first move.

210.             DISPLAYSURF.blit(ARROWIMG, ARROWRECT)

211.

212.         pygame.display.update()

213.         FPSCLOCK.tick()

214.

215.

216. def animateDroppingToken(board, column, color):

217.     x = XMARGIN + column * SPACESIZE

218.     y = YMARGIN - SPACESIZE

219.     dropSpeed = 1.0

220.

221.     lowestEmptySpace = getLowestEmptySpace(board, column)

222.

223.     while True:

224.         y += int(dropSpeed)

225.         dropSpeed += 0.5

226.         if int((y - YMARGIN) / SPACESIZE) >= lowestEmptySpace:

227.             return

228.         drawBoard(board, {'x':x, 'y':y, 'color':color})

229.         pygame.display.update()

230.         FPSCLOCK.tick()

231.

232.

233. def animateComputerMoving(board, column):

234.     x = BLACKPILERECT.left

235.     y = BLACKPILERECT.top

236.     speed = 1.0

237.     # moving the black tile up

238.     while y > (YMARGIN - SPACESIZE):

239.         y -= int(speed)

240.         speed += 0.5

241.         drawBoard(board, {'x':x, 'y':y, 'color':BLACK})

242.         pygame.display.update()

243.         FPSCLOCK.tick()

244.     # moving the black tile over

245.     y = YMARGIN - SPACESIZE

246.     speed = 1.0

247.     while x > (XMARGIN + column * SPACESIZE):

248.         x -= int(speed)

249.         speed += 0.5

250.         drawBoard(board, {'x':x, 'y':y, 'color':BLACK})

251.         pygame.display.update()

252.         FPSCLOCK.tick()

253.     # dropping the black tile

254.     animateDroppingToken(board, column, BLACK)

255.

256.

257. def getComputerMove(board):

258.     potentialMoves = getPotentialMoves(board, BLACK, DIFFICULTY)

259.     # get the best fitness from the potential moves

260.     bestMoveFitness = -1

261.     for i in range(BOARDWIDTH):

262.         if potentialMoves[i] > bestMoveFitness and isValidMove(board, i):

263.             bestMoveFitness = potentialMoves[i]

264.     # find all potential moves that have this best fitness

265.     bestMoves = []

266.     for i in range(len(potentialMoves)):

267.         if potentialMoves[i] == bestMoveFitness and isValidMove(board, i):

268.             bestMoves.append(i)

269.     return random.choice(bestMoves)

270.

271.

272. def getPotentialMoves(board, tile, lookAhead):

273.     if lookAhead == 0 or isBoardFull(board):

274.         return [0] * BOARDWIDTH

275.

276.     if tile == RED:

277.         enemyTile = BLACK

278.     else:

279.         enemyTile = RED

280.

281.     # Figure out the best move to make.

282.     potentialMoves = [0] * BOARDWIDTH

283.     for firstMove in range(BOARDWIDTH):

284.         dupeBoard = copy.deepcopy(board)

285.         if not isValidMove(dupeBoard, firstMove):

286.             continue

287.         makeMove(dupeBoard, tile, firstMove)

288.         if isWinner(dupeBoard, tile):

289.             # a winning move automatically gets a perfect fitness

290.             potentialMoves[firstMove] = 1

291.             break # don't bother calculating other moves

292.         else:

293.             # do other player's counter moves and determine best one

294.             if isBoardFull(dupeBoard):

295.                 potentialMoves[firstMove] = 0

296.             else:

297.                 for counterMove in range(BOARDWIDTH):

298.                     dupeBoard2 = copy.deepcopy(dupeBoard)

299.                     if not isValidMove(dupeBoard2, counterMove):

300.                         continue

301.                     makeMove(dupeBoard2, enemyTile, counterMove)

302.                     if isWinner(dupeBoard2, enemyTile):

303.                         # a losing move automatically gets the worst fitness

304.                         potentialMoves[firstMove] = -1

305.                         break

306.                     else:

307.                         # do the recursive call to getPotentialMoves()

308.                         results = getPotentialMoves(dupeBoard2, tile, lookAhead - 1)

309.                         potentialMoves[firstMove] += (sum(results) / BOARDWIDTH) / BOARDWIDTH

310.     return potentialMoves

311.

312.

313. def getLowestEmptySpace(board, column):

314.     # Return the row number of the lowest empty row in the given column.

315.     for y in range(BOARDHEIGHT-1, -1, -1):

316.         if board[column][y] == EMPTY:

317.             return y

318.     return -1

319.

320.

321. def isValidMove(board, column):

322.     # Returns True if there is an empty space in the given column.

323.     # Otherwise returns False.

324.     if column < 0 or column >= (BOARDWIDTH) or board[column][0] != EMPTY:

325.         return False

326.     return True

327.

328.

329. def isBoardFull(board):

330.     # Returns True if there are no empty spaces anywhere on the board.

331.     for x in range(BOARDWIDTH):

332.         for y in range(BOARDHEIGHT):

333.             if board[x][y] == EMPTY:

334.                 return False

335.     return True

336.

337.

338. def isWinner(board, tile):

339.     # check horizontal spaces

340.     for x in range(BOARDWIDTH - 3):

341.         for y in range(BOARDHEIGHT):

342.             if board[x][y] == tile and board[x+1][y] == tile and board[x+2][y] == tile and board[x+3][y] == tile:

343.                 return True

344.     # check vertical spaces

345.     for x in range(BOARDWIDTH):

346.         for y in range(BOARDHEIGHT - 3):

347.             if board[x][y] == tile and board[x][y+1] == tile and board[x][y+2] == tile and board[x][y+3] == tile:

348.                 return True

349.     # check / diagonal spaces

350.     for x in range(BOARDWIDTH - 3):

351.         for y in range(3, BOARDHEIGHT):

352.             if board[x][y] == tile and board[x+1][y-1] == tile and board[x+2][y-2] == tile and board[x+3][y-3] == tile:

353.                 return True

354.     # check \ diagonal spaces

355.     for x in range(BOARDWIDTH - 3):

356.         for y in range(BOARDHEIGHT - 3):

357.             if board[x][y] == tile and board[x+1][y+1] == tile and board[x+2][y+2] == tile and board[x+3][y+3] == tile:

358.                 return True

359.     return False

360.

361.

362. if __name__ == '__main__':

363.     main()

 

 


 

Gemgem, a “Bejeweled” Clone

   

“Bejeweled” is a game where gems fall to fill up a board. The player can swap any two adjacent gems to try to match three gems in a row (vertically or horizontally, but not diagonally). The matched gems then disappear, making way for new gems to fall from the top. Matching more than three gems, or causing a chain reaction of gem matches will result in more points. The player’s score slowly drops over time, so the player must constantly be making new matches. The game ends when no possible match can be made on the board.

Source Code for Gemgem

This source code can be downloaded from http://invpy.com/gemgem.py.

The image files that Flippy uses can be downloaded from http://invpy.com/gemgemimages.zip.

  1. # Gemgem (a Bejeweled clone)

  2. # By Al Sweigart [email protected]

  3. # http://inventwithpython.com/pygame

  4. # Released under a "Simplified BSD" license

  5.

  6. """

  7. This program has "gem data structures", which are basically dictionaries

  8. with the following keys:

  9.   'x' and 'y' - The location of the gem on the board. 0,0 is the top left.

 10.                 There is also a ROWABOVEBOARD row that 'y' can be set to,

 11.                 to indicate that it is above the board.

 12.   'direction' - one of the four constant variables UP, DOWN, LEFT, RIGHT.

 13.                 This is the direction the gem is moving.

 14.   'imageNum'  - The integer index into GEMIMAGES to denote which image

 15.                 this gem uses.

 16. """

 17.

 18. import random, time, pygame, sys, copy

 19. from pygame.locals import *

 20.

 21. FPS = 30 # frames per second to update the screen

 22. WINDOWWIDTH = 600  # width of the program's window, in pixels

 23. WINDOWHEIGHT = 600 # height in pixels

 24.

 25. BOARDWIDTH = 8 # how many columns in the board

 26. BOARDHEIGHT = 8 # how many rows in the board

 27. GEMIMAGESIZE = 64 # width & height of each space in pixels

 28.

 29. # NUMGEMIMAGES is the number of gem types. You will need .png image

 30. # files named gem0.png, gem1.png, etc. up to gem(N-1).png.

 31. NUMGEMIMAGES = 7

 32. assert NUMGEMIMAGES >= 5 # game needs at least 5 types of gems to work

 33.

 34. # NUMMATCHSOUNDS is the number of different sounds to choose from when

 35. # a match is made. The .wav files are named match0.wav, match1.wav, etc.

 36. NUMMATCHSOUNDS = 6

 37.

 38. MOVERATE = 25 # 1 to 100, larger num means faster animations

 39. DEDUCTSPEED = 0.8 # reduces score by 1 point every DEDUCTSPEED seconds.

 40.

 41. #             R    G    B

 42. PURPLE    = (255,   0, 255)

 43. LIGHTBLUE = (170, 190, 255)

 44. BLUE      = (  0,   0, 255)

 45. RED       = (255, 100, 100)

 46. BLACK     = (  0,   0,   0)

 47. BROWN     = ( 85,  65,   0)

 48. HIGHLIGHTCOLOR = PURPLE # color of the selected gem's border

 49. BGCOLOR = LIGHTBLUE # background color on the screen

 50. GRIDCOLOR = BLUE # color of the game board

 51. GAMEOVERCOLOR = RED # color of the "Game over" text.

 52. GAMEOVERBGCOLOR = BLACK # background color of the "Game over" text.

 53. SCORECOLOR = BROWN # color of the text for the player's score

 54.

 55. # The amount of space to the sides of the board to the edge of the window

 56. # is used several times, so calculate it once here and store in variables.

 57. XMARGIN = int((WINDOWWIDTH - GEMIMAGESIZE * BOARDWIDTH) / 2)

 58. YMARGIN = int((WINDOWHEIGHT - GEMIMAGESIZE * BOARDHEIGHT) / 2)

 59.

 60. # constants for direction values

 61. UP = 'up'

 62. DOWN = 'down'

 63. LEFT = 'left'

 64. RIGHT = 'right'

 65.

 66. EMPTY_SPACE = -1 # an arbitrary, nonpositive value

 67. ROWABOVEBOARD = 'row above board' # an arbitrary, noninteger value

 68.

 69. def main():

 70.     global FPSCLOCK, DISPLAYSURF, GEMIMAGES, GAMESOUNDS, BASICFONT, BOARDRECTS

 71.

 72.     # Initial set up.

 73.     pygame.init()

 74.     FPSCLOCK = pygame.time.Clock()

 75.     DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

 76.     pygame.display.set_caption('Gemgem')

 77.     BASICFONT = pygame.font.Font('freesansbold.ttf', 36)

 78.

 79.     # Load the images

 80.     GEMIMAGES = []

 81.     for i in range(1, NUMGEMIMAGES+1):

 82.         gemImage = pygame.image.load('gem%s.png' % i)

 83.         if gemImage.get_size() != (GEMIMAGESIZE, GEMIMAGESIZE):

 84.             gemImage = pygame.transform.smoothscale(gemImage, (GEMIMAGESIZE, GEMIMAGESIZE))

 85.         GEMIMAGES.append(gemImage)

 86.

 87.     # Load the sounds.

 88.     GAMESOUNDS = {}

 89.     GAMESOUNDS['bad swap'] = pygame.mixer.Sound('badswap.wav')

 90.     GAMESOUNDS['match'] = []

 91.     for i in range(NUMMATCHSOUNDS):

 92.         GAMESOUNDS['match'].append(pygame.mixer.Sound('match%s.wav' % i))

 93.

 94.     # Create pygame.Rect objects for each board space to

 95.     # do board-coordinate-to-pixel-coordinate conversions.

 96.     BOARDRECTS = []

 97.     for x in range(BOARDWIDTH):

 98.         BOARDRECTS.append([])

 99.         for y in range(BOARDHEIGHT):

100.             r = pygame.Rect((XMARGIN + (x * GEMIMAGESIZE),

101.                              YMARGIN + (y * GEMIMAGESIZE),

102.                              GEMIMAGESIZE,

103.                              GEMIMAGESIZE))

104.             BOARDRECTS[x].append(r)

105.

106.     while True:

107.         runGame()

108.

109.

110. def runGame():

111.     # Plays through a single game. When the game is over, this function returns.

112.

113.     # initialize the board

114.     gameBoard = getBlankBoard()

115.     score = 0

116.     fillBoardAndAnimate(gameBoard, [], score) # Drop the initial gems.

117.

118.     # initialize variables for the start of a new game

119.     firstSelectedGem = None

120.     lastMouseDownX = None

121.     lastMouseDownY = None

122.     gameIsOver = False

123.     lastScoreDeduction = time.time()

124.     clickContinueTextSurf = None

125.

126.     while True: # main game loop

127.         clickedSpace = None

128.         for event in pygame.event.get(): # event handling loop

129.             if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):

130.                 pygame.quit()

131.                 sys.exit()

132.             elif event.type == KEYUP and event.key == K_BACKSPACE:

133.                 return # start a new game

134.

135.             elif event.type == MOUSEBUTTONUP:

136.                 if gameIsOver:

137.                     return # after games ends, click to start a new game

138.

139.                 if event.pos == (lastMouseDownX, lastMouseDownY):

140.                     # This event is a mouse click, not the end of a mouse drag.

141.                     clickedSpace = checkForGemClick(event.pos)

142.                 else:

143.                     # this is the end of a mouse drag

144.                     firstSelectedGem = checkForGemClick((lastMouseDownX, lastMouseDownY))

145.                     clickedSpace = checkForGemClick(event.pos)

146.                     if not firstSelectedGem or not clickedSpace:

147.                         # if not part of a valid drag, deselect both

148.                         firstSelectedGem = None

149.                         clickedSpace = None

150.             elif event.type == MOUSEBUTTONDOWN:

151.                 # this is the start of a mouse click or mouse drag

152.                 lastMouseDownX, lastMouseDownY = event.pos

153.

154.         if clickedSpace and not firstSelectedGem:

155.             # This was the first gem clicked on.

156.             firstSelectedGem = clickedSpace

157.         elif clickedSpace and firstSelectedGem:

158.             # Two gems have been clicked on and selected. Swap the gems.

159.             firstSwappingGem, secondSwappingGem = getSwappingGems(gameBoard, firstSelectedGem, clickedSpace)

160.             if firstSwappingGem == None and secondSwappingGem == None:

161.                 # If both are None, then the gems were not adjacent

162.                 firstSelectedGem = None # deselect the first gem

163.                 continue

164.

165.             # Show the swap animation on the screen.

166.             boardCopy = getBoardCopyMinusGems(gameBoard, (firstSwappingGem, secondSwappingGem))

167.             animateMovingGems(boardCopy, [firstSwappingGem, secondSwappingGem], [], score)

168.

169.             # Swap the gems in the board data structure.

170.             gameBoard[firstSwappingGem['x']][firstSwappingGem['y']] = secondSwappingGem['imageNum']

171.             gameBoard[secondSwappingGem['x']][secondSwappingGem['y']] = firstSwappingGem['imageNum']

172.

173.             # See if this is a matching move.

174.             matchedGems = findMatchingGems(gameBoard)

175.             if matchedGems == []:

176.                 # Was not a matching move; swap the gems back

177.                 GAMESOUNDS['bad swap'].play()

178.                 animateMovingGems(boardCopy, [firstSwappingGem, secondSwappingGem], [], score)

179.                 gameBoard[firstSwappingGem['x']][firstSwappingGem['y']] = firstSwappingGem['imageNum']

180.                 gameBoard[secondSwappingGem['x']][secondSwappingGem['y']] = secondSwappingGem['imageNum']

181.             else:

182.                 # This was a matching move.

183.                 scoreAdd = 0

184.                 while matchedGems != []:

185.                     # Remove matched gems, then pull down the board.

186.

187.                     # points is a list of dicts that tells fillBoardAndAnimate()

188.                     # where on the screen to display text to show how many

189.                     # points the player got. points is a list because if

190.                     # the player gets multiple matches, then multiple points text should appear.

191.                     points = []

192.                     for gemSet in matchedGems:

193.                         scoreAdd += (10 + (len(gemSet) - 3) * 10)

194.                         for gem in gemSet:

195.                             gameBoard[gem[0]][gem[1]] = EMPTY_SPACE

196.                         points.append({'points': scoreAdd,

197.                                        'x': gem[0] * GEMIMAGESIZE + XMARGIN,

198.                                        'y': gem[1] * GEMIMAGESIZE + YMARGIN})

199.                     random.choice(GAMESOUNDS['match']).play()

200.                     score += scoreAdd

201.

202.                     # Drop the new gems.

203.                     fillBoardAndAnimate(gameBoard, points, score)

204.

205.                     # Check if there are any new matches.

206.                     matchedGems = findMatchingGems(gameBoard)

207.             firstSelectedGem = None

208.

209.             if not canMakeMove(gameBoard):

210.                 gameIsOver = True

211.

212.         # Draw the board.

213.         DISPLAYSURF.fill(BGCOLOR)

214.         drawBoard(gameBoard)

215.         if firstSelectedGem != None:

216.             highlightSpace(firstSelectedGem['x'], firstSelectedGem['y'])

217.         if gameIsOver:

218.             if clickContinueTextSurf == None:

219.                 # Only render the text once. In future iterations, just

220.                 # use the Surface object already in clickContinueTextSurf

221.                 clickContinueTextSurf = BASICFONT.render('Final Score: %s (Click to continue)' % (score), 1, GAMEOVERCOLOR, GAMEOVERBGCOLOR)

222.                 clickContinueTextRect = clickContinueTextSurf.get_rect()

223.                 clickContinueTextRect.center = int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2)

224.             DISPLAYSURF.blit(clickContinueTextSurf, clickContinueTextRect)

225.         elif score > 0 and time.time() - lastScoreDeduction > DEDUCTSPEED:

226.             # score drops over time

227.             score -= 1

228.             lastScoreDeduction = time.time()

229.         drawScore(score)

230.         pygame.display.update()

231.         FPSCLOCK.tick(FPS)

232.

233.

234. def getSwappingGems(board, firstXY, secondXY):

235.     # If the gems at the (X, Y) coordinates of the two gems are adjacent,

236.     # then their 'direction' keys are set to the appropriate direction

237.     # value to be swapped with each other.

238.     # Otherwise, (None, None) is returned.

239.     firstGem = {'imageNum': board[firstXY['x']][firstXY['y']],

240.                 'x': firstXY['x'],

241.                 'y': firstXY['y']}

242.     secondGem = {'imageNum': board[secondXY['x']][secondXY['y']],

243.                  'x': secondXY['x'],

244.                  'y': secondXY['y']}

245.     highlightedGem = None

246.     if firstGem['x'] == secondGem['x'] + 1 and firstGem['y'] == secondGem['y']:

247.         firstGem['direction'] = LEFT

248.         secondGem['direction'] = RIGHT

249.     elif firstGem['x'] == secondGem['x'] - 1 and firstGem['y'] == secondGem['y']:

250.         firstGem['direction'] = RIGHT

251.         secondGem['direction'] = LEFT

252.     elif firstGem['y'] == secondGem['y'] + 1 and firstGem['x'] == secondGem['x']:

253.         firstGem['direction'] = UP

254.         secondGem['direction'] = DOWN

255.     elif firstGem['y'] == secondGem['y'] - 1 and firstGem['x'] == secondGem['x']:

256.         firstGem['direction'] = DOWN

257.         secondGem['direction'] = UP

258.     else:

259.         # These gems are not adjacent and can't be swapped.

260.         return None, None

261.     return firstGem, secondGem

262.

263.

264. def getBlankBoard():

265.     # Create and return a blank board data structure.

266.     board = []

267.     for x in range(BOARDWIDTH):

268.         board.append([EMPTY_SPACE] * BOARDHEIGHT)

269.     return board

270.

271.

272. def canMakeMove(board):

273.     # Return True if the board is in a state where a matching

274.     # move can be made on it. Otherwise return False.

275.

276.     # The patterns in oneOffPatterns represent gems that are configured

277.     # in a way where it only takes one move to make a triplet.

278.     oneOffPatterns = (((0,1), (1,0), (2,0)),

279.                       ((0,1), (1,1), (2,0)),

280.                       ((0,0), (1,1), (2,0)),

281.                       ((0,1), (1,0), (2,1)),

282.                       ((0,0), (1,0), (2,1)),

283.                       ((0,0), (1,1), (2,1)),

284.                       ((0,0), (0,2), (0,3)),

285.                       ((0,0), (0,1), (0,3)))

286.

287.     # The x and y variables iterate over each space on the board.

288.     # If we use + to represent the currently iterated space on the

289.     # board, then this pattern: ((0,1), (1,0), (2,0))refers to identical

290.     # gems being set up like this:

291.     #

292.     #     +A

293.     #     B

294.     #     C

295.     #

296.     # That is, gem A is offset from the + by (0,1), gem B is offset

297.     # by (1,0), and gem C is offset by (2,0). In this case, gem A can

298.     # be swapped to the left to form a vertical three-in-a-row triplet.

299.     #

300.     # There are eight possible ways for the gems to be one move

301.     # away from forming a triple, hence oneOffPattern has 8 patterns.

302.

303.     for x in range(BOARDWIDTH):

304.         for y in range(BOARDHEIGHT):

305.             for pat in oneOffPatterns:

306.                 # check each possible pattern of "match in next move" to

307.                 # see if a possible move can be made.

308.                 if (getGemAt(board, x+pat[0][0], y+pat[0][1]) == \

309.                     getGemAt(board, x+pat[1][0], y+pat[1][1]) == \

310.                     getGemAt(board, x+pat[2][0], y+pat[2][1]) != None) or \

311.                    (getGemAt(board, x+pat[0][1], y+pat[0][0]) == \

312.                     getGemAt(board, x+pat[1][1], y+pat[1][0]) == \

313.                     getGemAt(board, x+pat[2][1], y+pat[2][0]) != None):

314.                     return True # return True the first time you find a pattern

315.     return False

316.

317.

318. def drawMovingGem(gem, progress):

319.     # Draw a gem sliding in the direction that its 'direction' key

320.     # indicates. The progress parameter is a number from 0 (just

321.     # starting) to 100 (slide complete).

322.     movex = 0

323.     movey = 0

324.     progress *= 0.01

325.

326.     if gem['direction'] == UP:

327.         movey = -int(progress * GEMIMAGESIZE)

328.     elif gem['direction'] == DOWN:

329.         movey = int(progress * GEMIMAGESIZE)

330.     elif gem['direction'] == RIGHT:

331.         movex = int(progress * GEMIMAGESIZE)

332.     elif gem['direction'] == LEFT:

333.         movex = -int(progress * GEMIMAGESIZE)

334.

335.     basex = gem['x']

336.     basey = gem['y']

337.     if basey == ROWABOVEBOARD:

338.         basey = -1

339.

340.     pixelx = XMARGIN + (basex * GEMIMAGESIZE)

341.     pixely = YMARGIN + (basey * GEMIMAGESIZE)

342.     r = pygame.Rect( (pixelx + movex, pixely + movey, GEMIMAGESIZE, GEMIMAGESIZE) )

343.     DISPLAYSURF.blit(GEMIMAGES[gem['imageNum']], r)

344.

345.

346. def pullDownAllGems(board):

347.     # pulls down gems on the board to the bottom to fill in any gaps

348.     for x in range(BOARDWIDTH):

349.         gemsInColumn = []

350.         for y in range(BOARDHEIGHT):

351.             if board[x][y] != EMPTY_SPACE:

352.                 gemsInColumn.append(board[x][y])

353.         board[x] = ([EMPTY_SPACE] * (BOARDHEIGHT - len(gemsInColumn))) + gemsInColumn

354.

355.

356. def getGemAt(board, x, y):

357.     if x < 0 or y < 0 or x >= BOARDWIDTH or y >= BOARDHEIGHT:

358.         return None

359.     else:

360.         return board[x][y]

361.

362.

363. def getDropSlots(board):

364.     # Creates a "drop slot" for each column and fills the slot with a

365.     # number of gems that that column is lacking. This function assumes

366.     # that the gems have been gravity dropped already.

367.     boardCopy = copy.deepcopy(board)

368.     pullDownAllGems(boardCopy)

369.

370.     dropSlots = []

371.     for i in range(BOARDWIDTH):

372.         dropSlots.append([])

373.

374.     # count the number of empty spaces in each column on the board

375.     for x in range(BOARDWIDTH):

376.         for y in range(BOARDHEIGHT-1, -1, -1): # start from bottom, going up

377.             if boardCopy[x][y] == EMPTY_SPACE:

378.                 possibleGems = list(range(len(GEMIMAGES)))

379.                 for offsetX, offsetY in ((0, -1), (1, 0), (0, 1), (-1, 0)):

380.                     # Narrow down the possible gems we should put in the

381.                     # blank space so we don't end up putting an two of

382.                     # the same gems next to each other when they drop.

383.                     neighborGem = getGemAt(boardCopy, x + offsetX, y + offsetY)

384.                     if neighborGem != None and neighborGem in possibleGems:

385.                         possibleGems.remove(neighborGem)

386.

387.                 newGem = random.choice(possibleGems)

388.                 boardCopy[x][y] = newGem

389.                 dropSlots[x].append(newGem)

390.     return dropSlots

391.

392.

393. def findMatchingGems(board):

394.     gemsToRemove = [] # a list of lists of gems in matching triplets that should be removed

395.     boardCopy = copy.deepcopy(board)

396.

397.     # loop through each space, checking for 3 adjacent identical gems

398.     for x in range(BOARDWIDTH):

399.         for y in range(BOARDHEIGHT):

400.             # look for horizontal matches

401.             if getGemAt(boardCopy, x, y) == getGemAt(boardCopy, x + 1, y) == getGemAt(boardCopy, x + 2, y) and getGemAt(boardCopy, x, y) != EMPTY_SPACE:

402.                 targetGem = boardCopy[x][y]

403.                 offset = 0

404.                 removeSet = []

405.                 while getGemAt(boardCopy, x + offset, y) == targetGem:

406.                     # keep checking, in case there's more than 3 gems in a row

407.                     removeSet.append((x + offset, y))

408.                     boardCopy[x + offset][y] = EMPTY_SPACE

409.                     offset += 1

410.                 gemsToRemove.append(removeSet)

411.

412.             # look for vertical matches

413.             if getGemAt(boardCopy, x, y) == getGemAt(boardCopy, x, y + 1) == getGemAt(boardCopy, x, y + 2) and getGemAt(boardCopy, x, y) != EMPTY_SPACE:

414.                 targetGem = boardCopy[x][y]

415.                 offset = 0

416.                 removeSet = []

417.                 while getGemAt(boardCopy, x, y + offset) == targetGem:

418.                     # keep checking if there's more than 3 gems in a row

419.                     removeSet.append((x, y + offset))

420.                     boardCopy[x][y + offset] = EMPTY_SPACE

421.                     offset += 1

422.                 gemsToRemove.append(removeSet)

423.

424.     return gemsToRemove

425.

426.

427. def highlightSpace(x, y):

428.     pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, BOARDRECTS[x][y], 4)

429.

430.

431. def getDroppingGems(board):

432.     # Find all the gems that have an empty space below them

433.     boardCopy = copy.deepcopy(board)

434.     droppingGems = []

435.     for x in range(BOARDWIDTH):

436.         for y in range(BOARDHEIGHT - 2, -1, -1):

437.             if boardCopy[x][y + 1] == EMPTY_SPACE and boardCopy[x][y] != EMPTY_SPACE:

438.                 # This space drops if not empty but the space below it is

439.                 droppingGems.append( {'imageNum': boardCopy[x][y], 'x': x, 'y': y, 'direction': DOWN} )

440.                 boardCopy[x][y] = EMPTY_SPACE

441.     return droppingGems

442.

443.

444. def animateMovingGems(board, gems, pointsText, score):

445.     # pointsText is a dictionary with keys 'x', 'y', and 'points'

446.     progress = 0 # progress at 0 represents beginning, 100 means finished.

447.     while progress < 100: # animation loop

448.         DISPLAYSURF.fill(BGCOLOR)

449.         drawBoard(board)

450.         for gem in gems: # Draw each gem.

451.             drawMovingGem(gem, progress)

452.         drawScore(score)

453.         for pointText in pointsText:

454.             pointsSurf = BASICFONT.render(str(pointText['points']), 1, SCORECOLOR)

455.             pointsRect = pointsSurf.get_rect()

456.             pointsRect.center = (pointText['x'], pointText['y'])

457.             DISPLAYSURF.blit(pointsSurf, pointsRect)

458.

459.         pygame.display.update()

460.         FPSCLOCK.tick(FPS)

461.         progress += MOVERATE # progress the animation a little bit more for the next frame

462.

463.

464. def moveGems(board, movingGems):

465.     # movingGems is a list of dicts with keys x, y, direction, imageNum

466.     for gem in movingGems:

467.         if gem['y'] != ROWABOVEBOARD:

468.             board[gem['x']][gem['y']] = EMPTY_SPACE

469.             movex = 0

470.             movey = 0

471.             if gem['direction'] == LEFT:

472.                 movex = -1

473.             elif gem['direction'] == RIGHT:

474.                 movex = 1

475.             elif gem['direction'] == DOWN:

476.                 movey = 1

477.             elif gem['direction'] == UP:

478.                 movey = -1

479.             board[gem['x'] + movex][gem['y'] + movey] = gem['imageNum']

480.         else:

481.             # gem is located above the board (where new gems come from)

482.             board[gem['x']][0] = gem['imageNum'] # move to top row

483.

484.

485. def fillBoardAndAnimate(board, points, score):

486.     dropSlots = getDropSlots(board)

487.     while dropSlots != [[]] * BOARDWIDTH:

488.         # do the dropping animation as long as there are more gems to drop

489.         movingGems = getDroppingGems(board)

490.         for x in range(len(dropSlots)):

491.             if len(dropSlots[x]) != 0:

492.                 # cause the lowest gem in each slot to begin moving in the DOWN direction

493.                 movingGems.append({'imageNum': dropSlots[x][0], 'x': x, 'y': ROWABOVEBOARD, 'direction': DOWN})

494.

495.         boardCopy = getBoardCopyMinusGems(board, movingGems)

496.         animateMovingGems(boardCopy, movingGems, points, score)

497.         moveGems(board, movingGems)

498.

499.         # Make the next row of gems from the drop slots

500.         # the lowest by deleting the previous lowest gems.

501.         for x in range(len(dropSlots)):

502.             if len(dropSlots[x]) == 0:

503.                 continue

504.             board[x][0] = dropSlots[x][0]

505.             del dropSlots[x][0]

506.

507.

508. def checkForGemClick(pos):

509.     # See if the mouse click was on the board

510.     for x in range(BOARDWIDTH):

511.         for y in range(BOARDHEIGHT):

512.             if BOARDRECTS[x][y].collidepoint(pos[0], pos[1]):

513.                 return {'x': x, 'y': y}

514.     return None # Click was not on the board.

515.

516.

517. def drawBoard(board):

518.     for x in range(BOARDWIDTH):

519.         for y in range(BOARDHEIGHT):

520.             pygame.draw.rect(DISPLAYSURF, GRIDCOLOR, BOARDRECTS[x][y], 1)

521.             gemToDraw = board[x][y]

522.             if gemToDraw != EMPTY_SPACE:

523.                 DISPLAYSURF.blit(GEMIMAGES[gemToDraw], BOARDRECTS[x][y])

524.

525.

526. def getBoardCopyMinusGems(board, gems):

527.     # Creates and returns a copy of the passed board data structure,

528.     # with the gems in the "gems" list removed from it.

529.     #

530.     # Gems is a list of dicts, with keys x, y, direction, imageNum

531.

532.     boardCopy = copy.deepcopy(board)

533.

534.     # Remove some of the gems from this board data structure copy.

535.     for gem in gems:

536.         if gem['y'] != ROWABOVEBOARD:

537.             boardCopy[gem['x']][gem['y']] = EMPTY_SPACE

538.     return boardCopy

539.

540.

541. def drawScore(score):

542.     scoreImg = BASICFONT.render(str(score), 1, SCORECOLOR)

543.     scoreRect = scoreImg.get_rect()

544.     scoreRect.bottomleft = (10, WINDOWHEIGHT - 6)

545.     DISPLAYSURF.blit(scoreImg, scoreRect)

546.

547.

548. if __name__ == '__main__':

549.     main()

Summary

I hope these game programs have given you your own ideas about what games you’d like to make and how you can write the code for them. Even if you don’t have any ideas of your own, it’s great practice to try to program clones of other games you’ve played.

Here are several websites that can teach you more about programming Python:

·         http://pygame.org – The official Pygame website has the source code to hundreds of games that people have written that make use of the Pygame library. You can learn a lot by downloading and reading other people’s source code.

·         http://python.org/doc/ - More Python tutorials and the documentation of all the Python modules and functions.

·         http://pygame.org/docs/ - Complete documentation on the modules and functions for Pygame

·         http://reddit.com/r/learnpython and http://reddit.com/r/learnprogramming have several users that could help you with finding resources to learn programming.

·         http://inventwithpython.com/pygame - This book's website, which includes all the source code for these programs and additional information. This site also has the image and sound files used in the Pygame programs.

·         http://inventwithpython.com - The website for the book “Invent Your Own Computer Games with Python”, which covers basic Python programming.

·         http://invpy.com/wiki - A wiki that covers individual Python programming concepts that you can look up if you need to learn about something specific.

·         http://invpy.com/traces - A web application that helps you trace through the execution of the programs in this book, step by step.

·         http://invpy.com/videos - Videos that accompany the programs in this book.

·         http://gamedevlessons.com - A helpful website about how to design and program video games.

·         [email protected] - My email address. Feel free to email me your questions about this book or about Python programming.

Or you can find out more about Python by searching the World Wide Web. Go to the search website http://google.com and search for “Python programming” or “Python tutorials” to find web sites that can teach you more about Python programming.

Now get going and invent your own games. And good luck!