summaryrefslogtreecommitdiff
path: root/cursesmodule/lifedemo.py
blob: 2a8a69b09f2a13358fbf014e34e837709f2fbd6a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#!/usr/bin/env python
# life.py -- A curses-based version of Conway's Game of Life.
# Contributed by A.M. Kuchling <amk@magnet.com>
#
# 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
#
# TODO : 
#   Support the mouse
#   Use colour if available
#   Make board updates faster
#

class LifeBoard:
    """Encapsulates a Life board

    Attributes:
    M,N : horizontal and vertical size of the board

    Methods:
    display(update_board) -- If update_board is true, compute the 
                             next generation.  Then display the state
			     of the board and refresh the screen.
    erase() -- clear the entire board
    makeRandom() -- fill the board randomly
    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('*')):
	"""Return a LifeBoard instance.

	scr -- curses screen object to use for display
	char -- character used to render live cells (default: '*')

	"""
	self.state={} ; self.scr=scr
	N, M = self.scr.getmaxyx()
	self.M, self.N = M-2, N-2-1
	self.char = char
	self.scr.clear()	

	# Draw a border around the board
	border_line='+'+(self.M*'-')+'+'
	self.scr.addstr(0, 0, border_line)
	self.scr.addstr(self.N+1,0, border_line)
	for y in range(0, self.N): 
	    self.scr.addstr(1+y, 0, '|') 
	    self.scr.addstr(1+y, self.M+1, '|')
	self.scr.refresh()
    def set(self, y, x): 
	"""Set a cell to the live state"""
	if x<0 or self.M<=x or y<0 or self.N<=y:
	    raise ValueError, "Coordinates out of range %i,%i"% (y,x)
	self.state[x,y] = 1
    def toggle(self, y, x): 
	"""Toggle a cell's state between live and dead"""
	if x<0 or self.M<=x or y<0 or self.N<=y:
	    raise ValueError, "Coordinates out of range %i,%i"% (y,x)
	if self.state.has_key(x,y): 
	    del self.state[x,y]
	    self.scr.addch(y+1, x+1, ' ')
	else:
	    self.state[x,y]=1
	    self.scr.addch(y+1, x+1, self.char)
	self.scr.refresh()
    def erase(self):
	"""Clear the entire board and update the board display"""
	self.state={}
	self.display(update_board=0)
    def display(self, update_board=1):
	"""Display the whole board, optionally computing one generation"""
	M,N = self.M, self.N 
	if not update_board:
	    for i in range(0, M):
		for j in range(0, N):
			if self.state.has_key( (i,j) ): 
			    self.scr.addch(j+1, i+1, self.char)
			else:
			    self.scr.addch(j+1, i+1, ' ')
	    self.scr.refresh()
	    return

	d={} ; self.boring=1
	for i in range(0, M):
	    L=range( max(0, i-1), min(M, i+2) )
	    for j in range(0, N):
		s=0
		live=self.state.has_key( (i,j) )
		for k in range( max(0, j-1), min(N, j+2) ):
		    for l in L:
			if self.state.has_key( (l,k) ):
			    s=s+1
		s=s-live
		if s==3: 
		    # Birth
		    d[i,j]=1 
		    self.scr.addch(j+1, i+1, self.char)
		    if not live: self.boring=0  
		elif s==2 and live: d[i,j]=1       # Survival
		elif live: 
		    # Death
		    self.scr.addch(j+1, i+1, ' ')
		    self.boring=0
	self.state=d
	self.scr.refresh()
    def makeRandom(self):
	"""Fill the board with a random pattern"""
	import whrandom
	self.state={}
	for i in range(0, self.M): 
	    for j in range(0, self.N):
		if whrandom.random()*10>5.0: self.set(j,i)


def erase_menu(stdscr, menu_y):
    "Clear the space where the menu resides"
    stdscr.move(menu_y, 0) ; stdscr.clrtoeol()
    stdscr.move(menu_y+1, 0) ; stdscr.clrtoeol()

def display_menu(stdscr, menu_y):
    "Display the menu of possible keystroke commands"
    erase_menu(stdscr, menu_y)
    stdscr.addstr(menu_y, 4, 'Use the cursor keys to move, and space or Enter to toggle a cell.')
    stdscr.addstr(menu_y+1, 4, 'E)rase the board, R)andom fill, S)tep once or C)ontinuously, Q)uit')

def main(stdscr):
    import string, curses

    # Clear the screen and dispaly the menu of keys
    stdscr.clear()
    stdscr_y, stdscr_x = stdscr.getmaxyx()
    menu_y=(stdscr_y-3)-1
    display_menu(stdscr, menu_y)

    # Allocate a subwindow for the Life board and create the board object
    subwin=stdscr.subwin(stdscr_y-3, stdscr_x, 0, 0) 
    board=LifeBoard(subwin, char=ord('*'))
    board.display(update_board=0)
    # xpos, ypos are the cursor's position
    xpos, ypos = board.M/2, board.N/2

    # Main loop:
    while (1):
	stdscr.move(1+ypos, 1+xpos)     # Move the cursor
	c=stdscr.getch()		# Get a keystroke
	if 0<c<256:
	    c=chr(c)
	    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)   
		while (1):
		    c=stdscr.getch()
		    if c!=-1: break
		    stdscr.addstr(0,0, '/'); stdscr.refresh()
		    board.display()
		    stdscr.addstr(0,0, '+'); stdscr.refresh()

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

	    elif c in 'Ee': board.erase()
	    elif c in 'Qq': break
	    elif c in 'Rr': 
		board.makeRandom()
		board.display(update_board=0)
	    elif c in 'Ss':
		board.display()
	    else: pass                  # Ignore incorrect keys
	elif c==curses.key_up and ypos>0: 	     ypos=ypos-1
	elif c==curses.key_down and ypos<board.N-1:  ypos=ypos+1
	elif c==curses.key_left and xpos>0:          xpos=xpos-1
	elif c==curses.key_right and xpos<board.M-1: xpos=xpos+1
        else: pass			# Ignore incorrect keys

if __name__=='__main__':
    import curses, traceback
    try:
	# Initialize curses
	stdscr=curses.initscr()
	# Turn off echoing of keys, and enter cbreak mode,
	# where no buffering is performed on keyboard input
	curses.noecho() ; curses.cbreak()

	# In keypad mode, escape sequences for special keys
	# (like the cursor keys) will be interpreted and
	# a special value like curses.key_left will be returned
	stdscr.keypad(1)
	main(stdscr)			# Enter the main loop
	# Set everything back to normal
	stdscr.keypad(0)
	curses.echo() ; curses.nocbreak()
	curses.endwin()			# Terminate curses
    except:
        # In the event of an error, restore the terminal
	# to a sane state.
	stdscr.keypad(0)
	curses.echo() ; curses.nocbreak()
	curses.endwin()
	traceback.print_exc()		# Print the exception