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 LifeBoard: 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) 263