"""Hourglass, by Al Sweigart al@inventwithpython.com
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.