The game of 'bridg-it'
computer AI from Martin Gardener ref. Mathematical Puzzles and Diversions models board as electrical circuit. Max voltage across resistance is computer's best move.
Ver 0.2 - minor improvement to handling window close - could leave the . window running in 0.1
Ver 0.3 - better alignment of user & computer move with the board and added option to allow random first move (get out of that!).
04.11.06 error in cget with 1 argument corrected.
proc help {{op stdout}} {
append all " Your aim is to complete a continuous line from top to bottom of the board -\n"
append all " joining the blue dots.\n\n"
append all " While the opponent aims to complete a continuous line from side to side,\n"
append all " joining the red dots.\n\n"
append all " Click to place a link between 2 blue dots.\n\n"
append all " Choose a smaller or larger board with the spinbox number and 'Restart'.\n\n"
append all " Once either of the players has succeeded, the other player cannot finish\n"
append all " The game can only end in victory for one or other player - draw is impossible.\n\n"
append all " Ref: Martin Gardner, Mathematical Puzzles and Diversions.\n"
tk_messageBox -message $all
puts $op $all
}
# the artificial intelligence solves a linear set of equations, so load:.
package require math::linearalgebra
#
# cells
# 1 n+1 2n+2
# 2 n+2....
# 3 n+3...
# 4...
#
#
# resistances - n2 = n*(n-1) number of vertical resistances
# 0 n+1 2n+2
# n2 n2+n
# 1 n+1....
# n2+1 n2+n+1
# 2 n+2...
# n2+2 n2+n+2
# 3...
#
# each cell forms a closed loop of resistances and has a current flowing round it
# the actual voltage across a reistance is R.(adjacent current-this current)..
proc calccurr {brd nx} { ;# make the computer's move
set nloops [expr {$nx*($nx-1)+1}]
#puts "Computer moves for $nx size board - $nloops "
# this has nx*(nx-1)+1 loops to be solved for.
# we create a matrix and solve it
set mt [math::linearalgebra::mkMatrix $nloops $nloops 0.0]
set vc [math::linearalgebra::mkVector $nloops 0.]
math::linearalgebra::setelem vc 0 1 ;# the driving voltage
set i 0
set rloop 0
while {$i<$nx} { ;# consider first loop. rshared to 'lower' side only
set iup [expr {$i*$nx}] ;# the shared resistor
set iadj [expr {$i*($nx-1)+1}] ;# adjacent current loop
set rloc [$brd getres $iup]
set rloop [expr {$rloop+$rloc}]
math::linearalgebra::setelem mt 0 $iadj -$rloc
math::linearalgebra::setelem mt $iadj 0 -$rloc
math::linearalgebra::setelem mt $iadj $iadj [expr {[math::linearalgebra::getelem $mt $iadj $iadj]+$rloc}]
incr i
}
math::linearalgebra::setelem mt 0 0 $rloop
set j 0
set iloop 0
while {$j<$nx-1} { ;# consider each loop row. Up sides already handled
set i 0
while {$i<$nx} { ;# consider each loop in row. Up side already handled
set iup [expr {$i*$nx+$j+1}] ;# the shared resistor
set ithis [expr {$i*($nx-1)+$j+1}] ;# the local current
set iadj [expr {$i*($nx-1)+$j+2}] ;# adjacent current loop
set rloc [$brd getres $iup]
math::linearalgebra::setelem mt $ithis $ithis [expr {[math::linearalgebra::getelem $mt $ithis $ithis]+$rloc}]
if {$j<$nx-2} { ;# there is nothing beneath nx-2.
math::linearalgebra::setelem mt $iadj $iadj [expr {[math::linearalgebra::getelem $mt $iadj $iadj]+$rloc}]
math::linearalgebra::setelem mt $ithis $iadj -$rloc
math::linearalgebra::setelem mt $iadj $ithis -$rloc
}
if {$i<$nx-1} {
set iup [expr {$nx*$nx+$i*($nx-1)+$j }] ;# the shared resistor
set iadj [expr {$ithis+($nx-1)}] ;# adjacent current loop
set rloc [$brd getres $iup]
# then cover right except at end of row
math::linearalgebra::setelem mt $ithis $ithis [expr {[math::linearalgebra::getelem $mt $ithis $ithis]+$rloc}]
math::linearalgebra::setelem mt $iadj $iadj [expr {[math::linearalgebra::getelem $mt $iadj $iadj]+$rloc}]
math::linearalgebra::setelem mt $ithis $iadj -$rloc
math::linearalgebra::setelem mt $iadj $ithis -$rloc
}
incr i
incr iloop
}
incr j
}
set currs [ math::linearalgebra::solveGauss $mt $vc ]
#now find voltage across each resistor - the biggest is the BEST computer move.
set imax -1
set vmax 0
for {set i 0} {$i<$nx*$nx+($nx-1)*($nx-1)} { incr i} {
if {[$brd getres $i]==1} { ;# not yet cut or shorted.
# find shared currents. Difference is the voltage
if {$i<$nx*$nx} { ;# a horizontal resistor
set ix [expr {$i/$nx}]
set jx [expr {$i%$nx}]
set iup [expr {$jx==0?0:($i-$ix)}]
set idn [expr {$jx>$nx-2?-1:$i-$ix+1}]
set v [expr {$idn>0?[lindex $currs $iup]-[lindex $currs $idn]:[lindex $currs $iup]}]
} else { ;# vert resist.
set iii [expr {$i-$nx*$nx}] ;# which ij
set ix [expr {$iii/($nx-1)}]
set jx [expr {$iii%($nx-1)}]
set iup [expr {$ix*($nx-1)+$jx+1}]
set idn [expr {$iup+$nx-1}]
set v [expr {[lindex $currs $iup]-[lindex $currs $idn]}]
}
if {abs($v)>abs($vmax)} {
set vmax $v
set imax $i
}
}
}
return [list $imax [lindex $currs 0]]
}
proc bridgit {w args} { ;# a game of bridgit, toplevel for the board
# and allows new smooth shaped buttons.
global $w.props ;# an array of options specific to the bridgit game 'class'
global $w.res ;# an array of resistors for the computer move
# set by .this -<option> <value>
array set $w.props {-size 7 -spacing 50} ; # define the option list and each default value
array set options {}
set bridArgs {} ;# list of arguments not specific to the class
foreach {opt val} $args {
if {[array names $w.props $opt]!=""} {set options($opt) $val
} else { lappend bridArgs $opt $val }
}
# make the base canvas.
eval toplevel $w $bridArgs ;# create the "procedure" w
bind $w <Destroy> "$w destroy %W"
interp hide {} $w
# Install the alias:
interp alias {} $w {} bridgitCmd $w ;# bridgitsCmd are sub-commands for this class
foreach opt [array names options] {
$w configure $opt $options($opt)
}
$w makeboard
wm withdraw .
return $w ;# the original object
}
proc bridgitCmd {self cmd args} {
switch -- $cmd {
configure {eval bridgitConfigure $self $cmd $args}
cget {eval bridgitCget $self $args}
{about} {return [help] ;# return unit value
}
{size} {return [$self cget -size] ;# return unit value
}
{destroy} { ;# all destroy events come here - check that we are destroying the lowest level window.
if {$self==[lindex $args 0]} {
#tk_messageBox -message "$self is being destroyed $args<<\n[info level ]"
exit
}
}
{getid} { set x [lindex $args 0]
set y [lindex $args 1]
set nx [lindex $args 2] ;# which move has been made
# returns 0,nx-1 for first column of player moves; nx-2nx-1 for second
#>nx*nx for a horizontal move
# -1 for an invalid move.
set spacing [$self cget -spacing]
set ix [expr {($x-$spacing/2-2)/$spacing}]
set iy [expr { ($y-2)/$spacing }]
set nc [expr {$nx*$nx}] ;# numbero fo vertical moves available to the player.
# identify side (top, bottom, L, R)
set ixrel [expr {($x-$spacing*$ix-$spacing/2)}]
set iyrel [expr {($y-($spacing*$iy))}]
if {$ix>=-1 && $ix<$nx} {
if {$iy>=0 && $iy<$nx} {
if {abs($ixrel-$spacing/2)>abs($iyrel-$spacing/2)} {
if {$ixrel<$spacing/2} { set sid "L" } else { set sid "R" }
} else {
if {$iyrel<$spacing/2} { set sid "T" } else { set sid "B" }
}
switch $sid {
"L" {
if {$ix>$nx} {return -1} ;# invalid cell
if {$ix<0} {return -2} ;# invalid cell
return [expr {$ix*$nx+$iy}]
}
"R" {
if {$ix<-1} {return -1} ;# invalid cell
if {$ix>$nx-1} {return -2} ;# invalid cell
return [expr {($ix+1)*$nx+$iy}]
}
"T" {
if {$ix>=($nx-1)} {return -1} ;# invalid cell
if { $ix<0 } {return -2} ;# invalid cell
if {$iy<1} {return -3} ;# invalid cell
return [expr {$nx*$nx+$ix*($nx-1)+$iy-1}]
}
"B" {
if {$ix>=($nx-1)} {return -1} ;# invalid cell
if { $ix<0} {return -2} ;# invalid cell
if {$iy>($nx-2)} {return -2} ;# invalid cell
return [expr {$nx*$nx+$ix*($nx-1)+$iy}]
}
}
}
}
return -1
}
{getcut} { return [eval $self getid $args] }
{makemove} {
set ishort [lindex $args 0] ;# the resistor being cut.
set nx [$self cget -size]
set spac [$self cget -spacing]
if {$ishort<$nx*$nx} { ;# H- move
set ix [expr {int($ishort/$nx)}]
set iy [expr {int($ishort%$nx)}]
$self.froth.board create line [expr {$ix*$spac+5}] [expr {($iy+.5)*$spac+5}] \
[expr {($ix+1)*$spac+5}] [expr {($iy+.5)*$spac+5}] -fill red -width 3
} else {
set nrel [expr {$ishort-$nx*$nx}] ;# id relative to start of horizontal res.
set ix [expr {int($nrel/($nx-1))}]
set iy [expr {int($nrel%($nx-1))}]
$self.froth.board create line [expr {($ix+1)*$spac+5}] [expr {($iy+.5)*$spac+5}] \
[expr {($ix+1)*$spac+5}] [expr {($iy+1.5)*$spac+5}] -fill red -width 3
}
}
{setres} {
global $self.props
set $self.props(res[lindex $args 0]) [lindex $args 1]
}
{getres} {
return [$self cget res[lindex $args 0] ]
}
{makeresists} {
set n [$self cget -size]
# the algorithm depends on shorting out or cutting a set of resistors.
# each resistor represents a possible computer's move.
# A player's move cuts the resistor; the computer move at same place shorts out the resistor.
for {set i 0} {$i<$n*$n+($n-1)*($n-1)} { incr i} {
$self setres $i 1
}
}
{makefirstm} {
set n [$self cget -size]
set spacing [$self cget -spacing]
$self makeboard
# here we make a random move - get out of that!
set nx [expr {int(rand()*($n-1)*$spacing+$spacing/2.)}]; set ny [expr {int(rand()*($n-1)*$spacing+$spacing/2.)}]
$self click $nx $ny
}
{makeboard} { ;# puts "$self makeboard -- $cmd $args";
set n [$self cget -size]
# insert symbols into a canvas.
set spacing [$self cget -spacing]
catch {destroy $self.froth}
set frm [frame $self.froth]
pack [canvas $frm.board -height [expr {$spacing*$n+10}] -width [expr {$spacing*$n+10}]] -padx 10 -pady 10
bind $frm.board <ButtonPress-1> "$self click %x %y"
bind $frm.board <Motion> "$self setcursor %x %y"
set i 0
while {$i<=$n} {
set x [expr {$spacing*$i+2.5}]
set j 0
while {$j<$n} {
set y [expr {$spacing*($j+.5)+2.5}]
$frm.board create oval $x $y [expr {$x+5}] [expr {$y+5}] -fill red
incr j
}
incr i
}
set i 0
while {$i<$n} {
set x [expr {$spacing*($i+0.5)+2.5}]
set j 0
while {$j<=$n} {
set y [expr {$spacing*$j+2.5}]
$frm.board create oval $x $y [expr {$x+5}] [expr {$y+5}] -fill blue
incr j
}
incr i
}
set ::ival "Your move."
pack [label $frm.mesg -textvariable ::ival] -padx 10 -pady 4
pack [frame $frm.butts] -padx 2 -pady 4
pack [ button $frm.rest -text "Restart" -command "$self makeboard "] -padx 10 -pady 1 -side left
pack [ button $frm.refm -text "Make My First move" -command "$self makefirstm "] -padx 10 -pady 1 -side left
pack [ spinbox $frm.setsiz -from 3 -to 18 -command "$self configure -size %s" -width 3] -padx 10 -pady 1 -side left
$frm.setsiz set [$self cget -size]
pack [ button $frm.aboutt -text "About..." -command "$self about"] -padx 10 -pady 4 -side left
pack [ button $frm.exit -text "Exit" -command "exit 1"] -padx 10 -pady 4 -side right
pack $frm
$self makeresists [$self cget -size]
return
}
{setwin} {
set ::ival "[lindex $args 0] Wins"
switch [lindex $args 0] {
{Boris} { $self.froth.board configure -background green }
default { $self.froth.board configure -background cyan }
}
}
{setcursor} {
set nx [$self cget -size]
set icut [$self getcut [lindex $args 0] [lindex $args 1] $nx]
if {$icut<$nx*$nx} { $self.froth.board configure -cursor sb_v_double_arrow
} else { $self.froth.board configure -cursor sb_h_double_arrow }
}
{click} {
set x [lindex $args 0]
set y [lindex $args 1]
set nx [$self cget -size] ;# xy are relative to the board (not window).
# identify nearest dot pair
# $self.froth.board the game frame x,y are relative to $self.froth.board.
set i1 [$self getid $x $y $nx]
set icut [$self getcut $x $y $nx ]
if {$icut<$nx*$nx} { set horv "v"} else { set horv "h"}
if {$icut>=0} {
if {[$self getres $icut]==1} {
$self playerscut $horv $icut $nx
$self setres $icut 1.e8 ;# my move - cuts out resistor
# computer move shorts the resistor
set ::ival "OK.."; update idletasks
$self computermove $nx ;# calculate response
} else { set ::ival "Already used" }
} else {
set ::ival "Invalid Move $icut"; update idletasks
}
}
{computermove} {
set nx [lindex $args 0] ;# make the computer's move
set res [ calccurr $self $nx]
set imax [lindex $res 0]
$self setres $imax 1.e-8
$self makemove $imax
set res [ calccurr $self $nx] ;# recalculate the current to check if computer has won.
set curmax [expr {abs([lindex $res 1])}]
if {$curmax<1.e-3} {
$self setwin "You"
} elseif {$curmax>1.e3} {
$self setwin "Boris"
} else {
set ::ival "Your move"
#puts "Best current is $curmax"
}
}
{playerscut} {
set horv [lindex $args 0]
set nres [lindex $args 1]; set nx [lindex $args 2]
set spac [$self cget -spacing]
set ix [expr {int($nres/$nx)}]
set iy [expr {int($nres%$nx)}]
if {$horv=="v"} {
set ix [expr {int($nres/$nx)}]
set iy [expr {int($nres%$nx)}]
$self.froth.board create line [expr {($ix+.5)*$spac+5}] [expr {$iy*$spac+5}] \
[expr {($ix+.5)*$spac+5}] [expr {($iy+1)*$spac+5}] -fill blue -width 3
} else { ;# a horizontal move
set nrel [expr {$nres-$nx*$nx}] ;# id relative to start of horizontal res.
set ix [expr {int($nrel/($nx-1))}]
set iy [expr {int($nrel%($nx-1)+1)}]
$self.froth.board create line [expr {($ix+0.5)*$spac+5}] [expr {$iy*$spac+5}] \
[expr {($ix+1.5)*$spac+5}] [expr {$iy*$spac+5}] -fill blue -width 3
}
}
{default} { ;# use $cmd to canvas widget
# puts "Action $cmd $args"
eval interp invokehidden {{}} $self $cmd $args
}
}
}
proc bridgitConfigure {self cmd args} {
# 3 scenarios:
#
# $args is empty -> return all options with their values
# $args is one element -> return current values
# $args is 2+ elements -> configure the options
global $self.props
switch [llength $args] {
0 { ;# return all options
set result [array names $self.props]
return $result
}
1 { ;# return argument values
if {[array names $self.props $opt]!=""} { lappend opts [$self cget $args]
} else { puts "No option $opt" }
return $opts
}
default { ;# >1 arg - an option and its value
# go through each option:
foreach {option value} [lrange $args 0 end] {
if {[array names $self.props $option]!=""} {
# set global array element for each option.
set $self.props($option) $value
} else {
$self configure $option $value
}
}
return {}
}
}
}
proc bridgitCget {self args} {
# cget defaults done by the interp cget command
upvar #0 $self.props props ;# get local address for global array
#puts "$self get $args [array names props]"
if {[array names props $args ]!=""} {
#puts "CGoth $self $args; $props($args)"
return $props($args)
}
return [uplevel 1 [list interp invokehidden {} $self cget $args]]
}
#console show
update idletasks
catch { destroy .game}
set bif [bridgit .game -size 9 -spacing 45]
gold added pix