blob: b4797bf0af5d8836231ff3e865ef6082d98bc807 [file] [log] [blame]
Andrew M. Kuchlinga3b5a5f2000-12-13 03:50:20 +00001#!/usr/bin/env python
2# life.py -- A curses-based version of Conway's Game of Life.
3# Contributed by A.M. Kuchling <amk1@bigfoot.com>
4#
5# An empty board will be displayed, and the following commands are available:
6# E : Erase the board
7# R : Fill the board randomly
8# S : Step for a single generation
9# C : Update continuously until a key is struck
10# Q : Quit
11# Cursor keys : Move the cursor around the board
12# Space or Enter : Toggle the contents of the cursor's position
13#
14# TODO :
15# Support the mouse
16# Use colour if available
17# Make board updates faster
18#
19
20class LifeBoard:
21 """Encapsulates a Life board
22
23 Attributes:
24 X,Y : horizontal and vertical size of the board
25 state : dictionary mapping (x,y) to 0 or 1
26
27 Methods:
28 display(update_board) -- If update_board is true, compute the
29 next generation. Then display the state
30 of the board and refresh the screen.
31 erase() -- clear the entire board
32 makeRandom() -- fill the board randomly
33 set(y,x) -- set the given cell to Live; doesn't refresh the screen
34 toggle(y,x) -- change the given cell from live to dead, or vice
35 versa, and refresh the screen display
36
37 """
38 def __init__(self, scr, char=ord('*')):
39 """Create a new LifeBoard instance.
40
41 scr -- curses screen object to use for display
42 char -- character used to render live cells (default: '*')
43 """
44 self.state={} ; self.scr=scr
45 Y, X = self.scr.getmaxyx()
46 self.X, self.Y = X-2, Y-2-1
47 self.char = char
48 self.scr.clear()
49
50 # Draw a border around the board
51 border_line='+'+(self.X*'-')+'+'
52 self.scr.addstr(0, 0, border_line)
53 self.scr.addstr(self.Y+1,0, border_line)
54 for y in range(0, self.Y):
55 self.scr.addstr(1+y, 0, '|')
56 self.scr.addstr(1+y, self.X+1, '|')
57 self.scr.refresh()
58
59 def set(self, y, x):
60 """Set a cell to the live state"""
61 if x<0 or self.X<=x or y<0 or self.Y<=y:
62 raise ValueError, "Coordinates out of range %i,%i"% (y,x)
63 self.state[x,y] = 1
64
65 def toggle(self, y, x):
66 """Toggle a cell's state between live and dead"""
67 if x<0 or self.X<=x or y<0 or self.Y<=y:
68 raise ValueError, "Coordinates out of range %i,%i"% (y,x)
69 if self.state.has_key( (x,y) ):
70 del self.state[x,y]
71 self.scr.addch(y+1, x+1, ' ')
72 else:
73 self.state[x,y]=1
74 self.scr.addch(y+1, x+1, self.char)
75 self.scr.refresh()
76
77 def erase(self):
78 """Clear the entire board and update the board display"""
79 self.state={}
80 self.display(update_board=0)
81
82 def display(self, update_board=1):
83 """Display the whole board, optionally computing one generation"""
84 M,N = self.X, self.Y
85 if not update_board:
86 for i in range(0, M):
87 for j in range(0, N):
88 if self.state.has_key( (i,j) ):
89 self.scr.addch(j+1, i+1, self.char)
90 else:
91 self.scr.addch(j+1, i+1, ' ')
92 self.scr.refresh()
93 return
94
95 d={} ; self.boring=1
96 for i in range(0, M):
97 L=range( max(0, i-1), min(M, i+2) )
98 for j in range(0, N):
99 s=0
100 live=self.state.has_key( (i,j) )
101 for k in range( max(0, j-1), min(N, j+2) ):
102 for l in L:
103 if self.state.has_key( (l,k) ):
104 s=s+1
105 s=s-live
106 if s==3:
107 # Birth
108 d[i,j]=1
109 self.scr.addch(j+1, i+1, self.char)
110 if not live: self.boring=0
111 elif s==2 and live: d[i,j]=1 # Survival
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
119 def makeRandom(self):
120 "Fill the board with a random pattern"
121 import whrandom
122 self.state={}
123 for i in range(0, self.X):
124 for j in range(0, self.Y):
125 if whrandom.random()*10>5.0: self.set(j,i)
126
127
128def erase_menu(stdscr, menu_y):
129 "Clear the space where the menu resides"
130 stdscr.move(menu_y, 0) ; stdscr.clrtoeol()
131 stdscr.move(menu_y+1, 0) ; stdscr.clrtoeol()
132
133def display_menu(stdscr, menu_y):
134 "Display the menu of possible keystroke commands"
135 erase_menu(stdscr, menu_y)
136 stdscr.addstr(menu_y, 4,
137 'Use the cursor keys to move, and space or Enter to toggle a cell.')
138 stdscr.addstr(menu_y+1, 4,
139 'E)rase the board, R)andom fill, S)tep once or C)ontinuously, Q)uit')
140
141def main(stdscr):
142 import string, curses
143
144 # Clear the screen and display the menu of keys
145 stdscr.clear()
146 stdscr_y, stdscr_x = stdscr.getmaxyx()
147 menu_y=(stdscr_y-3)-1
148 display_menu(stdscr, menu_y)
149
150 # Allocate a subwindow for the Life board and create the board object
151 subwin=stdscr.subwin(stdscr_y-3, stdscr_x, 0, 0)
152 board=LifeBoard(subwin, char=ord('*'))
153 board.display(update_board=0)
154
155 # xpos, ypos are the cursor's position
156 xpos, ypos = board.X/2, board.Y/2
157
158 # Main loop:
159 while (1):
160 stdscr.move(1+ypos, 1+xpos) # Move the cursor
161 c=stdscr.getch() # Get a keystroke
162 if 0<c<256:
163 c=chr(c)
164 if c in ' \n':
165 board.toggle(ypos, xpos)
166 elif c in 'Cc':
167 erase_menu(stdscr, menu_y)
168 stdscr.addstr(menu_y, 6, ' Hit any key to stop continuously '
169 'updating the screen.')
170 stdscr.refresh()
171 # Activate nodelay mode; getch() will return -1
172 # if no keystroke is available, instead of waiting.
173 stdscr.nodelay(1)
174 while (1):
175 c=stdscr.getch()
176 if c!=-1: break
177 stdscr.addstr(0,0, '/'); stdscr.refresh()
178 board.display()
179 stdscr.addstr(0,0, '+'); stdscr.refresh()
180
181 stdscr.nodelay(0) # Disable nodelay mode
182 display_menu(stdscr, menu_y)
183
184 elif c in 'Ee': board.erase()
185 elif c in 'Qq': break
186 elif c in 'Rr':
187 board.makeRandom()
188 board.display(update_board=0)
189 elif c in 'Ss':
190 board.display()
191 else: pass # Ignore incorrect keys
192 elif c==curses.KEY_UP and ypos>0: ypos=ypos-1
193 elif c==curses.KEY_DOWN and ypos<board.Y-1: ypos=ypos+1
194 elif c==curses.KEY_LEFT and xpos>0: xpos=xpos-1
195 elif c==curses.KEY_RIGHT and xpos<board.X-1: xpos=xpos+1
196 else: pass # Ignore incorrect keys
197
198if __name__=='__main__':
199 import curses, traceback
200 try:
201 # Initialize curses
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()
206
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
210 stdscr.keypad(1)
211 main(stdscr) # Enter the main loop
212 # Set everything back to normal
213 stdscr.keypad(0)
214 curses.echo() ; curses.nocbreak()
215 curses.endwin() # Terminate curses
216 except:
217 # In the event of an error, restore the terminal
218 # to a sane state.
219 stdscr.keypad(0)
220 curses.echo() ; curses.nocbreak()
221 curses.endwin()
222 traceback.print_exc() # Print the exception
223