1 #!/usr/bin/env python3
2
3 """
4 A curses-based version of Conway's Game of Life.
5
6 An empty board will be displayed, and the following commands are available:
7 E : Erase the board
8 R : Fill the board randomly
9 S : Step for a single generation
10 C : Update continuously until a key is struck
11 Q : Quit
12 Cursor keys : Move the cursor around the board
13 Space or Enter : Toggle the contents of the cursor's position
14
15 Contributed by Andrew Kuchling, Mouse support and color by Dafydd Crosby.
16 """
17
18 import curses
19 import random
20
21
22 class ESC[4;38;5;81mLifeBoard:
23 """Encapsulates a Life board
24
25 Attributes:
26 X,Y : horizontal and vertical size of the board
27 state : dictionary mapping (x,y) to 0 or 1
28
29 Methods:
30 display(update_board) -- If update_board is true, compute the
31 next generation. Then display the state
32 of the board and refresh the screen.
33 erase() -- clear the entire board
34 make_random() -- fill the board randomly
35 set(y,x) -- set the given cell to Live; doesn't refresh the screen
36 toggle(y,x) -- change the given cell from live to dead, or vice
37 versa, and refresh the screen display
38
39 """
40 def __init__(self, scr, char=ord('*')):
41 """Create a new LifeBoard instance.
42
43 scr -- curses screen object to use for display
44 char -- character used to render live cells (default: '*')
45 """
46 self.state = {}
47 self.scr = scr
48 Y, X = self.scr.getmaxyx()
49 self.X, self.Y = X - 2, Y - 2 - 1
50 self.char = char
51 self.scr.clear()
52
53 # Draw a border around the board
54 border_line = '+' + (self.X * '-') + '+'
55 self.scr.addstr(0, 0, border_line)
56 self.scr.addstr(self.Y + 1, 0, border_line)
57 for y in range(0, self.Y):
58 self.scr.addstr(1 + y, 0, '|')
59 self.scr.addstr(1 + y, self.X + 1, '|')
60 self.scr.refresh()
61
62 def set(self, y, x):
63 """Set a cell to the live state"""
64 if x < 0 or self.X <= x or y < 0 or self.Y <= y:
65 raise ValueError("Coordinates out of range %i,%i" % (y, x))
66 self.state[x, y] = 1
67
68 def toggle(self, y, x):
69 """Toggle a cell's state between live and dead"""
70 if x < 0 or self.X <= x or y < 0 or self.Y <= y:
71 raise ValueError("Coordinates out of range %i,%i" % (y, x))
72 if (x, y) in self.state:
73 del self.state[x, y]
74 self.scr.addch(y + 1, x + 1, ' ')
75 else:
76 self.state[x, y] = 1
77 if curses.has_colors():
78 # Let's pick a random color!
79 self.scr.attrset(curses.color_pair(random.randrange(1, 7)))
80 self.scr.addch(y + 1, x + 1, self.char)
81 self.scr.attrset(0)
82 self.scr.refresh()
83
84 def erase(self):
85 """Clear the entire board and update the board display"""
86 self.state = {}
87 self.display(update_board=False)
88
89 def display(self, update_board=True):
90 """Display the whole board, optionally computing one generation"""
91 M, N = self.X, self.Y
92 if not update_board:
93 for i in range(0, M):
94 for j in range(0, N):
95 if (i, j) in self.state:
96 self.scr.addch(j + 1, i + 1, self.char)
97 else:
98 self.scr.addch(j + 1, i + 1, ' ')
99 self.scr.refresh()
100 return
101
102 d = {}
103 self.boring = 1
104 for i in range(0, M):
105 L = range(max(0, i - 1), min(M, i + 2))
106 for j in range(0, N):
107 s = 0
108 live = (i, j) in self.state
109 for k in range(max(0, j - 1), min(N, j + 2)):
110 for l in L:
111 if (l, k) in self.state:
112 s += 1
113 s -= live
114 if s == 3:
115 # Birth
116 d[i, j] = 1
117 if curses.has_colors():
118 # Let's pick a random color!
119 self.scr.attrset(curses.color_pair(
120 random.randrange(1, 7)))
121 self.scr.addch(j + 1, i + 1, self.char)
122 self.scr.attrset(0)
123 if not live:
124 self.boring = 0
125 elif s == 2 and live:
126 # Survival
127 d[i, j] = 1
128 elif live:
129 # Death
130 self.scr.addch(j + 1, i + 1, ' ')
131 self.boring = 0
132 self.state = d
133 self.scr.refresh()
134
135 def make_random(self):
136 "Fill the board with a random pattern"
137 self.state = {}
138 for i in range(0, self.X):
139 for j in range(0, self.Y):
140 if random.random() > 0.5:
141 self.set(j, i)
142
143
144 def erase_menu(stdscr, menu_y):
145 "Clear the space where the menu resides"
146 stdscr.move(menu_y, 0)
147 stdscr.clrtoeol()
148 stdscr.move(menu_y + 1, 0)
149 stdscr.clrtoeol()
150
151
152 def display_menu(stdscr, menu_y):
153 "Display the menu of possible keystroke commands"
154 erase_menu(stdscr, menu_y)
155
156 # If color, then light the menu up :-)
157 if curses.has_colors():
158 stdscr.attrset(curses.color_pair(1))
159 stdscr.addstr(menu_y, 4,
160 'Use the cursor keys to move, and space or Enter to toggle a cell.')
161 stdscr.addstr(menu_y + 1, 4,
162 'E)rase the board, R)andom fill, S)tep once or C)ontinuously, Q)uit')
163 stdscr.attrset(0)
164
165
166 def keyloop(stdscr):
167 # Clear the screen and display the menu of keys
168 stdscr.clear()
169 stdscr_y, stdscr_x = stdscr.getmaxyx()
170 menu_y = (stdscr_y - 3) - 1
171 display_menu(stdscr, menu_y)
172
173 # If color, then initialize the color pairs
174 if curses.has_colors():
175 curses.init_pair(1, curses.COLOR_BLUE, 0)
176 curses.init_pair(2, curses.COLOR_CYAN, 0)
177 curses.init_pair(3, curses.COLOR_GREEN, 0)
178 curses.init_pair(4, curses.COLOR_MAGENTA, 0)
179 curses.init_pair(5, curses.COLOR_RED, 0)
180 curses.init_pair(6, curses.COLOR_YELLOW, 0)
181 curses.init_pair(7, curses.COLOR_WHITE, 0)
182
183 # Set up the mask to listen for mouse events
184 curses.mousemask(curses.BUTTON1_CLICKED)
185
186 # Allocate a subwindow for the Life board and create the board object
187 subwin = stdscr.subwin(stdscr_y - 3, stdscr_x, 0, 0)
188 board = LifeBoard(subwin, char=ord('*'))
189 board.display(update_board=False)
190
191 # xpos, ypos are the cursor's position
192 xpos, ypos = board.X // 2, board.Y // 2
193
194 # Main loop:
195 while True:
196 stdscr.move(1 + ypos, 1 + xpos) # Move the cursor
197 c = stdscr.getch() # Get a keystroke
198 if 0 < c < 256:
199 c = chr(c)
200 if c in ' \n':
201 board.toggle(ypos, xpos)
202 elif c in 'Cc':
203 erase_menu(stdscr, menu_y)
204 stdscr.addstr(menu_y, 6, ' Hit any key to stop continuously '
205 'updating the screen.')
206 stdscr.refresh()
207 # Activate nodelay mode; getch() will return -1
208 # if no keystroke is available, instead of waiting.
209 stdscr.nodelay(1)
210 while True:
211 c = stdscr.getch()
212 if c != -1:
213 break
214 stdscr.addstr(0, 0, '/')
215 stdscr.refresh()
216 board.display()
217 stdscr.addstr(0, 0, '+')
218 stdscr.refresh()
219
220 stdscr.nodelay(0) # Disable nodelay mode
221 display_menu(stdscr, menu_y)
222
223 elif c in 'Ee':
224 board.erase()
225 elif c in 'Qq':
226 break
227 elif c in 'Rr':
228 board.make_random()
229 board.display(update_board=False)
230 elif c in 'Ss':
231 board.display()
232 else:
233 # Ignore incorrect keys
234 pass
235 elif c == curses.KEY_UP and ypos > 0:
236 ypos -= 1
237 elif c == curses.KEY_DOWN and ypos + 1 < board.Y:
238 ypos += 1
239 elif c == curses.KEY_LEFT and xpos > 0:
240 xpos -= 1
241 elif c == curses.KEY_RIGHT and xpos + 1 < board.X:
242 xpos += 1
243 elif c == curses.KEY_MOUSE:
244 mouse_id, mouse_x, mouse_y, mouse_z, button_state = curses.getmouse()
245 if (mouse_x > 0 and mouse_x < board.X + 1 and
246 mouse_y > 0 and mouse_y < board.Y + 1):
247 xpos = mouse_x - 1
248 ypos = mouse_y - 1
249 board.toggle(ypos, xpos)
250 else:
251 # They've clicked outside the board
252 curses.flash()
253 else:
254 # Ignore incorrect keys
255 pass
256
257
258 def main(stdscr):
259 keyloop(stdscr) # Enter the main loop
260
261 if __name__ == '__main__':
262 curses.wrapper(main)