Fish Tank

Tags: huge, artistic, bext

Watch your own virtual fish in a virtual fish tank, complete with air bubblers and kelp plants. Each time you run the program, it randomly generates the fish using different fish types and colors. Take a break and enjoy the calm serenity of this software aquarium, or try programming in some virtual sharks to terrorize its inhabitants! You can’t run this program from your IDE or editor. This program uses the bext module and must be run from the Command Prompt or Terminal in order to display correctly. More information about the bext module can be found at https://pypi.org/project/bext/.

fish_tank.py
  1"""Fish Tank, by Al Sweigart al@inventwithpython.com
  2A peaceful animation of a fish tank. Press Ctrl-C to stop.
  3Similar to ASCIIQuarium or @EmojiAquarium, but mine is based on an
  4older ASCII fish tank program for DOS.
  5https://robobunny.com/projects/asciiquarium/html/
  6https://twitter.com/EmojiAquarium
  7This code is available at https://nostarch.com/big-book-small-python-programming
  8Tags: extra-large, artistic, bext"""
  9
 10import random, sys, time
 11
 12try:
 13    import bext
 14except ImportError:
 15    print('This program requires the bext module, which you')
 16    print('can install by following the instructions at')
 17    print('https://pypi.org/project/Bext/')
 18    sys.exit()
 19
 20# Set up the constants:
 21WIDTH, HEIGHT = bext.size()
 22# We can't print to the last column on Windows without it adding a
 23# newline automatically, so reduce the width by one:
 24WIDTH -= 1
 25
 26NUM_KELP = 2  # (!) Try changing this to 10.
 27NUM_FISH = 10  # (!) Try changing this to 2 or 100.
 28NUM_BUBBLERS = 1  # (!) Try changing this to 0 or 10.
 29FRAMES_PER_SECOND = 4  # (!) Try changing this number to 1 or 60.
 30# (!) Try changing the constants to create a fish tank with only kelp,
 31# or only bubblers.
 32
 33# NOTE: Every string in a fish dictionary should be the same length.
 34FISH_TYPES = [
 35  {'right': ['><>'],          'left': ['<><']},
 36  {'right': ['>||>'],         'left': ['<||<']},
 37  {'right': ['>))>'],         'left': ['<[[<']},
 38  {'right': ['>||o', '>||.'], 'left': ['o||<', '.||<']},
 39  {'right': ['>))o', '>)).'], 'left': ['o[[<', '.[[<']},
 40  {'right': ['>-==>'],        'left': ['<==-<']},
 41  {'right': [r'>\\>'],        'left': ['<//<']},
 42  {'right': ['><)))*>'],      'left': ['<*(((><']},
 43  {'right': ['}-[[[*>'],      'left': ['<*]]]-{']},
 44  {'right': [']-<)))b>'],     'left': ['<d(((>-[']},
 45  {'right': ['><XXX*>'],      'left': ['<*XXX><']},
 46  {'right': ['_.-._.-^=>', '.-._.-.^=>',
 47             '-._.-._^=>', '._.-._.^=>'],
 48   'left':  ['<=^-._.-._', '<=^.-._.-.',
 49             '<=^_.-._.-', '<=^._.-._.']},
 50  ]  # (!) Try adding your own fish to FISH_TYPES.
 51LONGEST_FISH_LENGTH = 10  # Longest single string in FISH_TYPES.
 52
 53# The x and y positions where a fish runs into the edge of the screen:
 54LEFT_EDGE = 0
 55RIGHT_EDGE = WIDTH - 1 - LONGEST_FISH_LENGTH
 56TOP_EDGE = 0
 57BOTTOM_EDGE = HEIGHT - 2
 58
 59
 60def main():
 61    global FISHES, BUBBLERS, BUBBLES, KELPS, STEP
 62    bext.bg('black')
 63    bext.clear()
 64
 65    # Generate the global variables:
 66    FISHES = []
 67    for i in range(NUM_FISH):
 68        FISHES.append(generateFish())
 69
 70    # NOTE: Bubbles are drawn, but not the bubblers themselves.
 71    BUBBLERS = []
 72    for i in range(NUM_BUBBLERS):
 73        # Each bubbler starts at a random position.
 74        BUBBLERS.append(random.randint(LEFT_EDGE, RIGHT_EDGE))
 75    BUBBLES = []
 76
 77    KELPS = []
 78    for i in range(NUM_KELP):
 79        kelpx = random.randint(LEFT_EDGE, RIGHT_EDGE)
 80        kelp = {'x': kelpx, 'segments': []}
 81        # Generate each segment of the kelp:
 82        for i in range(random.randint(6, HEIGHT - 1)):
 83            kelp['segments'].append(random.choice(['(', ')']))
 84        KELPS.append(kelp)
 85
 86    # Run the simulation:
 87    STEP = 1
 88    while True:
 89        simulateAquarium()
 90        drawAquarium()
 91        time.sleep(1 / FRAMES_PER_SECOND)
 92        clearAquarium()
 93        STEP += 1
 94
 95
 96def getRandomColor():
 97    """Return a string of a random color."""
 98    return random.choice(('black', 'red', 'green', 'yellow', 'blue',
 99                          'purple', 'cyan', 'white'))
100
101
102def generateFish():
103    """Return a dictionary that represents a fish."""
104    fishType = random.choice(FISH_TYPES)
105
106    # Set up colors for each character in the fish text:
107    colorPattern = random.choice(('random', 'head-tail', 'single'))
108    fishLength = len(fishType['right'][0])
109    if colorPattern == 'random':  # All parts are randomly colored.
110        colors = []
111        for i in range(fishLength):
112            colors.append(getRandomColor())
113    if colorPattern == 'single' or colorPattern == 'head-tail':
114        colors = [getRandomColor()] * fishLength  # All the same color.
115    if colorPattern == 'head-tail':  # Head/tail different from body.
116        headTailColor = getRandomColor()
117        colors[0] = headTailColor  # Set head color.
118        colors[-1] = headTailColor  # Set tail color.
119
120    # Set up the rest of fish data structure:
121    fish = {'right':            fishType['right'],
122            'left':             fishType['left'],
123            'colors':           colors,
124            'hSpeed':           random.randint(1, 6),
125            'vSpeed':           random.randint(5, 15),
126            'timeToHDirChange': random.randint(10, 60),
127            'timeToVDirChange': random.randint(2, 20),
128            'goingRight':       random.choice([True, False]),
129            'goingDown':        random.choice([True, False])}
130
131    # 'x' is always the leftmost side of the fish body:
132    fish['x'] = random.randint(0, WIDTH - 1 - LONGEST_FISH_LENGTH)
133    fish['y'] = random.randint(0, HEIGHT - 2)
134    return fish
135
136
137def simulateAquarium():
138    """Simulate the movements in the aquarium for one step."""
139    global FISHES, BUBBLERS, BUBBLES, KELP, STEP
140
141    # Simulate the fish for one step:
142    for fish in FISHES:
143        # Move the fish horizontally:
144        if STEP % fish['hSpeed'] == 0:
145            if fish['goingRight']:
146                if fish['x'] != RIGHT_EDGE:
147                    fish['x'] += 1  # Move the fish right.
148                else:
149                    fish['goingRight'] = False  # Turn the fish around.
150                    fish['colors'].reverse()  # Turn the colors around.
151            else:
152                if fish['x'] != LEFT_EDGE:
153                    fish['x'] -= 1  # Move the fish left.
154                else:
155                    fish['goingRight'] = True  # Turn the fish around.
156                    fish['colors'].reverse()  # Turn the colors around.
157
158        # Fish can randomly change their horizontal direction:
159        fish['timeToHDirChange'] -= 1
160        if fish['timeToHDirChange'] == 0:
161            fish['timeToHDirChange'] = random.randint(10, 60)
162            # Turn the fish around:
163            fish['goingRight'] = not fish['goingRight']
164
165        # Move the fish vertically:
166        if STEP % fish['vSpeed'] == 0:
167            if fish['goingDown']:
168                if fish['y'] != BOTTOM_EDGE:
169                    fish['y'] += 1  # Move the fish down.
170                else:
171                    fish['goingDown'] = False  # Turn the fish around.
172            else:
173                if fish['y'] != TOP_EDGE:
174                    fish['y'] -= 1  # Move the fish up.
175                else:
176                    fish['goingDown'] = True  # Turn the fish around.
177
178        # Fish can randomly change their vertical direction:
179        fish['timeToVDirChange'] -= 1
180        if fish['timeToVDirChange'] == 0:
181            fish['timeToVDirChange'] = random.randint(2, 20)
182            # Turn the fish around:
183            fish['goingDown'] = not fish['goingDown']
184
185    # Generate bubbles from bubblers:
186    for bubbler in BUBBLERS:
187        # There is a 1 in 5 chance of making a bubble:
188        if random.randint(1, 5) == 1:
189            BUBBLES.append({'x': bubbler, 'y': HEIGHT - 2})
190
191    # Move the bubbles:
192    for bubble in BUBBLES:
193        diceRoll = random.randint(1, 6)
194        if (diceRoll == 1) and (bubble['x'] != LEFT_EDGE):
195            bubble['x'] -= 1  # Bubble goes left.
196        elif (diceRoll == 2) and (bubble['x'] != RIGHT_EDGE):
197            bubble['x'] += 1  # Bubble goes right.
198
199        bubble['y'] -= 1  # The bubble always goes up.
200
201    # Iterate over BUBBLES in reverse because I'm deleting from BUBBLES
202    # while iterating over it.
203    for i in range(len(BUBBLES) - 1, -1, -1):
204        if BUBBLES[i]['y'] == TOP_EDGE:  # Delete bubbles that reach the top.
205            del BUBBLES[i]
206
207    # Simulate the kelp waving for one step:
208    for kelp in KELPS:
209        for i, kelpSegment in enumerate(kelp['segments']):
210            # 1 in 20 chance to change waving:
211            if random.randint(1, 20) == 1:
212                if kelpSegment == '(':
213                    kelp['segments'][i] = ')'
214                elif kelpSegment == ')':
215                    kelp['segments'][i] = '('
216
217
218def drawAquarium():
219    """Draw the aquarium on the screen."""
220    global FISHES, BUBBLERS, BUBBLES, KELP, STEP
221
222    # Draw quit message.
223    bext.fg('white')
224    bext.goto(0, 0)
225    print('Fish Tank, by Al Sweigart    Ctrl-C to quit.', end='')
226
227    # Draw the bubbles:
228    bext.fg('white')
229    for bubble in BUBBLES:
230        bext.goto(bubble['x'], bubble['y'])
231        print(random.choice(('o', 'O')), end='')
232
233    # Draw the fish:
234    for fish in FISHES:
235        bext.goto(fish['x'], fish['y'])
236
237        # Get the correct right- or left-facing fish text.
238        if fish['goingRight']:
239            fishText = fish['right'][STEP % len(fish['right'])]
240        else:
241            fishText = fish['left'][STEP % len(fish['left'])]
242
243        # Draw each character of the fish text in the right color.
244        for i, fishPart in enumerate(fishText):
245            bext.fg(fish['colors'][i])
246            print(fishPart, end='')
247
248    # Draw the kelp:
249    bext.fg('green')
250    for kelp in KELPS:
251        for i, kelpSegment in enumerate(kelp['segments']):
252            if kelpSegment == '(':
253                bext.goto(kelp['x'], BOTTOM_EDGE - i)
254            elif kelpSegment == ')':
255                bext.goto(kelp['x'] + 1, BOTTOM_EDGE - i)
256            print(kelpSegment, end='')
257
258    # Draw the sand on the bottom:
259    bext.fg('yellow')
260    bext.goto(0, HEIGHT - 1)
261    print(chr(9617) * (WIDTH - 1), end='')  # Draws '░' characters.
262
263    sys.stdout.flush()  # (Required for bext-using programs.)
264
265
266def clearAquarium():
267    """Draw empty spaces over everything on the screen."""
268    global FISHES, BUBBLERS, BUBBLES, KELP
269
270    # Draw the bubbles:
271    for bubble in BUBBLES:
272        bext.goto(bubble['x'], bubble['y'])
273        print(' ', end='')
274
275    # Draw the fish:
276    for fish in FISHES:
277        bext.goto(fish['x'], fish['y'])
278
279        # Draw each character of the fish text in the right color.
280        print(' ' * len(fish['left'][0]), end='')
281
282    # Draw the kelp:
283    for kelp in KELPS:
284        for i, kelpSegment in enumerate(kelp['segments']):
285            bext.goto(kelp['x'], HEIGHT - 2 - i)
286            print('  ', end='')
287
288    sys.stdout.flush()  # (Required for bext-using programs.)
289
290
291# If this program was run (instead of imported), run the game:
292if __name__ == '__main__':
293    try:
294        main()
295    except KeyboardInterrupt:
296        sys.exit()  # When Ctrl-C is pressed, end the program.

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