Fish Tank
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.