Flooder is a colorful game where a player tries to fill the board with a single color by changing the color of the tile in the upper-left corner. This new color spreads to all neighboring tiles that matched the original color. It’s similar to the Flood It mobile game. This program also has a colorblind mode, which uses shapes instead of flat colored tiles. It relies on the recursive flood fill algorithm to paint the board and works similarly to the “paint bucket” or “fill” tool in many painting applications.
Figure 28-1 shows what the output will look like when you run flooder.py.
Accessibility is a large issue in video games, and addressing it can take many forms. For example, deuteranopia, or red-green colorblindness, causes shades of red and green to appear the same, making it hard to distinguish between red objects and green objects on the screen. We can make Flooder more accessible with a mode that uses distinct shapes instead of distinct colors. Note that even the colorblind mode still uses color. This means you can eliminate the “standard” mode, if you wish, and have even color-sighted users play in the colorblind mode. The best accessibility designs are those that incorporate accessibility considerations from the start rather than add them as a separate mode. This reduces the amount of code we have to write and makes any future bug fixes easier.
Other accessibility issues include making sure that text is large enough to be read without perfect vision, that sound effects have visual cues and spoken language has subtitles for those hard of hearing, and that controls can be remapped to other keyboard keys so people can play the game with one hand. The YouTube channel Game Maker’s Toolkit has a video series called “Designing for Disability” that covers many aspects of designing your games with accessibility in mind.
1. """Flooder, by Al Sweigart [email protected]
2. A colorful game where you try to fill the board with a single color. Has
3. a mode for colorblind players.
4. Inspired by the "Flood It!" game.
5. This code is available at https://nostarch.com/big-book-small-python-programming
6. Tags: large, bext, game"""
7.
8. import random, sys
9.
10. try:
11. import bext
12. except ImportError:
13. print('This program requires the bext module, which you')
14. print('can install by following the instructions at')
15. print('https://pypi.org/project/Bext/')
16. sys.exit()
17.
18. # Set up the constants:
19. BOARD_WIDTH = 16 # (!) Try changing this to 4 or 40.
20. BOARD_HEIGHT = 14 # (!) Try changing this to 4 or 20.
21. MOVES_PER_GAME = 20 # (!) Try changing this to 3 or 300.
22.
23. # Constants for the different shapes used in colorblind mode:
24. HEART = chr(9829) # Character 9829 is '♥'.
25. DIAMOND = chr(9830) # Character 9830 is '♦'.
26. SPADE = chr(9824) # Character 9824 is '♠'.
27. CLUB = chr(9827) # Character 9827 is '♣'.
28. BALL = chr(9679) # Character 9679 is '●'.
29. TRIANGLE = chr(9650) # Character 9650 is '▲'.
30.
31. BLOCK = chr(9608) # Character 9608 is '█'
32. LEFTRIGHT = chr(9472) # Character 9472 is '─'
33. UPDOWN = chr(9474) # Character 9474 is '│'
34. DOWNRIGHT = chr(9484) # Character 9484 is '┌'
35. DOWNLEFT = chr(9488) # Character 9488 is '┐'
36. UPRIGHT = chr(9492) # Character 9492 is '└'
37. UPLEFT = chr(9496) # Character 9496 is '┘'
38. # A list of chr() codes is at https://inventwithpython.com/chr
39.
40. # All the color/shape tiles used on the board:
41. TILE_TYPES = (0, 1, 2, 3, 4, 5)
42. COLORS_MAP = {0: 'red', 1: 'green', 2:'blue',
43. 3:'yellow', 4:'cyan', 5:'purple'}
44. COLOR_MODE = 'color mode'
45. SHAPES_MAP = {0: HEART, 1: TRIANGLE, 2: DIAMOND,
46. 3: BALL, 4: CLUB, 5: SPADE}
47. SHAPE_MODE = 'shape mode'
48.
49.
50. def main():
51. bext.bg('black')
52. bext.fg('white')
53. bext.clear()
54. print('''Flooder, by Al Sweigart [email protected]
55.
56. Set the upper left color/shape, which fills in all the
57. adjacent squares of that color/shape. Try to make the
58. entire board the same color/shape.''')
59.
60. print('Do you want to play in colorblind mode? Y/N')
61. response = input('> ')
62. if response.upper().startswith('Y'):
63. displayMode = SHAPE_MODE
64. else:
65. displayMode = COLOR_MODE
66.
67. gameBoard = getNewBoard()
68. movesLeft = MOVES_PER_GAME
69.
70. while True: # Main game loop.
71. displayBoard(gameBoard, displayMode)
72.
73. print('Moves left:', movesLeft)
74. playerMove = askForPlayerMove(displayMode)
75. changeTile(playerMove, gameBoard, 0, 0)
76. movesLeft -= 1
77.
78. if hasWon(gameBoard):
79. displayBoard(gameBoard, displayMode)
80. print('You have won!')
81. break
82. elif movesLeft == 0:
83. displayBoard(gameBoard, displayMode)
84. print('You have run out of moves!')
85. break
86.
87.
88. def getNewBoard():
89. """Return a dictionary of a new Flood It board."""
90.
91. # Keys are (x, y) tuples, values are the tile at that position.
92. board = {}
93.
94. # Create random colors for the board.
95. for x in range(BOARD_WIDTH):
96. for y in range(BOARD_HEIGHT):
97. board[(x, y)] = random.choice(TILE_TYPES)
98.
99. # Make several tiles the same as their neighbor. This creates groups
100. # of the same color/shape.
101. for i in range(BOARD_WIDTH * BOARD_HEIGHT):
102. x = random.randint(0, BOARD_WIDTH - 2)
103. y = random.randint(0, BOARD_HEIGHT - 1)
104. board[(x + 1, y)] = board[(x, y)]
105. return board
106.
107.
108. def displayBoard(board, displayMode):
109. """Display the board on the screen."""
110. bext.fg('white')
111. # Display the top edge of the board:
112. print(DOWNRIGHT + (LEFTRIGHT * BOARD_WIDTH) + DOWNLEFT)
113.
114. # Display each row:
115. for y in range(BOARD_HEIGHT):
116. bext.fg('white')
117. if y == 0: # The first row begins with '>'.
118. print('>', end='')
119. else: # Later rows begin with a white vertical line.
120. print(UPDOWN, end='')
121.
122. # Display each tile in this row:
123. for x in range(BOARD_WIDTH):
124. bext.fg(COLORS_MAP[board[(x, y)]])
125. if displayMode == COLOR_MODE:
126. print(BLOCK, end='')
127. elif displayMode == SHAPE_MODE:
128. print(SHAPES_MAP[board[(x, y)]], end='')
129.
130. bext.fg('white')
131. print(UPDOWN) # Rows end with a white vertical line.
132. # Display the bottom edge of the board:
133. print(UPRIGHT + (LEFTRIGHT * BOARD_WIDTH) + UPLEFT)
134.
135.
136. def askForPlayerMove(displayMode):
137. """Let the player select a color to paint the upper left tile."""
138. while True:
139. bext.fg('white')
140. print('Choose one of ', end='')
141.
142. if displayMode == COLOR_MODE:
143. bext.fg('red')
144. print('(R)ed ', end='')
145. bext.fg('green')
146. print('(G)reen ', end='')
147. bext.fg('blue')
148. print('(B)lue ', end='')
149. bext.fg('yellow')
150. print('(Y)ellow ', end='')
151. bext.fg('cyan')
152. print('(C)yan ', end='')
153. bext.fg('purple')
154. print('(P)urple ', end='')
155. elif displayMode == SHAPE_MODE:
156. bext.fg('red')
157. print('(H)eart, ', end='')
158. bext.fg('green')
159. print('(T)riangle, ', end='')
160. bext.fg('blue')
161. print('(D)iamond, ', end='')
162. bext.fg('yellow')
163. print('(B)all, ', end='')
164. bext.fg('cyan')
165. print('(C)lub, ', end='')
166. bext.fg('purple')
167. print('(S)pade, ', end='')
168. bext.fg('white')
169. print('or QUIT:')
170. response = input('> ').upper()
171. if response == 'QUIT':
172. print('Thanks for playing!')
173. sys.exit()
174. if displayMode == COLOR_MODE and response in tuple('RGBYCP'):
175. # Return a tile type number based on the response:
176. return {'R': 0, 'G': 1, 'B': 2,
177. 'Y': 3, 'C': 4, 'P': 5}[response]
178. if displayMode == SHAPE_MODE and response in tuple('HTDBCS'):
179. # Return a tile type number based on the response:
180. return {'H': 0, 'T': 1, 'D':2,
181. 'B': 3, 'C': 4, 'S': 5}[response]
182.
183.
184. def changeTile(tileType, board, x, y, charToChange=None):
185. """Change the color/shape of a tile using the recursive flood fill
186. algorithm."""
187. if x == 0 and y == 0:
188. charToChange = board[(x, y)]
189. if tileType == charToChange:
190. return # Base Case: Already is the same tile.
191.
192. board[(x, y)] = tileType
193.
194. if x > 0 and board[(x - 1, y)] == charToChange:
195. # Recursive Case: Change the left neighbor's tile:
196. changeTile(tileType, board, x - 1, y, charToChange)
197. if y > 0 and board[(x, y - 1)] == charToChange:
198. # Recursive Case: Change the top neighbor's tile:
199. changeTile(tileType, board, x, y - 1, charToChange)
200. if x < BOARD_WIDTH - 1 and board[(x + 1, y)] == charToChange:
201. # Recursive Case: Change the right neighbor's tile:
202. changeTile(tileType, board, x + 1, y, charToChange)
203. if y < BOARD_HEIGHT - 1 and board[(x, y + 1)] == charToChange:
204. # Recursive Case: Change the bottom neighbor's tile:
205. changeTile(tileType, board, x, y + 1, charToChange)
206.
207.
208. def hasWon(board):
209. """Return True if the entire board is one color/shape."""
210. tile = board[(0, 0)]
211.
212. for x in range(BOARD_WIDTH):
213. for y in range(BOARD_HEIGHT):
214. if board[(x, y)] != tile:
215. return False
216. return True
217.
218.
219. # If this program was run (instead of imported), run the game:
220. if __name__ == '__main__':
221. main()
After entering the source code and running it a few times, try making experimental changes to it. The comments marked with (!)
have suggestions for small changes you can make. On your own, you can also try to figure out how to do the following:
Try to find the answers to the following questions. Experiment with some modifications to the code and rerun the program to see what effect the changes have.
board = {}
on line 92 to board = []
?return board
on line 105 to return None
?movesLeft -= 1
on line 76 to movesLeft -= 0
?