1#!/usr/bin/env python3 2 3""" 4A curses-based version of Conway's Game of Life. 5 6An 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 15Contributed by Andrew Kuchling, Mouse support and color by Dafydd Crosby. 16""" 17 18import curses 19import random 20 21 22class 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 144def 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 152def 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 166def 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 258def main(stdscr): 259 keyloop(stdscr) # Enter the main loop 260 261if __name__ == '__main__': 262 curses.wrapper(main) 263