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/.
Figure 27-1 show what the output will look like when you run fishtank.py.
Modern graphical programs often generate animations by erasing their entire window and redrawing it 30 or 60 times a second. This gives them a frame rate of 30 or 60 frames per second (FPS). The higher the FPS, the more fluid the animated movement appears.
Drawing to terminal windows is much slower. If we erased the entire terminal window to redraw its contents with the bext
module, we typically would only get about 3 or 4 FPS. This would cause a noticeable flicker in the window.
We can speed this up by only drawing characters to the parts of the terminal window that have changed. Most of the fish tank program’s output is empty space, so to make the elements move, the clearAquarium()
only has to draw ' '
space characters to the places where the fish, kelp, and bubbles currently are. This increases our frame rate, reduces flickering, and makes for a much more pleasant fish tank animation.
1. """Fish Tank, by Al Sweigart [email protected]
2. A peaceful animation of a fish tank. Press Ctrl-C to stop.
3. Similar to ASCIIQuarium or @EmojiAquarium, but mine is based on an
4. older ASCII fish tank program for DOS.
5. https://robobunny.com/projects/asciiquarium/html/
6. https://twitter.com/EmojiAquarium
7. This code is available at https://nostarch.com/big-book-small-python-programming
8. Tags: extra-large, artistic, bext"""
9.
10. import random, sys, time
11.
12. try:
13. import bext
14. except 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:
21. WIDTH, 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:
24. WIDTH -= 1
25.
26. NUM_KELP = 2 # (!) Try changing this to 10.
27. NUM_FISH = 10 # (!) Try changing this to 2 or 100.
28. NUM_BUBBLERS = 1 # (!) Try changing this to 0 or 10.
29. FRAMES_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.
34. FISH_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.
51. LONGEST_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:
54. LEFT_EDGE = 0
55. RIGHT_EDGE = WIDTH - 1 - LONGEST_FISH_LENGTH
56. TOP_EDGE = 0
57. BOTTOM_EDGE = HEIGHT - 2
58.
59.
60. def 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.
96. def getRandomColor():
97. """Return a string of a random color."""
98. return random.choice(('black', 'red', 'green', 'yellow', 'blue',
99. 'purple', 'cyan', 'white'))
100.
101.
102. def 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.
137. def 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.
218. def 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.
266. def 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:
292. if __name__ == '__main__':
293. try:
294. main()
295. except KeyboardInterrupt:
296. sys.exit() # When Ctrl-C is pressed, end the program.
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.
LONGEST_FISH_LENGTH = 10
on line 51 to LONGEST_FISH_LENGTH = 50
?'right': fishType['right']
on line 121 to 'right': fishType['left']
?bext.fg('green')
on line 249 to bext.fg('red')
?clearAquarium()
on line 92?bext.fg(fish['colors'][i])
on line 245 to bext.fg('random')
?random.randint(10, 60)
on line 161 to 1
?