Updated 2003-11-30 22:40:23

Peter Lewerin this is a toy, not a game. It is a rudimentary application of "Artificial Life", in which the bugs by a process of selection gradually acquire movement patterns that make them more efficient in finding bacteria to eat. If they can't find food, they die. If they find a lot of food, they will eventually split into two bugs with slightly different (hopefully improved) movement patterns.


 #! /bin/sh
 # \
  exec tclsh "$0" "$@"
 package require Tk

 # TkBugs --
 #
 #       An implementation of the BUGS program from
 #       the book "The Magic Machine", by A K Dewdney
 #       (ISBN 0-7167-2144-9).  Substantial changes
 #       have been made to the code to adapt it to
 #       the Tk environment and an event-driven
 #       programming model.
 #
 # Copyright
 #       (c) 2001 Peter Lewerin. All rights reserved.
 #       This program is free software; you may
 #       redistribute it and/or modify it as you wish.
 #
 # RCS: $Revision: 1.5 $ $Date: 2003-12-01 07:00:03 $
 ### configuration variables ########################
 #
 # canvas width, height, background color
 set width		   150
 set height		   100
 set canvasBgColor	  grey

 # colors for drawing bugs and bacteria
 set bugColor		 white
 set bugInspColor	yellow
 set bactColor		purple

 # initial number of bugs/bacteria
 set initialBugs		    10
 set initialBacteria	   200

 set delayBugAction	   100
 set delayReplBact	   500

 # parameters for bug behaviour
 set energyToMove	    10
 set energyToSplit	  1000
 set ageToSplit		   800
 set energyMax		  1500
 set energyPerBact	    40
 #
 ### end of configuration variables #################

 # repeat --
 #
 # Arguments:
 #	n	an integer value
 #	script	a script
 #
 # Results:
 #	The script is executed n times.  The procedure
 #	returns an empty string.
 #

 proc repeat {n script} {
     for {set i 0} {$i < $n} {incr i} {
         uplevel $script
     } ;# end for (i)

     return
 }

 wm title . TkBugs

 set timeElapsed 0

 set xmove [list   0  2  2  0 -2 -2]
 set ymove [list  -2 -1  1  2  1 -1]

 array set bugs [list]

 # init --
 #	Sets up the program to run the bug simulation.
 #
 # Results:
 #	Creates the canvas if it didn't exist before;
 #	removes all canvas items and all timers if they
 #	exist; creates initial bug and bacterium items;
 #	sets up timers.
 #	Returns an empty string.
 #

 proc init {} {
     global width height canvasBgColor
     global initialBugs delayBugAction
     global initialBacteria delayReplBact

     # lifted from a postcard from Richard Suchenwirth
     if {![winfo exists .c]} {
         canvas .c -width $width -height $height \
             -background $canvasBgColor \
             -highlightcolor $canvasBgColor
         pack .c
         bind .c <1>   [list source [info script]]
         bind .c <3>   [list tk_popup .m %X %Y]
     }
     .c delete all
     destroy .m
     foreach {i} [after info] {after cancel $i}

     menu .m -tearoff no

     .m add checkbutton -label {Show info} \
         -command toggleInfo
     .m add checkbutton -label {Debugging mode} \
         -command toggleDebugMode
     .m add checkbutton -label {Inspect} \
         -command toggleInspectMode
     .m add separator
     .m add cascade     -label {About...} \
         -state disabled

     generateInitialBugs $initialBugs
     generateInitialBacteria $initialBacteria
     after $delayBugAction bugAction
     after $delayReplBact replenishBacteria
     focus .c
     return
 }

 # toggleInfo --
 #	Show or hide information about the program.
 #
 # Results:
 #	Creates an information frame if it didn't
 #	already exist.
 #	If the frame is visible, this procedure
 #	hides it; if it is hidden, this procedure
 #	shows it.
 #	Returns an empty string.
 #

 proc toggleInfo {} {
     global numBugs numCycles
     if {![winfo exists .info]} {
         frame .info
         label .info.numbugs -textvariable numBugs
         label .info.time -textvariable numCycles
         pack .info.numbugs .info.time -side left
     }

     if {[catch {llength [pack info .info]}]} {
         pack .info
     } else {
         pack forget .info
     } ;# end if
     return
 }

 # toggleDebugMode --
 #	Toggles debugging mode.
 #
 # Results:
 #	Returns an empty string.
 #

 proc toggleDebugMode {} {
     if {[.c cget -cursor] == "crosshair"} {
         .c configure -cursor arrow
         bind .c <1> [list source [info script]]
     } else {
         .c configure -cursor crosshair
         bind .c <1> [list killBug %x %y]
     } ;# end if

     return
 }

 # toggleInspectMode --
 #	Toggles inspection mode.
 #
 # Results:
 #	Creates an inspection frame if it didn't
 #	already exist.
 #	If the frame is visible, this procedure
 #	1) hides it, 2) unbinds the Tab key, and
 #	3) calls nullInspId; if it is hidden,
 #	this procedure 1) shows it, 2) binds the
 #	Tab key to the procedure bumpInspId, and
 #	3) calls bumpInspId.
 #	Returns an empty string.
 #

 proc toggleInspectMode {} {
     global inspLabel inspAttrs

     if {![winfo exists .inspect]} {
         frame .inspect
         label .inspect.id -font {Helvetica 9 bold} \
             -textvariable inspLabel
         label .inspect.attrs -textvariable inspAttrs
         pack .inspect.id .inspect.attrs
     }

     if {![catch {llength [pack info .inspect]}]} {
         pack forget .inspect
         bind .c <Tab> {}
         nullInspId
     } else {
         pack .inspect
         bind .c <Tab> bumpInspId
         bumpInspId
     } ;# end if

     return
 }

 # bugAction --
 #	This procedure is called by a timer
 #	to update the state of the bug items.
 #
 # Results:
 #	Each bug
 #	1) eats bacteria
 #	2) moves if it has enough energy left
 #	3) loses a unit of energy and ages one cycle
 #	4) splits into two new bugs if it has enough
 #	   energy and is old enough
 #	5) dies, if it has run out of energy.
 #	The timer is then reset to $delayBugAction.
 #	Returns an empty string.
 #

 proc bugAction {} {
     global bugs
     global numBugs numCycles timeElapsed
     global delayBugAction
     global inspLabel inspAttrs
     global energyToMove energyToSplit ageToSplit

     set bugids [.c find withtag {bug||inspected}]
     set numBugs [format "Bugs: %d" [llength $bugids]]
     set numCycles [format "Cycles: %d" \
         [incr timeElapsed]]
     if {[string length [set inspId [.c find withtag inspected]]]} {
         set inspLabel "Bug # $inspId"
         set inspAttrs "Energy: $bugs($inspId,energy)"
         append inspAttrs "  Age: $bugs($inspId,age)"
         append inspAttrs "  Dir: $bugs($inspId,dir)\n"
         append inspAttrs "  Lt: $bugs($inspId,lt)"
         append inspAttrs "  Fw: $bugs($inspId,fw)"
         append inspAttrs "  Rt: $bugs($inspId,rt)\n"
         append inspAttrs "  Hl: $bugs($inspId,hl)"
         append inspAttrs "  Rv: $bugs($inspId,rv)"
         append inspAttrs "  Hr: $bugs($inspId,hr)"
     }
     foreach {id} $bugids {
         feedBug $id
         if {$bugs($id,energy) >= $energyToMove} {
             moveBug $id
         }
         incr bugs($id,energy) -1
         incr bugs($id,age)
         if {
             $bugs($id,energy) >= $energyToSplit &&
             $bugs($id,age)    >= $ageToSplit
         } {
             set id' [cloneBug $id]
             mutateBug $id
             mutateBug ${id'}
         }
         if {$bugs($id,energy) <= 0} {
             destroyBug $id
         } ;# end if
     } ;# end foreach
     after $delayBugAction bugAction
     return
 }

 # feedBug --
 #	Simulates a bug eating one or more bacteria,
 #	gaining energy.
 #
 # Arguments:
 #	id	the canvas item number for the bug.
 #
 # Results:
 #	Increases bug's energy by 40 for each bacterium
 #	eaten (unless the bug already has 1500 or more
 #	units of energy).
 #	The canvas items representing the bacteria are
 #	removed.
 #	Returns total amount of energy gained.
 #

 proc feedBug {id} {
     global bugs energyMax energyPerBact
     set total 0
     foreach {i} [eval .c find overlap [.c bbox $id]] {
         if {[lsearch [.c gettags $i] bacterium] >= 0} {
             if {$bugs($id,energy) < $energyMax} {
                 incr total $energyPerBact
             }
             .c delete $i
         } ;# end if
     }
     incr bugs($id,energy) $total
 }

 # moveBug --
 #	Simulates a bug changing direction and moving.
 #
 # Arguments:
 #	id	the canvas item number for the bug.
 #
 # Results:
 #	See comments inside procedure.  Returns an
 #	empty string.
 #

 proc moveBug {id} {
     global bugs xmove ymove
     set sum 0
     # choose which direction change to make by adding
     # the `genetical' bias for the different turns
     # and moves...
     foreach {g} [list fw rt hr rv hl lt] {
         set sum [expr {$sum + $bugs($id,$g)}]
     }
     # ...then take a random number in the range
     # [0..$sum)...
     set r [expr {rand()*$sum}]
     # ...find the corresponding direction change and
     # apply it.
     foreach {g} [list fw rt hr rv hl lt] \
         {i} {0 1 2 3 4 5} {
         set r [expr {$r - $bugs($id,$g)}]
         if {$r <= 0} {
             set bugs($id,dir) \
                 [expr {($bugs($id,dir)+$i)%6}]
             break
         } ;# end if
     }

     # make the direction change, bouncing from walls...
     set width [.c cget -width]
     set height [.c cget -height]
     foreach {x y} [.c coords $id] break
     set dx [lindex $xmove $bugs($id,dir)]
     if {$x+$dx < 0 || $x+$dx+2 > $width} {
         set dx [expr {$dx*-1}]
     }
     set dy [lindex $ymove $bugs($id,dir)]
     if {$y+$dy < 0 || $y+$dy+2 > $height} {
         set dy [expr {$dy*-1}]
     }
     # ...do it!
     .c move $id $dx $dy
     return
 }

 # cloneBug --
 #	Simulate a bug splitting into two identical
 #	bugs.
 #
 # Arguments:
 #	id	the canvas item number for the bug.
 #
 # Results:
 #	Creates a new bug canvas item, and copies all
 #	attributes from the old bug to the new (except
 #	for energy, which is halved, and age, which is
 #	set to 0).
 #	Returns the canvas item number of the new bug.
 #

 proc cloneBug {id} {
     global bugs
     set bugs($id,energy) [expr {$bugs($id,energy)/2}]
     set bugs($id,age) 0
     set id2 [eval drawBug [lrange [.c coords $id] 0 1]]
     foreach {name} [array names bugs $id,*] {
         regsub {^(.*?),(.*)} $name $id2,\\2 name2
         set bugs($name2) $bugs($name)
     } ;# end foreach
     return $id2
 }

 # mutateBug --
 #	Change one of the six `genes' that control
 #	bug movement.
 #
 # Arguments:
 #	id	the canvas item number for the bug.
 #
 # Results:
 #	One randomly chosen `gene' is doubled or
 #	halved.
 #	Returns an empty string.
 #

 proc mutateBug {id} {
     global bugs
     set g [lindex [list fw rt hr rv hl lt] \
         [expr {int(rand()*6)}]]
     if {int(rand()*2) > 0} {
         set bugs($id,$g) [expr {$bugs($id,$g)*2}]
     } else {
         set bugs($id,$g) [expr {$bugs($id,$g)/2.0}]
     } ;# end if
     return
 }

 # drawBug --
 #	Draws the image of a bug on the canvas.
 #
 # Arguments:
 #	x, y	The coordinates for the upper left
 #		corner of the bug canvas item.
 #
 # Results:
 #	Returns the canvas item number of the bug.
 #

 proc drawBug {x y} {
     global bugColor
     .c create rectangle $x $y [expr {$x+2}] \
         [expr {$y+2}] -outline $bugColor \
         -fill $bugColor -tags bug
 }

 # getRandCoords --
 #	Generates a coordinate within a given
 #	width and height.
 #
 # Arguments:
 #	width	the x coordinate will be generated
 #	within [0..$width).  If $width is 0, the
 #	canvas width will be used instead.
 #	height	the y coordinate will be generated
 #	within [0..$height).  If $height is 0, the
 #	canvas height will be used instead.
 #	xofs	is added to the x coordinate
 #	yofs	is added to the y coordinate
 #
 # Results:
 #	Returns a list consisting of the x and y
 #	coordinates.
 #

 proc getRandCoords {{width 0} {height 0} {xofs 0} \
     {yofs 0}} {
     if {$width<1} { set width [.c cget -width] }
     if {$height<1} { set height [.c cget -height] }

     set x [expr {int(rand()*$width+$xofs)}]
     set y [expr {int(rand()*$height+$yofs)}]
     return [list $x $y]
 }

 # createBug --
 #	Sets the initial attributes of a bug, and
 #	creates the canvas item for it.
 #
 # Results:
 #	Returns the canvas item number of the bug.
 #

 proc createBug {} {
     global bugs energyPerBact
     set width [.c cget -width]
     set height [.c cget -height]
     set id [eval drawBug [getRandCoords \
         [incr width -2] [incr height -2] 1 1]]
     set bugs($id,energy) $energyPerBact
     set bugs($id,age) 0
     set bugs($id,fw) [expr {pow(2,int(rand()*10-5))}]
     set bugs($id,rt) [expr {pow(2,int(rand()*10-5))}]
     set bugs($id,hr) [expr {pow(2,int(rand()*10-5))}]
     set bugs($id,rv) [expr {pow(2,int(rand()*10-5))}]
     set bugs($id,hl) [expr {pow(2,int(rand()*10-5))}]
     set bugs($id,lt) [expr {pow(2,int(rand()*10-5))}]
     set bugs($id,dir) [expr {int(rand()*6)}]

     return $id
 }

 # generateInitialBugs --
 #	Creates a number of bugs.
 #
 # Arguments:
 #	n	number of bugs to create.
 #
 # Results:
 #	Bugs!  Returns an empty string.
 #

 proc generateInitialBugs {n} {
     repeat $n createBug
     return
 }

 # drawBacterium --
 #	Draws the image of a bacterium on the canvas.
 #
 # Arguments:
 #	x, y	The coordinates for the upper left
 #		corner of the bacterium canvas item.
 #
 # Results:
 #	Returns the canvas item number of the bacterium.
 #

 proc drawBacterium {x y} {
     global bactColor
     .c create rectangle $x $y $x $y \
         -outline $bactColor -tags bacterium
 }

 # generateInitialBacteria --
 #	Creates a number of bacteria.
 #
 # Arguments:
 #	n	number of bacteria to create.
 #
 # Results:
 #	Bacteria!  Returns an empty string.
 #

 proc generateInitialBacteria {n} {
     repeat $n {
         eval drawBacterium [getRandCoords]
     }
     return
 }

 # replenishBacteria --
 #	This procedure is called by a timer
 #	to add one more bacterium to the canvas.
 #
 # Results:
 #	One more bacterium; resets the timer.
 #	Returns an empty string.
 #

 proc replenishBacteria {} {
     global delayReplBact
     eval drawBacterium [getRandCoords]
     after $delayReplBact replenishBacteria
     return
 }

 # killBug --
 #	Handles an attempt to kill a bug with
 #	a mouseclick.
 #
 # Arguments:
 #	x, y	the canvas coordinates for the
 #		killing mousepress.
 #
 # Results:
 #	If there is a canvas item such that it
 #	1) overlaps the coordinates of the
 #	mouseclick, and 2) is a bug, then it is
 #	destroyed.  In any case, a dark grey
 #	spot marks the place where the assault
 #	occurred.
 #	Returns an empty string.
 #

 proc killBug {x y} {
     set items [.c find overlap $x $y $x $y]
     set id {}
     foreach {item} $items {
         set tags [.c gettags $item]
         if {[lsearch $tags bug] >= 0} {
             set id $item
             break
         } ;# end if
     } ;# end foreach

     if {[llength $id] > 0} {
         destroyBug $id
     }
     .c create rectangle $x $y $x $y \
         -outline {dark grey}
     return
 } ;# end killBug

 # bumpInspId --
 #	Set the identity of the inspected
 #	bug to the next bug on the canvas.
 #
 # Results:
 #	Sets the color of the inspected bug
 #	to $bugInspColor.
 #	Returns the id of the inspected bug.
 #

 proc bumpInspId {} {
     global bugColor bugInspColor

     # get a list of bugs
     set bugids [.c find withtag {bug||inspected}]

     # is any existing bug inspected?
     set inspId [.c find withtag inspected]
     if {![string length $inspId]} {
         # nope, get one
         set inspId [lindex $bugids 0]
     } else {
         # yep, revert it to uninspected...
         .c itemconfigure $inspId \
             -outline $bugColor \
             -fill $bugColor
         .c dtag inspected

         # ...and set inspId to the next id in
         # the bug list.
         set idx [lsearch $bugids $inspId]
         set inspId [lindex $bugids [incr idx]]

         # did that work?
         if {![llength $inspId]} {
             # no, get another
             set inspId [lindex $bugids 0]
         } ;# end if
     } ;# end if

     # now, give the bug the inspected tag
     # and color it appropriately
     .c itemconfigure $inspId \
         -tags {inspected bug} \
         -outline $bugInspColor \
         -fill $bugInspColor

     return $inspId
 } ;# end bumpInspId

 # nullInspId --
 #
 # Results:
 #

 proc nullInspId {} {
     global bugColor

     set inspId [.c find withtag inspected]
     if {[string length $inspId]} {
         .c itemconfigure $inspId \
             -outline $bugColor \
             -fill $bugColor
         .c dtag inspected
     }

     return
 } ;# end nullInspId

 # destroyBug --
 #	Handles the death of a bug.
 #
 # Arguments:
 #	id	the canvas item number for the bug.
 #
 # Results:
 #	If the dying bug was under inspection,
 #	switch the inspection to the next bug.
 #	Then remove the attributes of the bug,
 #	and delete the canvas item.
 #	Returns an empty string.
 #

 proc destroyBug {id} {
     if {$id == [.c find withtag inspected]} {
         bumpInspId
     } ;# end if
     array unset bugs $id,*
     .c delete $id

     return
 } ;# end destroyBug

 init

Please don't put this in the Games category: you can't actually play, win or lose this application.

Category Application | Category Toys