forked from asweigart/the-big-book-of-small-python-projects
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhourglass.py
191 lines (160 loc) · 7.64 KB
/
hourglass.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""Hourglass, by Al Sweigart [email protected]
An animation of an hourglass with falling sand. Press Ctrl-C to stop.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: large, artistic, bext, simulation"""
import random, sys, time
try:
import bext
except ImportError:
print('This program requires the bext module, which you')
print('can install by following the instructions at')
print('https://pypi.org/project/Bext/')
sys.exit()
# Set up the constants:
PAUSE_LENGTH = 0.2 # (!) Try changing this to 0.0 or 1.0.
# (!) Try changing this to any number between 0 and 100:
WIDE_FALL_CHANCE = 50
SCREEN_WIDTH = 79
SCREEN_HEIGHT = 25
X = 0 # The index of X values in an (x, y) tuple is 0.
Y = 1 # The index of Y values in an (x, y) tuple is 1.
SAND = chr(9617)
WALL = chr(9608)
# Set up the walls of the hour glass:
HOURGLASS = set() # Has (x, y) tuples for where hourglass walls are.
# (!) Try commenting out some HOURGLASS.add() lines to erase walls:
for i in range(18, 37):
HOURGLASS.add((i, 1)) # Add walls for the top cap of the hourglass.
HOURGLASS.add((i, 23)) # Add walls for the bottom cap.
for i in range(1, 5):
HOURGLASS.add((18, i)) # Add walls for the top left straight wall.
HOURGLASS.add((36, i)) # Add walls for the top right straight wall.
HOURGLASS.add((18, i + 19)) # Add walls for the bottom left.
HOURGLASS.add((36, i + 19)) # Add walls for the bottom right.
for i in range(8):
HOURGLASS.add((19 + i, 5 + i)) # Add the top left slanted wall.
HOURGLASS.add((35 - i, 5 + i)) # Add the top right slanted wall.
HOURGLASS.add((25 - i, 13 + i)) # Add the bottom left slanted wall.
HOURGLASS.add((29 + i, 13 + i)) # Add the bottom right slanted wall.
# Set up the initial sand at the top of the hourglass:
INITIAL_SAND = set()
for y in range(8):
for x in range(19 + y, 36 - y):
INITIAL_SAND.add((x, y + 4))
def main():
bext.fg('yellow')
bext.clear()
# Draw the quit message:
bext.goto(0, 0)
print('Ctrl-C to quit.', end='')
# Display the walls of the hourglass:
for wall in HOURGLASS:
bext.goto(wall[X], wall[Y])
print(WALL, end='')
while True: # Main program loop.
allSand = list(INITIAL_SAND)
# Draw the initial sand:
for sand in allSand:
bext.goto(sand[X], sand[Y])
print(SAND, end='')
runHourglassSimulation(allSand)
def runHourglassSimulation(allSand):
"""Keep running the sand falling simulation until the sand stops
moving."""
while True: # Keep looping until sand has run out.
random.shuffle(allSand) # Random order of grain simulation.
sandMovedOnThisStep = False
for i, sand in enumerate(allSand):
if sand[Y] == SCREEN_HEIGHT - 1:
# Sand is on the very bottom, so it won't move:
continue
# If nothing is under this sand, move it down:
noSandBelow = (sand[X], sand[Y] + 1) not in allSand
noWallBelow = (sand[X], sand[Y] + 1) not in HOURGLASS
canFallDown = noSandBelow and noWallBelow
if canFallDown:
# Draw the sand in its new position down one space:
bext.goto(sand[X], sand[Y])
print(' ', end='') # Clear the old position.
bext.goto(sand[X], sand[Y] + 1)
print(SAND, end='')
# Set the sand in its new position down one space:
allSand[i] = (sand[X], sand[Y] + 1)
sandMovedOnThisStep = True
else:
# Check if the sand can fall to the left:
belowLeft = (sand[X] - 1, sand[Y] + 1)
noSandBelowLeft = belowLeft not in allSand
noWallBelowLeft = belowLeft not in HOURGLASS
left = (sand[X] - 1, sand[Y])
noWallLeft = left not in HOURGLASS
notOnLeftEdge = sand[X] > 0
canFallLeft = (noSandBelowLeft and noWallBelowLeft
and noWallLeft and notOnLeftEdge)
# Check if the sand can fall to the right:
belowRight = (sand[X] + 1, sand[Y] + 1)
noSandBelowRight = belowRight not in allSand
noWallBelowRight = belowRight not in HOURGLASS
right = (sand[X] + 1, sand[Y])
noWallRight = right not in HOURGLASS
notOnRightEdge = sand[X] < SCREEN_WIDTH - 1
canFallRight = (noSandBelowRight and noWallBelowRight
and noWallRight and notOnRightEdge)
# Set the falling direction:
fallingDirection = None
if canFallLeft and not canFallRight:
fallingDirection = -1 # Set the sand to fall left.
elif not canFallLeft and canFallRight:
fallingDirection = 1 # Set the sand to fall right.
elif canFallLeft and canFallRight:
# Both are possible, so randomly set it:
fallingDirection = random.choice((-1, 1))
# Check if the sand can "far" fall two spaces to
# the left or right instead of just one space:
if random.random() * 100 <= WIDE_FALL_CHANCE:
belowTwoLeft = (sand[X] - 2, sand[Y] + 1)
noSandBelowTwoLeft = belowTwoLeft not in allSand
noWallBelowTwoLeft = belowTwoLeft not in HOURGLASS
notOnSecondToLeftEdge = sand[X] > 1
canFallTwoLeft = (canFallLeft and noSandBelowTwoLeft
and noWallBelowTwoLeft and notOnSecondToLeftEdge)
belowTwoRight = (sand[X] + 2, sand[Y] + 1)
noSandBelowTwoRight = belowTwoRight not in allSand
noWallBelowTwoRight = belowTwoRight not in HOURGLASS
notOnSecondToRightEdge = sand[X] < SCREEN_WIDTH - 2
canFallTwoRight = (canFallRight
and noSandBelowTwoRight and noWallBelowTwoRight
and notOnSecondToRightEdge)
if canFallTwoLeft and not canFallTwoRight:
fallingDirection = -2
elif not canFallTwoLeft and canFallTwoRight:
fallingDirection = 2
elif canFallTwoLeft and canFallTwoRight:
fallingDirection = random.choice((-2, 2))
if fallingDirection == None:
# This sand can't fall, so move on.
continue
# Draw the sand in its new position:
bext.goto(sand[X], sand[Y])
print(' ', end='') # Erase old sand.
bext.goto(sand[X] + fallingDirection, sand[Y] + 1)
print(SAND, end='') # Draw new sand.
# Move the grain of sand to its new position:
allSand[i] = (sand[X] + fallingDirection, sand[Y] + 1)
sandMovedOnThisStep = True
sys.stdout.flush() # (Required for bext-using programs.)
time.sleep(PAUSE_LENGTH) # Pause after this
# If no sand has moved on this step, reset the hourglass:
if not sandMovedOnThisStep:
time.sleep(2)
# Erase all of the sand:
for sand in allSand:
bext.goto(sand[X], sand[Y])
print(' ', end='')
break # Break out of main simulation loop.
# If this program was run (instead of imported), run the game:
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
sys.exit() # When Ctrl-C is pressed, end the program.