Here's an version of Conway's Game of Life I recently updated to use Snit's Not Incr Tcl; I'm posting it now as an example of application development using Snit. -- WHD
10/21/2002: To update this for Snit V0.7, I replaced "argv" with "args" throughout. -- WHD
#----------------------------------------------------------------------- # TITLE: # life.tcl # # AUTHOR: # Will Duquette # # DESCRIPTION: # life.tcl implements John Conway's classic Game of Life, one of # the first experiments with what's now called artificial life. # # Think of a plane divided up into squares like a checker board. # Each square can hold a one-celled animal. You start the game # by placing cells in squares. Then you watch the cells breed # and die through successive generations. The game is to find # starting patterns that do interesting things. # # Each square on the board has 8 neighbor squares; cells breed # and die based on how crowded they are, i.e., the number of # neighbors they have. Each new generation is computed as # follows: # # For each square, count the number of neighbor cells. # If the square is empty, and it has exactly 3 neighbor cells, a # new cell will be born there. If the square has a cell in it, # and the cell has less than 2 or more than 3 neighbor cells, # the cell will die. All of the counting is done first, and # then the new cells are added and the dead cell are removed all # at once. # # This GUI implementation allows cells to be added and removed # by clicking on the board. A generation passes when the # "Generate" button is clicked, or when the player presses the # Return key. # # The implementation has two pieces: # # 1. The board, which contains cells that can be turned on # and off and knows how to compute a new generation. The board # includes its own GUI display code. # # 2. The rest of the GUI. # # If the code were written for reuse, the board would be split into # two pieces: a generic gameboard suitable for Life, Othello, and # similar games, and Life code that uses the board. package require snit #----------------------------------------------------------------------- # The Board # # The Board is implemented as a Tk canvas widget, broken up into # squares on an NxN grid. The background is white and the grid # lines are cyan. Each square holds a circle object which # can be set to any desired color, normally white (for dead cells) and # forestgreen (for living cells). Each circle has a tag "i,j" so that # it can be manipulated individually. snit::widget board { # By default, 20x20 cells option -cells 20 # By default, each cell is 20x20 pixels option -pixels 20 # Milliseconds between generations option -delay 200 # For each cell on the board, this array remembers # whether the cell is alive or dead, and the coordinates of its # neighbors; this allows the neighbors to be counted more quickly. variable data # Remembers the list of i,j indices, to save time while generating. variable indices {} # True if we're completely constructed, false otherwise. variable constructed 0 constructor {args} { # FIRST, create the canvas. Then configure the options. component hull is [canvas $self -background white] $self configurelist $args # NEXT, set up the board. $self SetupBoard # No longer constructing. set constructed 1 } onconfigure -cells {value} { set options(-cells) $value if {$constructed} { $self SetupBoard } } onconfigure -pixels {value} { set options(-pixels) $value if {$constructed} { $self SetupBoard } } # This is still a canvas; delegate other methods and options to it: delegate method * to hull delegate option * to hull method SetupBoard {} { # Destroy any previous definition $self delete all array unset data set cells $options(-cells) set pixels $options(-pixels) set size [expr {$cells * $pixels}] # FIRST, set the size of the canvas $self configure -width $size -height $size # NEXT, draw the grid lines. for {set i 1} {$i < $cells} {incr i 1} { set pos [expr {$i * $pixels}] # Draw a vertical line $i cells over $self create line 0 $pos $size $pos -fill cyan # Draw a horizontal line $i cells down $self create line $pos 0 $pos $size -fill cyan } # NEXT, compute the list of indices set indices {} for {set i 0} {$i < $cells} {incr i 1} { for {set j 0} {$j < $cells} {incr j 1} { lappend indices $i,$j } } # NEXT, add a circle object to each cell for {set i 0} {$i < $cells} {incr i 1} { for {set j 0} {$j < $cells} {incr j 1} { # Compute the upper left corner of the circle set p0 [expr {$i*$pixels + 1}] set q0 [expr {$j*$pixels + 1}] # Compute the lower left corner of the circle set p1 [expr {$p0 + $pixels - 2}] set q1 [expr {$q0 + $pixels - 2}] # Create the circle, tagging it $i,$j $self create oval $p0 $q0 $p1 $q1 \ -tag $i,$j -fill white -outline white # When the user clicks on it, it should toggle. $self bind $i,$j <Button> [list $self toggle $i,$j] # Initialize the corresponding data structure set data($i,$j) 0 # Cache the coordinates of each neighbor. set data($i,$j-neighbors) "" foreach {iof jof} {-1 -1 -1 0 -1 1 0 -1 0 1 1 -1 1 0 1 1} { set r [expr {($i + $iof) % $cells}] set c [expr {($j + $jof) % $cells}] lappend data($i,$j-neighbors) "$r,$c" } } } } # toggle ij # # Toggles cell i,j on the board. method toggle {ij} { # FIRST, toggle the cell in the array. if {$data($ij) == 0} { $self setcell $ij } else { $self clearcell $ij } } # setcell ij # # Sets cell ij to alive method setcell {ij} { $self itemconfigure $ij -fill forestgreen set data($ij) 1 } # clearcell ij # # Clears (kills) cell i,j method clearcell {ij} { $self itemconfigure $ij -fill white set data($ij) 0 } # clear # # Sets the board cells to all dead method clear {} { foreach ij $indices { if {$data($ij)} { $self clearcell $ij } } } # generate # # The generate function takes the cells on the board through one # generation. method generate {} { # Count the neighbors of each cell. During start up we cached the # coordinates of the neighbors of each cell, so now we can just # iterate over them quickly. foreach ij $indices { set nCount 0 foreach neighbor $data($ij-neighbors) { incr nCount $data($neighbor) } set count($ij) $nCount } # Set the new contents of each cell based on the count. set changes 0 foreach ij $indices { if {$count($ij) < 2 || $count($ij) > 3} { if {$data($ij)} { # Cell is dead $self clearcell $ij incr changes } } elseif {$count($ij) == 3} { # Cell is born, if there wasn't one if {!$data($ij)} { $self setcell $ij incr changes } } } return $changes } # run # # Generate indefinitely. method run {} { if {[$self generate] > 0} { after $options(-delay) [list $self run] } } # stop # # Stop running method stop {} { after cancel [list $self run] } } #----------------------------------------------------------------------- # The Main GUI # # This GUI really just adds a toolbar to the basic board; in a sense, # it's inheriting and extending the board. snit::widget lifegame { constructor {args} { component hull is [frame $self] frame $self.boardframe -borderwidth 5 -relief ridge component board is [board $self.boardframe.board] pack $self.boardframe.board pack $self.boardframe -side bottom -padx 2 -pady 2 bind . <Return> [list $self generate] frame $self.bar $self addbtn "Generate" generate $self addbtn "Run" run $self addbtn "Stop" stop $self addbtn "Clear" clear button $self.bar.exit -text "Exit" -command exit pack $self.bar.exit -side right -padx 2 -pady 2 pack $self.bar -side top -fill x # Configure the options $self configurelist $args } # Delegate -cells and -pixels to the board, so that clients can # specify the board size; delegate other options to the hull. delegate option -cells to board delegate option -pixels to board delegate option * to hull # Frames don't have any methods but configure and cget, which are # already taken care of; so delegate everything else straight to the # board. delegate method * to board # Private method: add a button to the toolbar method addbtn {text method} { button $self.bar.$method -text $text -command [list $self $method] pack $self.bar.$method -side left -padx 2 -pady 2 } } #----------------------------------------------------------------------- # The Main program lifegame .lifegame -cells 40 -pixels 15 pack .lifegame -side bottom -fill both -padx 2 -pady 2