This simulation shows a forest whose trees are constantly growing and then being burned down. On each step of the simulation, there is a 1 percent chance that a blank space grows into a tree and a 1 percent chance that a tree is struck by lightning and burns. Fires will spread to adjacent trees, so a densely packed forest is more likely to suffer a larger fire than a sparsely packed one. This simulation was inspired by Nicky Case’s Emoji Sim at http://ncase.me/simulating/model/.
When you run forestfiresim.py, the output will look like this:
This simulation is an example of emergent behavior—the interaction between simple parts in a system creating complicated patterns. Empty spaces grow into trees, lightning turns trees into fire, and fire turns trees back into empty spaces while spreading to neighboring trees. By adjusting the tree growth and lightning strike rate, you can cause the forest to display different phenomena. For example, a low lightning chance but high growth rate causes large, constant forest fires, since the trees tend to be near each other and quickly replenish. A low growth rate but high lightning strike chance creates several small fires that quickly extinguish due to a lack of nearby trees. We don’t explicitly program any of this behavior; rather, it naturally emerges from the system that we created.
1. """Forest Fire Sim, by Al Sweigart [email protected]
2. A simulation of wildfires spreading in a forest. Press Ctrl-C to stop.
3. Inspired by Nicky Case's Emoji Sim http://ncase.me/simulating/model/
4. This code is available at https://nostarch.com/big-book-small-python-programming
5. Tags: short, bext, simulation"""
6.
7. import random, sys, time
8.
9. try:
10. import bext
11. except ImportError:
12. print('This program requires the bext module, which you')
13. print('can install by following the instructions at')
14. print('https://pypi.org/project/Bext/')
15. sys.exit()
16.
17. # Set up the constants:
18. WIDTH = 79
19. HEIGHT = 22
20.
21. TREE = 'A'
22. FIRE = 'W'
23. EMPTY = ' '
24.
25. # (!) Try changing these settings to anything between 0.0 and 1.0:
26. INITIAL_TREE_DENSITY = 0.20 # Amount of forest that starts with trees.
27. GROW_CHANCE = 0.01 # Chance a blank space turns into a tree.
28. FIRE_CHANCE = 0.01 # Chance a tree is hit by lightning & burns.
29.
30. # (!) Try setting the pause length to 1.0 or 0.0:
31. PAUSE_LENGTH = 0.5
32.
33.
34. def main():
35. forest = createNewForest()
36. bext.clear()
37.
38. while True: # Main program loop.
39. displayForest(forest)
40.
41. # Run a single simulation step:
42. nextForest = {'width': forest['width'],
43. 'height': forest['height']}
44.
45. for x in range(forest['width']):
46. for y in range(forest['height']):
47. if (x, y) in nextForest:
48. # If we've already set nextForest[(x, y)] on a
49. # previous iteration, just do nothing here:
50. continue
51.
52. if ((forest[(x, y)] == EMPTY)
53. and (random.random() <= GROW_CHANCE)):
54. # Grow a tree in this empty space.
55. nextForest[(x, y)] = TREE
56. elif ((forest[(x, y)] == TREE)
57. and (random.random() <= FIRE_CHANCE)):
58. # Lightning sets this tree on fire.
59. nextForest[(x, y)] = FIRE
60. elif forest[(x, y)] == FIRE:
61. # This tree is currently burning.
62. # Loop through all the neighboring spaces:
63. for ix in range(-1, 2):
64. for iy in range(-1, 2):
65. # Fire spreads to neighboring trees:
66. if forest.get((x + ix, y + iy)) == TREE:
67. nextForest[(x + ix, y + iy)] = FIRE
68. # The tree has burned down now, so erase it:
69. nextForest[(x, y)] = EMPTY
70. else:
71. # Just copy the existing object:
72. nextForest[(x, y)] = forest[(x, y)]
73. forest = nextForest
74.
75. time.sleep(PAUSE_LENGTH)
76.
77.
78. def createNewForest():
79. """Returns a dictionary for a new forest data structure."""
80. forest = {'width': WIDTH, 'height': HEIGHT}
81. for x in range(WIDTH):
82. for y in range(HEIGHT):
83. if (random.random() * 100) <= INITIAL_TREE_DENSITY:
84. forest[(x, y)] = TREE # Start as a tree.
85. else:
86. forest[(x, y)] = EMPTY # Start as an empty space.
87. return forest
88.
89.
90. def displayForest(forest):
91. """Display the forest data structure on the screen."""
92. bext.goto(0, 0)
93. for y in range(forest['height']):
94. for x in range(forest['width']):
95. if forest[(x, y)] == TREE:
96. bext.fg('green')
97. print(TREE, end='')
98. elif forest[(x, y)] == FIRE:
99. bext.fg('red')
100. print(FIRE, end='')
101. elif forest[(x, y)] == EMPTY:
102. print(EMPTY, end='')
103. print()
104. bext.fg('reset') # Use the default font color.
105. print('Grow chance: {}% '.format(GROW_CHANCE * 100), end='')
106. print('Lightning chance: {}% '.format(FIRE_CHANCE * 100), end='')
107. print('Press Ctrl-C to quit.')
108.
109.
110. # If this program was run (instead of imported), run the game:
111. if __name__ == '__main__':
112. try:
113. main()
114. except KeyboardInterrupt:
115. 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.
bext.fg('green')
on line 96 to bext.fg('random')
?EMPTY = ' '
on line 23 to EMPTY = '.'
?forest.get((x + ix, y + iy)) == TREE
on line 66 to forest.get((x + ix, y + iy)) == EMPTY
?nextForest[(x, y)] = EMPTY
on line 69 to nextForest[(x, y)] = FIRE
?forest[(x, y)] = EMPTY
on line 86 to forest[(x, y)] = TREE
?