Version 3 of Life in Snit

Updated 2002-10-22 01:12:41

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

Category Games Category Application