17db96d56Sopenharmony_ci#!/usr/bin/env python3
27db96d56Sopenharmony_ci
37db96d56Sopenharmony_ci"""
47db96d56Sopenharmony_ciA curses-based version of Conway's Game of Life.
57db96d56Sopenharmony_ci
67db96d56Sopenharmony_ciAn empty board will be displayed, and the following commands are available:
77db96d56Sopenharmony_ci E : Erase the board
87db96d56Sopenharmony_ci R : Fill the board randomly
97db96d56Sopenharmony_ci S : Step for a single generation
107db96d56Sopenharmony_ci C : Update continuously until a key is struck
117db96d56Sopenharmony_ci Q : Quit
127db96d56Sopenharmony_ci Cursor keys :  Move the cursor around the board
137db96d56Sopenharmony_ci Space or Enter : Toggle the contents of the cursor's position
147db96d56Sopenharmony_ci
157db96d56Sopenharmony_ciContributed by Andrew Kuchling, Mouse support and color by Dafydd Crosby.
167db96d56Sopenharmony_ci"""
177db96d56Sopenharmony_ci
187db96d56Sopenharmony_ciimport curses
197db96d56Sopenharmony_ciimport random
207db96d56Sopenharmony_ci
217db96d56Sopenharmony_ci
227db96d56Sopenharmony_ciclass LifeBoard:
237db96d56Sopenharmony_ci    """Encapsulates a Life board
247db96d56Sopenharmony_ci
257db96d56Sopenharmony_ci    Attributes:
267db96d56Sopenharmony_ci    X,Y : horizontal and vertical size of the board
277db96d56Sopenharmony_ci    state : dictionary mapping (x,y) to 0 or 1
287db96d56Sopenharmony_ci
297db96d56Sopenharmony_ci    Methods:
307db96d56Sopenharmony_ci    display(update_board) -- If update_board is true, compute the
317db96d56Sopenharmony_ci                             next generation.  Then display the state
327db96d56Sopenharmony_ci                             of the board and refresh the screen.
337db96d56Sopenharmony_ci    erase() -- clear the entire board
347db96d56Sopenharmony_ci    make_random() -- fill the board randomly
357db96d56Sopenharmony_ci    set(y,x) -- set the given cell to Live; doesn't refresh the screen
367db96d56Sopenharmony_ci    toggle(y,x) -- change the given cell from live to dead, or vice
377db96d56Sopenharmony_ci                   versa, and refresh the screen display
387db96d56Sopenharmony_ci
397db96d56Sopenharmony_ci    """
407db96d56Sopenharmony_ci    def __init__(self, scr, char=ord('*')):
417db96d56Sopenharmony_ci        """Create a new LifeBoard instance.
427db96d56Sopenharmony_ci
437db96d56Sopenharmony_ci        scr -- curses screen object to use for display
447db96d56Sopenharmony_ci        char -- character used to render live cells (default: '*')
457db96d56Sopenharmony_ci        """
467db96d56Sopenharmony_ci        self.state = {}
477db96d56Sopenharmony_ci        self.scr = scr
487db96d56Sopenharmony_ci        Y, X = self.scr.getmaxyx()
497db96d56Sopenharmony_ci        self.X, self.Y = X - 2, Y - 2 - 1
507db96d56Sopenharmony_ci        self.char = char
517db96d56Sopenharmony_ci        self.scr.clear()
527db96d56Sopenharmony_ci
537db96d56Sopenharmony_ci        # Draw a border around the board
547db96d56Sopenharmony_ci        border_line = '+' + (self.X * '-') + '+'
557db96d56Sopenharmony_ci        self.scr.addstr(0, 0, border_line)
567db96d56Sopenharmony_ci        self.scr.addstr(self.Y + 1, 0, border_line)
577db96d56Sopenharmony_ci        for y in range(0, self.Y):
587db96d56Sopenharmony_ci            self.scr.addstr(1 + y, 0, '|')
597db96d56Sopenharmony_ci            self.scr.addstr(1 + y, self.X + 1, '|')
607db96d56Sopenharmony_ci        self.scr.refresh()
617db96d56Sopenharmony_ci
627db96d56Sopenharmony_ci    def set(self, y, x):
637db96d56Sopenharmony_ci        """Set a cell to the live state"""
647db96d56Sopenharmony_ci        if x < 0 or self.X <= x or y < 0 or self.Y <= y:
657db96d56Sopenharmony_ci            raise ValueError("Coordinates out of range %i,%i" % (y, x))
667db96d56Sopenharmony_ci        self.state[x, y] = 1
677db96d56Sopenharmony_ci
687db96d56Sopenharmony_ci    def toggle(self, y, x):
697db96d56Sopenharmony_ci        """Toggle a cell's state between live and dead"""
707db96d56Sopenharmony_ci        if x < 0 or self.X <= x or y < 0 or self.Y <= y:
717db96d56Sopenharmony_ci            raise ValueError("Coordinates out of range %i,%i" % (y, x))
727db96d56Sopenharmony_ci        if (x, y) in self.state:
737db96d56Sopenharmony_ci            del self.state[x, y]
747db96d56Sopenharmony_ci            self.scr.addch(y + 1, x + 1, ' ')
757db96d56Sopenharmony_ci        else:
767db96d56Sopenharmony_ci            self.state[x, y] = 1
777db96d56Sopenharmony_ci            if curses.has_colors():
787db96d56Sopenharmony_ci                # Let's pick a random color!
797db96d56Sopenharmony_ci                self.scr.attrset(curses.color_pair(random.randrange(1, 7)))
807db96d56Sopenharmony_ci            self.scr.addch(y + 1, x + 1, self.char)
817db96d56Sopenharmony_ci            self.scr.attrset(0)
827db96d56Sopenharmony_ci        self.scr.refresh()
837db96d56Sopenharmony_ci
847db96d56Sopenharmony_ci    def erase(self):
857db96d56Sopenharmony_ci        """Clear the entire board and update the board display"""
867db96d56Sopenharmony_ci        self.state = {}
877db96d56Sopenharmony_ci        self.display(update_board=False)
887db96d56Sopenharmony_ci
897db96d56Sopenharmony_ci    def display(self, update_board=True):
907db96d56Sopenharmony_ci        """Display the whole board, optionally computing one generation"""
917db96d56Sopenharmony_ci        M, N = self.X, self.Y
927db96d56Sopenharmony_ci        if not update_board:
937db96d56Sopenharmony_ci            for i in range(0, M):
947db96d56Sopenharmony_ci                for j in range(0, N):
957db96d56Sopenharmony_ci                    if (i, j) in self.state:
967db96d56Sopenharmony_ci                        self.scr.addch(j + 1, i + 1, self.char)
977db96d56Sopenharmony_ci                    else:
987db96d56Sopenharmony_ci                        self.scr.addch(j + 1, i + 1, ' ')
997db96d56Sopenharmony_ci            self.scr.refresh()
1007db96d56Sopenharmony_ci            return
1017db96d56Sopenharmony_ci
1027db96d56Sopenharmony_ci        d = {}
1037db96d56Sopenharmony_ci        self.boring = 1
1047db96d56Sopenharmony_ci        for i in range(0, M):
1057db96d56Sopenharmony_ci            L = range(max(0, i - 1), min(M, i + 2))
1067db96d56Sopenharmony_ci            for j in range(0, N):
1077db96d56Sopenharmony_ci                s = 0
1087db96d56Sopenharmony_ci                live = (i, j) in self.state
1097db96d56Sopenharmony_ci                for k in range(max(0, j - 1), min(N, j + 2)):
1107db96d56Sopenharmony_ci                    for l in L:
1117db96d56Sopenharmony_ci                        if (l, k) in self.state:
1127db96d56Sopenharmony_ci                            s += 1
1137db96d56Sopenharmony_ci                s -= live
1147db96d56Sopenharmony_ci                if s == 3:
1157db96d56Sopenharmony_ci                    # Birth
1167db96d56Sopenharmony_ci                    d[i, j] = 1
1177db96d56Sopenharmony_ci                    if curses.has_colors():
1187db96d56Sopenharmony_ci                        # Let's pick a random color!
1197db96d56Sopenharmony_ci                        self.scr.attrset(curses.color_pair(
1207db96d56Sopenharmony_ci                            random.randrange(1, 7)))
1217db96d56Sopenharmony_ci                    self.scr.addch(j + 1, i + 1, self.char)
1227db96d56Sopenharmony_ci                    self.scr.attrset(0)
1237db96d56Sopenharmony_ci                    if not live:
1247db96d56Sopenharmony_ci                        self.boring = 0
1257db96d56Sopenharmony_ci                elif s == 2 and live:
1267db96d56Sopenharmony_ci                    # Survival
1277db96d56Sopenharmony_ci                    d[i, j] = 1
1287db96d56Sopenharmony_ci                elif live:
1297db96d56Sopenharmony_ci                    # Death
1307db96d56Sopenharmony_ci                    self.scr.addch(j + 1, i + 1, ' ')
1317db96d56Sopenharmony_ci                    self.boring = 0
1327db96d56Sopenharmony_ci        self.state = d
1337db96d56Sopenharmony_ci        self.scr.refresh()
1347db96d56Sopenharmony_ci
1357db96d56Sopenharmony_ci    def make_random(self):
1367db96d56Sopenharmony_ci        "Fill the board with a random pattern"
1377db96d56Sopenharmony_ci        self.state = {}
1387db96d56Sopenharmony_ci        for i in range(0, self.X):
1397db96d56Sopenharmony_ci            for j in range(0, self.Y):
1407db96d56Sopenharmony_ci                if random.random() > 0.5:
1417db96d56Sopenharmony_ci                    self.set(j, i)
1427db96d56Sopenharmony_ci
1437db96d56Sopenharmony_ci
1447db96d56Sopenharmony_cidef erase_menu(stdscr, menu_y):
1457db96d56Sopenharmony_ci    "Clear the space where the menu resides"
1467db96d56Sopenharmony_ci    stdscr.move(menu_y, 0)
1477db96d56Sopenharmony_ci    stdscr.clrtoeol()
1487db96d56Sopenharmony_ci    stdscr.move(menu_y + 1, 0)
1497db96d56Sopenharmony_ci    stdscr.clrtoeol()
1507db96d56Sopenharmony_ci
1517db96d56Sopenharmony_ci
1527db96d56Sopenharmony_cidef display_menu(stdscr, menu_y):
1537db96d56Sopenharmony_ci    "Display the menu of possible keystroke commands"
1547db96d56Sopenharmony_ci    erase_menu(stdscr, menu_y)
1557db96d56Sopenharmony_ci
1567db96d56Sopenharmony_ci    # If color, then light the menu up :-)
1577db96d56Sopenharmony_ci    if curses.has_colors():
1587db96d56Sopenharmony_ci        stdscr.attrset(curses.color_pair(1))
1597db96d56Sopenharmony_ci    stdscr.addstr(menu_y, 4,
1607db96d56Sopenharmony_ci        'Use the cursor keys to move, and space or Enter to toggle a cell.')
1617db96d56Sopenharmony_ci    stdscr.addstr(menu_y + 1, 4,
1627db96d56Sopenharmony_ci        'E)rase the board, R)andom fill, S)tep once or C)ontinuously, Q)uit')
1637db96d56Sopenharmony_ci    stdscr.attrset(0)
1647db96d56Sopenharmony_ci
1657db96d56Sopenharmony_ci
1667db96d56Sopenharmony_cidef keyloop(stdscr):
1677db96d56Sopenharmony_ci    # Clear the screen and display the menu of keys
1687db96d56Sopenharmony_ci    stdscr.clear()
1697db96d56Sopenharmony_ci    stdscr_y, stdscr_x = stdscr.getmaxyx()
1707db96d56Sopenharmony_ci    menu_y = (stdscr_y - 3) - 1
1717db96d56Sopenharmony_ci    display_menu(stdscr, menu_y)
1727db96d56Sopenharmony_ci
1737db96d56Sopenharmony_ci    # If color, then initialize the color pairs
1747db96d56Sopenharmony_ci    if curses.has_colors():
1757db96d56Sopenharmony_ci        curses.init_pair(1, curses.COLOR_BLUE, 0)
1767db96d56Sopenharmony_ci        curses.init_pair(2, curses.COLOR_CYAN, 0)
1777db96d56Sopenharmony_ci        curses.init_pair(3, curses.COLOR_GREEN, 0)
1787db96d56Sopenharmony_ci        curses.init_pair(4, curses.COLOR_MAGENTA, 0)
1797db96d56Sopenharmony_ci        curses.init_pair(5, curses.COLOR_RED, 0)
1807db96d56Sopenharmony_ci        curses.init_pair(6, curses.COLOR_YELLOW, 0)
1817db96d56Sopenharmony_ci        curses.init_pair(7, curses.COLOR_WHITE, 0)
1827db96d56Sopenharmony_ci
1837db96d56Sopenharmony_ci    # Set up the mask to listen for mouse events
1847db96d56Sopenharmony_ci    curses.mousemask(curses.BUTTON1_CLICKED)
1857db96d56Sopenharmony_ci
1867db96d56Sopenharmony_ci    # Allocate a subwindow for the Life board and create the board object
1877db96d56Sopenharmony_ci    subwin = stdscr.subwin(stdscr_y - 3, stdscr_x, 0, 0)
1887db96d56Sopenharmony_ci    board = LifeBoard(subwin, char=ord('*'))
1897db96d56Sopenharmony_ci    board.display(update_board=False)
1907db96d56Sopenharmony_ci
1917db96d56Sopenharmony_ci    # xpos, ypos are the cursor's position
1927db96d56Sopenharmony_ci    xpos, ypos = board.X // 2, board.Y // 2
1937db96d56Sopenharmony_ci
1947db96d56Sopenharmony_ci    # Main loop:
1957db96d56Sopenharmony_ci    while True:
1967db96d56Sopenharmony_ci        stdscr.move(1 + ypos, 1 + xpos)   # Move the cursor
1977db96d56Sopenharmony_ci        c = stdscr.getch()                # Get a keystroke
1987db96d56Sopenharmony_ci        if 0 < c < 256:
1997db96d56Sopenharmony_ci            c = chr(c)
2007db96d56Sopenharmony_ci            if c in ' \n':
2017db96d56Sopenharmony_ci                board.toggle(ypos, xpos)
2027db96d56Sopenharmony_ci            elif c in 'Cc':
2037db96d56Sopenharmony_ci                erase_menu(stdscr, menu_y)
2047db96d56Sopenharmony_ci                stdscr.addstr(menu_y, 6, ' Hit any key to stop continuously '
2057db96d56Sopenharmony_ci                              'updating the screen.')
2067db96d56Sopenharmony_ci                stdscr.refresh()
2077db96d56Sopenharmony_ci                # Activate nodelay mode; getch() will return -1
2087db96d56Sopenharmony_ci                # if no keystroke is available, instead of waiting.
2097db96d56Sopenharmony_ci                stdscr.nodelay(1)
2107db96d56Sopenharmony_ci                while True:
2117db96d56Sopenharmony_ci                    c = stdscr.getch()
2127db96d56Sopenharmony_ci                    if c != -1:
2137db96d56Sopenharmony_ci                        break
2147db96d56Sopenharmony_ci                    stdscr.addstr(0, 0, '/')
2157db96d56Sopenharmony_ci                    stdscr.refresh()
2167db96d56Sopenharmony_ci                    board.display()
2177db96d56Sopenharmony_ci                    stdscr.addstr(0, 0, '+')
2187db96d56Sopenharmony_ci                    stdscr.refresh()
2197db96d56Sopenharmony_ci
2207db96d56Sopenharmony_ci                stdscr.nodelay(0)       # Disable nodelay mode
2217db96d56Sopenharmony_ci                display_menu(stdscr, menu_y)
2227db96d56Sopenharmony_ci
2237db96d56Sopenharmony_ci            elif c in 'Ee':
2247db96d56Sopenharmony_ci                board.erase()
2257db96d56Sopenharmony_ci            elif c in 'Qq':
2267db96d56Sopenharmony_ci                break
2277db96d56Sopenharmony_ci            elif c in 'Rr':
2287db96d56Sopenharmony_ci                board.make_random()
2297db96d56Sopenharmony_ci                board.display(update_board=False)
2307db96d56Sopenharmony_ci            elif c in 'Ss':
2317db96d56Sopenharmony_ci                board.display()
2327db96d56Sopenharmony_ci            else:
2337db96d56Sopenharmony_ci                # Ignore incorrect keys
2347db96d56Sopenharmony_ci                pass
2357db96d56Sopenharmony_ci        elif c == curses.KEY_UP and ypos > 0:
2367db96d56Sopenharmony_ci            ypos -= 1
2377db96d56Sopenharmony_ci        elif c == curses.KEY_DOWN and ypos + 1 < board.Y:
2387db96d56Sopenharmony_ci            ypos += 1
2397db96d56Sopenharmony_ci        elif c == curses.KEY_LEFT and xpos > 0:
2407db96d56Sopenharmony_ci            xpos -= 1
2417db96d56Sopenharmony_ci        elif c == curses.KEY_RIGHT and xpos + 1 < board.X:
2427db96d56Sopenharmony_ci            xpos += 1
2437db96d56Sopenharmony_ci        elif c == curses.KEY_MOUSE:
2447db96d56Sopenharmony_ci            mouse_id, mouse_x, mouse_y, mouse_z, button_state = curses.getmouse()
2457db96d56Sopenharmony_ci            if (mouse_x > 0 and mouse_x < board.X + 1 and
2467db96d56Sopenharmony_ci                mouse_y > 0 and mouse_y < board.Y + 1):
2477db96d56Sopenharmony_ci                xpos = mouse_x - 1
2487db96d56Sopenharmony_ci                ypos = mouse_y - 1
2497db96d56Sopenharmony_ci                board.toggle(ypos, xpos)
2507db96d56Sopenharmony_ci            else:
2517db96d56Sopenharmony_ci                # They've clicked outside the board
2527db96d56Sopenharmony_ci                curses.flash()
2537db96d56Sopenharmony_ci        else:
2547db96d56Sopenharmony_ci            # Ignore incorrect keys
2557db96d56Sopenharmony_ci            pass
2567db96d56Sopenharmony_ci
2577db96d56Sopenharmony_ci
2587db96d56Sopenharmony_cidef main(stdscr):
2597db96d56Sopenharmony_ci    keyloop(stdscr)                 # Enter the main loop
2607db96d56Sopenharmony_ci
2617db96d56Sopenharmony_ciif __name__ == '__main__':
2627db96d56Sopenharmony_ci    curses.wrapper(main)
263