Home | History | Annotate | Download | only in demo
      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