2 # life.py -- A curses-based version of Conway's Game of Life.
5 # An empty board will be displayed, and the following commands are available:
7 # R : Fill the board randomly
8 # S : Step for a single generation
9 # C : Update continuously until a key is struck
11 # Cursor keys : Move the cursor around the board
12 # Space or Enter : Toggle the contents of the cursor's position
16 # Use colour if available
17 # Make board updates faster
20 import random
, string
, traceback
24 """Encapsulates a Life board
27 X,Y : horizontal and vertical size of the board
28 state : dictionary mapping (x,y) to 0 or 1
31 display(update_board) -- If update_board is true, compute the
32 next generation. Then display the state
33 of the board and refresh the screen.
34 erase() -- clear the entire board
35 makeRandom() -- fill the board randomly
36 set(y,x) -- set the given cell to Live; doesn't refresh the screen
37 toggle(y,x) -- change the given cell from live to dead, or vice
38 versa, and refresh the screen display
41 def __init__(self
, scr
, char
=ord('*')):
42 """Create a new LifeBoard instance.
44 scr -- curses screen object to use for display
45 char -- character used to render live cells (default: '*')
47 self
.state
={} ; self
.scr
=scr
48 Y
, X
= self
.scr
.getmaxyx()
49 self
.X
, self
.Y
= X
-2, Y
-2-1
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, '|')
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
)
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 self
.state
.has_key( (x
,y
) ):
74 self
.scr
.addch(y
+1, x
+1, ' ')
77 self
.scr
.addch(y
+1, x
+1, self
.char
)
81 """Clear the entire board and update the board display"""
83 self
.display(update_board
=0)
85 def display(self
, update_board
=1):
86 """Display the whole board, optionally computing one generation"""
91 if self
.state
.has_key( (i
,j
) ):
92 self
.scr
.addch(j
+1, i
+1, self
.char
)
94 self
.scr
.addch(j
+1, i
+1, ' ')
100 L
=range( max(0, i
-1), min(M
, i
+2) )
101 for j
in range(0, N
):
103 live
=self
.state
.has_key( (i
,j
) )
104 for k
in range( max(0, j
-1), min(N
, j
+2) ):
106 if self
.state
.has_key( (l
,k
) ):
112 self
.scr
.addch(j
+1, i
+1, self
.char
)
113 if not live
: self
.boring
=0
114 elif s
==2 and live
: d
[i
,j
]=1 # Survival
117 self
.scr
.addch(j
+1, i
+1, ' ')
122 def makeRandom(self
):
123 "Fill the board with a random pattern"
125 for i
in range(0, self
.X
):
126 for j
in range(0, self
.Y
):
127 if random
.random() > 0.5: self
.set(j
,i
)
130 def erase_menu(stdscr
, menu_y
):
131 "Clear the space where the menu resides"
132 stdscr
.move(menu_y
, 0) ; stdscr
.clrtoeol()
133 stdscr
.move(menu_y
+1, 0) ; stdscr
.clrtoeol()
135 def display_menu(stdscr
, menu_y
):
136 "Display the menu of possible keystroke commands"
137 erase_menu(stdscr
, menu_y
)
138 stdscr
.addstr(menu_y
, 4,
139 'Use the cursor keys to move, and space or Enter to toggle a cell.')
140 stdscr
.addstr(menu_y
+1, 4,
141 'E)rase the board, R)andom fill, S)tep once or C)ontinuously, Q)uit')
145 # Clear the screen and display the menu of keys
147 stdscr_y
, stdscr_x
= stdscr
.getmaxyx()
148 menu_y
=(stdscr_y
-3)-1
149 display_menu(stdscr
, menu_y
)
151 # Allocate a subwindow for the Life board and create the board object
152 subwin
=stdscr
.subwin(stdscr_y
-3, stdscr_x
, 0, 0)
153 board
=LifeBoard(subwin
, char
=ord('*'))
154 board
.display(update_board
=0)
156 # xpos, ypos are the cursor's position
157 xpos
, ypos
= board
.X
/2, board
.Y
/2
161 stdscr
.move(1+ypos
, 1+xpos
) # Move the cursor
162 c
=stdscr
.getch() # Get a keystroke
166 board
.toggle(ypos
, xpos
)
168 erase_menu(stdscr
, menu_y
)
169 stdscr
.addstr(menu_y
, 6, ' Hit any key to stop continuously '
170 'updating the screen.')
172 # Activate nodelay mode; getch() will return -1
173 # if no keystroke is available, instead of waiting.
178 stdscr
.addstr(0,0, '/'); stdscr
.refresh()
180 stdscr
.addstr(0,0, '+'); stdscr
.refresh()
182 stdscr
.nodelay(0) # Disable nodelay mode
183 display_menu(stdscr
, menu_y
)
185 elif c
in 'Ee': board
.erase()
186 elif c
in 'Qq': break
189 board
.display(update_board
=0)
192 else: pass # Ignore incorrect keys
193 elif c
==curses
.KEY_UP
and ypos
>0: ypos
=ypos
-1
194 elif c
==curses
.KEY_DOWN
and ypos
<board
.Y
-1: ypos
=ypos
+1
195 elif c
==curses
.KEY_LEFT
and xpos
>0: xpos
=xpos
-1
196 elif c
==curses
.KEY_RIGHT
and xpos
<board
.X
-1: xpos
=xpos
+1
197 else: pass # Ignore incorrect keys
199 if __name__
=='__main__':
202 stdscr
=curses
.initscr()
203 # Turn off echoing of keys, and enter cbreak mode,
204 # where no buffering is performed on keyboard input
205 curses
.noecho() ; curses
.cbreak()
207 # In keypad mode, escape sequences for special keys
208 # (like the cursor keys) will be interpreted and
209 # a special value like curses.KEY_LEFT will be returned
211 main(stdscr
) # Enter the main loop
212 # Set everything back to normal
214 curses
.echo() ; curses
.nocbreak()
215 curses
.endwin() # Terminate curses
217 # In the event of an error, restore the terminal
220 curses
.echo() ; curses
.nocbreak()
222 traceback
.print_exc() # Print the exception