blob: 4ce09d2f24fca186bb32deab52a08671b89dad9f [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.
Andrew M. Kuchling10f9c072001-11-05 21:25:42 +00003# Contributed by AMK
Andrew M. Kuchlinga3b5a5f2000-12-13 03:50:20 +00004#
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
Andrew M. Kuchling9a624482002-04-10 14:50:16 +000020import random, string, traceback
21import curses
22
Andrew M. Kuchlinga3b5a5f2000-12-13 03:50:20 +000023class LifeBoard:
24 """Encapsulates a Life board
25
26 Attributes:
27 X,Y : horizontal and vertical size of the board
28 state : dictionary mapping (x,y) to 0 or 1
29
30 Methods:
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
39
40 """
41 def __init__(self, scr, char=ord('*')):
42 """Create a new LifeBoard instance.
43
44 scr -- curses screen object to use for display
45 char -- character used to render live cells (default: '*')
46 """
47 self.state={} ; 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 self.state.has_key( (x,y) ):
73 del self.state[x,y]
74 self.scr.addch(y+1, x+1, ' ')
75 else:
76 self.state[x,y]=1
77 self.scr.addch(y+1, x+1, self.char)
78 self.scr.refresh()
79
80 def erase(self):
81 """Clear the entire board and update the board display"""
82 self.state={}
83 self.display(update_board=0)
84
85 def display(self, update_board=1):
86 """Display the whole board, optionally computing one generation"""
87 M,N = self.X, self.Y
88 if not update_board:
89 for i in range(0, M):
90 for j in range(0, N):
91 if self.state.has_key( (i,j) ):
92 self.scr.addch(j+1, i+1, self.char)
93 else:
94 self.scr.addch(j+1, i+1, ' ')
95 self.scr.refresh()
96 return
97
98 d={} ; self.boring=1
99 for i in range(0, M):
100 L=range( max(0, i-1), min(M, i+2) )
101 for j in range(0, N):
102 s=0
103 live=self.state.has_key( (i,j) )
104 for k in range( max(0, j-1), min(N, j+2) ):
105 for l in L:
106 if self.state.has_key( (l,k) ):
107 s=s+1
108 s=s-live
109 if s==3:
110 # Birth
111 d[i,j]=1
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
115 elif live:
116 # Death
117 self.scr.addch(j+1, i+1, ' ')
118 self.boring=0
119 self.state=d
120 self.scr.refresh()
121
122 def makeRandom(self):
123 "Fill the board with a random pattern"
Andrew M. Kuchlinga3b5a5f2000-12-13 03:50:20 +0000124 self.state={}
125 for i in range(0, self.X):
126 for j in range(0, self.Y):
Andrew M. Kuchling9a624482002-04-10 14:50:16 +0000127 if random.random() > 0.5: self.set(j,i)
Andrew M. Kuchlinga3b5a5f2000-12-13 03:50:20 +0000128
129
130def 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()
134
135def 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')
142
143def main(stdscr):
Andrew M. Kuchlinga3b5a5f2000-12-13 03:50:20 +0000144
145 # Clear the screen and display the menu of keys
146 stdscr.clear()
147 stdscr_y, stdscr_x = stdscr.getmaxyx()
148 menu_y=(stdscr_y-3)-1
149 display_menu(stdscr, menu_y)
150
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)
155
156 # xpos, ypos are the cursor's position
157 xpos, ypos = board.X/2, board.Y/2
158
159 # Main loop:
160 while (1):
161 stdscr.move(1+ypos, 1+xpos) # Move the cursor
162 c=stdscr.getch() # Get a keystroke
163 if 0<c<256:
164 c=chr(c)
165 if c in ' \n':
166 board.toggle(ypos, xpos)
167 elif c in 'Cc':
168 erase_menu(stdscr, menu_y)
169 stdscr.addstr(menu_y, 6, ' Hit any key to stop continuously '
170 'updating the screen.')
171 stdscr.refresh()
172 # Activate nodelay mode; getch() will return -1
173 # if no keystroke is available, instead of waiting.
174 stdscr.nodelay(1)
175 while (1):
176 c=stdscr.getch()
177 if c!=-1: break
178 stdscr.addstr(0,0, '/'); stdscr.refresh()
179 board.display()
180 stdscr.addstr(0,0, '+'); stdscr.refresh()
181
182 stdscr.nodelay(0) # Disable nodelay mode
183 display_menu(stdscr, menu_y)
184
185 elif c in 'Ee': board.erase()
186 elif c in 'Qq': break
187 elif c in 'Rr':
188 board.makeRandom()
189 board.display(update_board=0)
190 elif c in 'Ss':
191 board.display()
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
198
199if __name__=='__main__':
Andrew M. Kuchlinga3b5a5f2000-12-13 03:50:20 +0000200 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