Flooder
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.
How It Works
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.
flooder.py
1"""Flooder, by Al Sweigart al@inventwithpython.com
2A colorful game where you try to fill the board with a single color. Has
3a mode for colorblind players.
4Inspired by the "Flood It!" game.
5This code is available at https://nostarch.com/big-book-small-python-programming
6Tags: large, bext, game"""
7
8import random, sys
9
10try:
11 import bext
12except 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:
19BOARD_WIDTH = 16 # (!) Try changing this to 4 or 40.
20BOARD_HEIGHT = 14 # (!) Try changing this to 4 or 20.
21MOVES_PER_GAME = 20 # (!) Try changing this to 3 or 300.
22
23# Constants for the different shapes used in colorblind mode:
24HEART = chr(9829) # Character 9829 is '♥'.
25DIAMOND = chr(9830) # Character 9830 is '♦'.
26SPADE = chr(9824) # Character 9824 is '♠'.
27CLUB = chr(9827) # Character 9827 is '♣'.
28BALL = chr(9679) # Character 9679 is '●'.
29TRIANGLE = chr(9650) # Character 9650 is '▲'.
30
31BLOCK = chr(9608) # Character 9608 is '█'
32LEFTRIGHT = chr(9472) # Character 9472 is '─'
33UPDOWN = chr(9474) # Character 9474 is '│'
34DOWNRIGHT = chr(9484) # Character 9484 is '┌'
35DOWNLEFT = chr(9488) # Character 9488 is '┐'
36UPRIGHT = chr(9492) # Character 9492 is '└'
37UPLEFT = 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:
41TILE_TYPES = (0, 1, 2, 3, 4, 5)
42COLORS_MAP = {0: 'red', 1: 'green', 2:'blue',
43 3:'yellow', 4:'cyan', 5:'purple'}
44COLOR_MODE = 'color mode'
45SHAPES_MAP = {0: HEART, 1: TRIANGLE, 2: DIAMOND,
46 3: BALL, 4: CLUB, 5: SPADE}
47SHAPE_MODE = 'shape mode'
48
49
50def main():
51 bext.bg('black')
52 bext.fg('white')
53 bext.clear()
54 print('''Flooder, by Al Sweigart al@inventwithpython.com
55
56Set the upper left color/shape, which fills in all the
57adjacent squares of that color/shape. Try to make the
58entire 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
88def 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
108def 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
136def 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
184def 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
208def 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:
220if __name__ == '__main__':
221 main()