life.py 8.59 KB
Newer Older
1
#!/usr/bin/env python3
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

"""
A curses-based version of Conway's Game of Life.

An empty board will be displayed, and the following commands are available:
 E : Erase the board
 R : Fill the board randomly
 S : Step for a single generation
 C : Update continuously until a key is struck
 Q : Quit
 Cursor keys :  Move the cursor around the board
 Space or Enter : Toggle the contents of the cursor's position

Contributed by Andrew Kuchling, Mouse support and color by Dafydd Crosby.
"""

18
import curses
19 20
import random

21

22 23 24 25 26 27
class LifeBoard:
    """Encapsulates a Life board

    Attributes:
    X,Y : horizontal and vertical size of the board
    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 36 37 38 39 40
    set(y,x) -- set the given cell to Live; doesn't refresh the screen
    toggle(y,x) -- change the given cell from live to dead, or vice
                   versa, and refresh the screen display

    """
    def __init__(self, scr, char=ord('*')):
41 42 43 44 45
        """Create a new LifeBoard instance.

        scr -- curses screen object to use for display
        char -- character used to render live cells (default: '*')
        """
46 47
        self.state = {}
        self.scr = scr
48 49 50 51 52 53
        Y, X = self.scr.getmaxyx()
        self.X, self.Y = X-2, Y-2-1
        self.char = char
        self.scr.clear()

        # 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 58 59 60 61 62 63 64
        for y in range(0, self.Y):
            self.scr.addstr(1+y, 0, '|')
            self.scr.addstr(1+y, self.X+1, '|')
        self.scr.refresh()

    def set(self, y, x):
        """Set a cell to the live state"""
        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 67 68 69
        self.state[x,y] = 1

    def toggle(self, y, x):
        """Toggle a cell's state between live and dead"""
70 71 72 73
        if x < 0 or self.X <= x or y < 0 or self.Y <= y:
            raise ValueError("Coordinates out of range %i,%i"% (y, x))
        if (x, y) in self.state:
            del self.state[x, y]
74 75
            self.scr.addch(y+1, x+1, ' ')
        else:
76
            self.state[x, y] = 1
77
            if curses.has_colors():
78 79
                # Let's pick a random color!
                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 87
        self.state = {}
        self.display(update_board=False)
88

89
    def display(self, update_board=True):
90 91 92 93 94
        """Display the whole board, optionally computing one generation"""
        M,N = self.X, self.Y
        if not update_board:
            for i in range(0, M):
                for j in range(0, N):
95
                    if (i,j) in self.state:
96 97 98 99 100 101
                        self.scr.addch(j+1, i+1, self.char)
                    else:
                        self.scr.addch(j+1, i+1, ' ')
            self.scr.refresh()
            return

102 103
        d = {}
        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 110
                for k in range( max(0, j-1), min(N, j+2) ):
                    for l in L:
111
                        if (l,k) in self.state:
112 113 114
                            s += 1
                s -= live
                if s == 3:
115
                    # Birth
116
                    d[i,j] = 1
117
                    if curses.has_colors():
118 119 120
                        # Let's pick a random color!
                        self.scr.attrset(curses.color_pair(
                            random.randrange(1, 7)))
121
                    self.scr.addch(j+1, i+1, self.char)
122
                    self.scr.attrset(0)
123 124
                    if not live: self.boring = 0
                elif s == 2 and live: d[i,j] = 1       # Survival
125 126 127
                elif live:
                    # Death
                    self.scr.addch(j+1, i+1, ' ')
128 129
                    self.boring = 0
        self.state = d
130
        self.scr.refresh()
131

132
    def make_random(self):
133
        "Fill the board with a random pattern"
134
        self.state = {}
135
        for i in range(0, self.X):
136
            for j in range(0, self.Y):
137 138
                if random.random() > 0.5:
                    self.set(j,i)
139 140 141 142


def erase_menu(stdscr, menu_y):
    "Clear the space where the menu resides"
143 144 145 146
    stdscr.move(menu_y, 0)
    stdscr.clrtoeol()
    stdscr.move(menu_y+1, 0)
    stdscr.clrtoeol()
147 148 149 150

def display_menu(stdscr, menu_y):
    "Display the menu of possible keystroke commands"
    erase_menu(stdscr, menu_y)
151

Senthil Kumaran's avatar
Senthil Kumaran committed
152
    # If color, then light the menu up :-)
153 154
    if curses.has_colors():
        stdscr.attrset(curses.color_pair(1))
155
    stdscr.addstr(menu_y, 4,
156
        'Use the cursor keys to move, and space or Enter to toggle a cell.')
157
    stdscr.addstr(menu_y+1, 4,
158
        'E)rase the board, R)andom fill, S)tep once or C)ontinuously, Q)uit')
159
    stdscr.attrset(0)
160

161
def keyloop(stdscr):
162 163 164
    # Clear the screen and display the menu of keys
    stdscr.clear()
    stdscr_y, stdscr_x = stdscr.getmaxyx()
165
    menu_y = (stdscr_y-3)-1
166 167
    display_menu(stdscr, menu_y)

Senthil Kumaran's avatar
Senthil Kumaran committed
168
    # If color, then initialize the color pairs
169 170 171 172 173 174 175 176 177 178 179 180
    if curses.has_colors():
        curses.init_pair(1, curses.COLOR_BLUE, 0)
        curses.init_pair(2, curses.COLOR_CYAN, 0)
        curses.init_pair(3, curses.COLOR_GREEN, 0)
        curses.init_pair(4, curses.COLOR_MAGENTA, 0)
        curses.init_pair(5, curses.COLOR_RED, 0)
        curses.init_pair(6, curses.COLOR_YELLOW, 0)
        curses.init_pair(7, curses.COLOR_WHITE, 0)

    # Set up the mask to listen for mouse events
    curses.mousemask(curses.BUTTON1_CLICKED)

181
    # Allocate a subwindow for the Life board and create the board object
182 183 184
    subwin = stdscr.subwin(stdscr_y-3, stdscr_x, 0, 0)
    board = LifeBoard(subwin, char=ord('*'))
    board.display(update_board=False)
185 186

    # xpos, ypos are the cursor's position
187
    xpos, ypos = board.X//2, board.Y//2
188 189

    # Main loop:
190
    while True:
191
        stdscr.move(1+ypos, 1+xpos)     # Move the cursor
192
        c = stdscr.getch()                # Get a keystroke
193
        if 0 < c < 256:
194
            c = chr(c)
195 196 197 198 199 200 201 202 203 204
            if c in ' \n':
                board.toggle(ypos, xpos)
            elif c in 'Cc':
                erase_menu(stdscr, menu_y)
                stdscr.addstr(menu_y, 6, ' Hit any key to stop continuously '
                              'updating the screen.')
                stdscr.refresh()
                # Activate nodelay mode; getch() will return -1
                # if no keystroke is available, instead of waiting.
                stdscr.nodelay(1)
205
                while True:
206 207 208
                    c = stdscr.getch()
                    if c != -1:
                        break
209
                    stdscr.addstr(0, 0, '/')
210
                    stdscr.refresh()
211
                    board.display()
212
                    stdscr.addstr(0, 0, '+')
213
                    stdscr.refresh()
214 215 216 217

                stdscr.nodelay(0)       # Disable nodelay mode
                display_menu(stdscr, menu_y)

218 219 220 221
            elif c in 'Ee':
                board.erase()
            elif c in 'Qq':
                break
222
            elif c in 'Rr':
223
                board.make_random()
224
                board.display(update_board=False)
225 226 227
            elif c in 'Ss':
                board.display()
            else: pass                  # Ignore incorrect keys
228 229 230 231
        elif c == curses.KEY_UP and ypos > 0:            ypos -= 1
        elif c == curses.KEY_DOWN and ypos < board.Y-1:  ypos += 1
        elif c == curses.KEY_LEFT and xpos > 0:          xpos -= 1
        elif c == curses.KEY_RIGHT and xpos < board.X-1: xpos += 1
232
        elif c == curses.KEY_MOUSE:
233 234 235
            mouse_id, mouse_x, mouse_y, mouse_z, button_state = curses.getmouse()
            if (mouse_x > 0 and mouse_x < board.X+1 and
                mouse_y > 0 and mouse_y < board.Y+1):
236 237 238 239 240 241
                xpos = mouse_x - 1
                ypos = mouse_y - 1
                board.toggle(ypos, xpos)
            else:
                # They've clicked outside the board
                curses.flash()
242 243 244 245 246 247 248 249 250 251
        else:
            # Ignore incorrect keys
            pass


def main(stdscr):
    keyloop(stdscr)                    # Enter the main loop

if __name__ == '__main__':
    curses.wrapper(main)