Version 0 of Life in Snit

Updated 2002-10-11 20:56:01

Here's an version of Conway's Game of Life I wrote a year or so ago; I'm posting it now as an example of application development using Snit's Not Incr Tcl. The "board" on which the automata live is coded using Snit; the top-level GUI isn't (and probably should be...hmmm. Maybe I'll repost this later.). -- 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.  Other buttons will place various well-known and
 #        interesting patterns on the board, or clear it completely.
 #
 #        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

     # 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 {argv} {
         # FIRST, create the canvas.  Then configure the options.
         component hull is [canvas $self -background white]
         $self configurelist $argv

         # 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 {
             $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
         foreach ij $indices {
             if {$count($ij) < 2 || $count($ij) > 3} {
                 # Cell is dead
                 $self clearcell $ij
             } elseif {$count($ij) == 3} {
                 # Cell is born, if there wasn't one
                 $self setcell $ij
             }
         }
     }

     # run
     # 
     # Generate indefinitely.
     method run {} {
         $self generate
         after 10 [list $self run]
     }

     # stop
     # 
     # Stop running
     method stop {} {
         after cancel [list $self run]
     }
 }

 frame .boardFrame -borderwidth 5 -relief ridge
 set b [board .boardFrame.board -cells 20 -pixels 20]
 pack $b

 bind . <Return> [list $b generate]

 frame .buttons

 button .buttons.generate -text "Generate" -command [list $b generate]
 pack .buttons.generate -side left -padx 2

 button .buttons.run -text "Run" -command [list $b run]
 pack .buttons.run -side left -padx 2

 button .buttons.stop -text "Stop" -command [list $b stop]
 pack .buttons.stop -side left -padx 2

 button .buttons.clear -text "Clear" -command [list $b clear]
 pack .buttons.clear -side left -padx 2

 button .buttons.exit -text "Exit" -command exit
 pack .buttons.exit -side right -padx 2

 pack .buttons -side top -fill x
 pack .boardFrame -side bottom -fill both -padx 2 -pady 2