Flooder

Tags: large, bext, game

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()

https://inventwithpython.com/bigbookpython/project28.html