life.py 8.8 KB

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