iConnect4

Michael Jacobson 2003-05-14 - While showing my 6 year old son the game TkAlign4 he said that it was great but needed a computer to play against (as dad was busy at the time). So with some help from a friend, Jason Tang, we came up with these enhancements to TkAlign4. I also tried to make sure that it would be playable on the PocketPc version of Tcl/Tk. Note that the AI's playing ability is selectable from the menu (Stupid, Dumb, Easy, Medium, Hard, Best) and the default is Easy (which does not seem to easy to me ;-).

If you want to just download a copy then get it here [L1 ] or a TclKit version here [L2 ]

Jason Tang: For the curious, I implemented the game AI as an alpha-beta pruning tree.

package require Tk

set info "iConnect4 (was TkAlign4) v1.1
    by Richard Suchenwirth
AI Game Architect 
    by Jason Tang
Computer Play updates
    by Michael Jacobson

Game Play
    Two players, red and yellow.
    Click on a column to insert
    piece. If you have four pieces
    in a row (horizontal, vertical,
    diagonal), you win.
 
Computer Opponent
    You may play against the computer
    or even have it play itself. You
    may halt the computer by changing
    it back to a human player with
    the spin box.
"
frame .f
set g(status) {6 6 6 6 6 6 6}
button .f.0 -text New   -command {reset .c}
button .f.1 -text Reset -command {reset .c all}
spinbox .f.2s  -textvar g(pred) -width 8 -values {Player1 Computer} -command {opponentchg .f.2s %s}
set g(pred) Player1
label  .f.2 -bg red    -width 2 -textvar g(red)
spinbox .f.3s -textvar g(pyellow) -width 8 -values {Player2 Computer} -command {opponentchg .f.3s %s}
set g(pyellow) Player2
label  .f.3 -bg yellow -width 2 -textvar g(yellow)
button .f.4 -text X -command {exit} ;# mainly for WinCE platform
eval pack [winfo children .f] -side left -fill y
canvas .c
eval pack [winfo children .]
wm geometry . 240x320+0+0

proc reset {c {what ""}} {
    global g
    $c delete all
    if {$what=="all"} {
        set g(red) 0
        set g(yellow) 0
        set g(toPlay) red
    } else {
            set g(toPlay) $g(toPlay) ;# to trip the trace
    }
    oval $c 107 2 133 28 -fill $g(toPlay) -tag chip
    $c create rect 0 30 240 240 -fill darkblue
    foreach x {0 1 2 3 4 5 6} {
        set x0 [expr $x*32+10]
        set x1 [expr $x0+26]
        foreach y {1 2 3 4 5 6} {
            set y0 [expr $y*32+16]
            set y1 [expr $y0+26]
            set id [oval $c $x0 $y0 $x1 $y1 -fill black -tag $x,$y]
            $c bind $id <1> [list insert $c $x]
        }
    }
    
}

proc insert {c x {block 1}} {
         if {$block} {
                 # do not let manual insert if in computer control mode
                 if {$::g(p$::g(toPlay)) == "Computer" } {return}                         
         }
    if {[$c find withtag chip]==""} return
    if {[colorof $c $x,1] != "black"} return
    $c delete chip
    global g
    set color $g(toPlay)
    $c itemconfig $x,1 -fill $color
    set y 1
    while 1 {
        update
        if {[colorof $c $x,[expr $y+1]] != "black"} break
        $c itemconfig $x,$y       -fill black
        $c itemconfig $x,[incr y] -fill $color
        after 100
    }
    set g(status) [lreplace $g(status) $x $x [expr $y-1]]
    if ![win $c $x $y] {
        set g(toPlay) [expr {$color=="red"? "yellow" : "red"}]
        oval $c 107 2 133 28 -fill $g(toPlay) -tag chip
    }
}

proc colorof {c tag} {
    $c itemcget $tag -fill
}

proc win {c x y} {
    global g
    set self [colorof $c $x,$y]
    foreach {dx dy} {1 0  0 1  1 1  1 -1} {
        set mdx [expr -$dx]; set mdy [expr -$dy]
        set row $x,$y
        set x0 $x; set y0 $y
        while 1 {
            if {[colorof $c [incr x0 $dx],[incr y0 $dy]]!=$self} break
            lappend row $x0,$y0
        }
        set x0 $x; set y0 $y
        while 1 {
            if {[colorof $c [incr x0 $mdx],[incr y0 $mdy]]!=$self} break
            lappend row $x0,$y0
        }
        if {[llength $row] >= 4} {
                #puts "We have a winner - Now flash the 4 in a row"
            foreach chip $row {$c addtag win withtag $chip}
            $c itemconfig win -fill green
            set last green
            for {set i 1} {$i < 6} {incr i} {
                set new [expr {$last=="green"? "$self" : "green"}]
                   after [expr {500 * $i}] \
                      $c itemconfig win -fill $new                       
                set last $new
                    }
#            set g(toPlay) [expr {$self=="red"? "yellow" : "red"}]
            tk_messageBox -message "$g(p$self) wins"
            incr ::g($self)
            return 1
        }
    }
    return 0
}

if {$tcl_platform(os)=="Windows CE"} {
    proc rp {x0 y0 x1 y1 {n 0} } {
        set xm [expr {($x0+$x1)/2.}]
        set ym [expr {($y0+$y1)/2.}]
        set rx [expr {$xm-$x0}]
        set ry [expr {$ym-$y0}]
        if {$n==0} {
           set n [expr {round(($rx+$ry))}]
        }
        set step [expr {atan(1)*8/$n}]
        set res ""
        set th [expr {atan(1)*6}]
        for {set i 0} {$i<$n} {incr i} {
                   lappend res \
                [expr {$xm+$rx*cos($th)}]
                   lappend res \
                [expr {$ym+$ry*sin($th)}]
                   set th [expr {$th+$step}]
        }
        set res
    }
    proc oval {w x0 y0 x1 y1 args} {
        eval $w create poly [rp $x0 $y0 $x1 $y1] $args
    }
} else {
    proc oval {w x0 y0 x1 y1 args} {
        eval $w create oval $x0 $y0 $x1 $y1 $args
    }
}
 

######################## AI Stuff below  ##############################
proc bestMove {color} {
        set ans [getMove $color]
        if {$::ABORT != 1} {
                insert .c $ans 0
        } else {
                set ::ABORT 0
        }
}

# sets the AI's difficulty level
# higher number == tougher AI (but also much slower)
# even numbers tends to favor a more aggressive AI
# odd numbers tends to favor a more defensive AI
if {$tcl_platform(os)=="Windows CE"} {
         set DIFFICULTY 1 ;# make it run faster on this platform
} else {
         set DIFFICULTY 3
}
set ABORT 0

proc getMove {color} {
    global DIFFICULTY
    set scores ""
    foreach col {0 1 2 3 4 5 6} {
        # first make a duplicate of the board
        dupBoard board

        # next simulate where drop would occur
        for {set row 6} {$row >= 1} {incr row -1} {
            if {$board($row,$col) == ""} {
                set board($row,$col) $color
                break
            }
        }
        if {$row <= 0} {
            # column is filled; skip to next one
            set result -10001
        } else {
            set result [getMoveAB board $row $col $color $color \
                            -100001 100001 $DIFFICULTY]
            #puts "col $col:  $result"
            if {$result == 10000} {
                return $col
            }
        }
        lappend scores $result
    }

    # now pick the best score
    set bestscore [lindex $scores 0]
    set bestcols 0

    foreach i {1 2 3 4 5 6} {
        set current [lindex $scores $i]
        if {$current > $bestscore} {
            set bestscore $current
            set bestcols $i
        } elseif {$current == $bestscore} {
            lappend bestcols $i
        }
    }
    return [lindex $bestcols [expr {int (rand () * [llength $bestcols])}]]
}

# performs a somewhat modified alpha-beta search on the board
proc getMoveAB {ob row col me current alpha beta depth} {
    update
    if {$::ABORT == 1} {return 10000}
    upvar $ob origBoard
    # this will check to see if search is at a terminal state
    set myscore [getScore origBoard $row $col $current]
    if {$depth <= 0 || $myscore == 10000} {
        if {$me != $current} {
            set myscore [expr {-1 * $myscore}]
        }
        return $myscore
    } elseif {$me != $current} {
        # examining a max node -- do alpha pruning
        incr depth -1
        set newCurrent [oppColor $current]
        foreach col {0 1 2 3 4 5 6} {
            array set board [array get origBoard]
            for {set row 6} {$row >= 1} {incr row -1} {
                if {$board($row,$col) == ""} {
                    set board($row,$col) $newCurrent
                    break
                }
            }
            if {$row <= 0} {
                continue
            }
            set score [getMoveAB board $row $col $me $newCurrent \
                           $alpha $beta $depth]
            if {$score > $alpha} {
                set alpha $score
            }
            if {$alpha >= $beta} {
                return $alpha
            }
        }
        return $alpha
    } else {
        # examining a min node -- do beta pruning
        incr depth -1
        set newCurrent [oppColor $current]
        foreach col {0 1 2 3 4 5 6} {
            array set board [array get origBoard]
            for {set row 6} {$row >= 1} {incr row -1} {
                if {$board($row,$col) == ""} {
                    set board($row,$col) $newCurrent
                    break
                }
            }
            if {$row <= 0} {
                continue
            }
            set score [getMoveAB board $row $col $me $newCurrent \
                           $alpha $beta $depth]
            if {$score < $beta} {
                set beta $score
            }
            if {$beta <= $alpha} {
                return $beta
            }
        }
        return $beta
    }
}

proc dupBoard {dest} {
    upvar $dest board
    foreach col {0 1 2 3 4 5 6} {
        set num 0
        foreach row {1 2 3 4 5 6} {
            set c [colorof .c $col,$row]
            if {$c == "black"} {
                set board($row,$col) ""
            } else {
                set board($row,$col) $c
            }
        }
    }    
} 

proc oppColor {color} {
    if {$color == "red"} {
        return yellow
    }
    return red
}

proc getScore {b row col who} {
    upvar $b board
    set sum 0
    foreach {dx dy ex ey} {-1 0 1 0 0 -1 0 1 1 -1 -1 1 -1 -1 1 1} {
        set leftbound 1
        set rightbound 1
        set score 1
        for {set c [expr {$col + $dx}]; set r [expr {$row + $dy}]; set i 0} \
            {$i < 3} \
            {incr c $dx; incr r $dy; incr i} {
            if {![info exists board($r,$c)]} {
                set leftbound 0
                break
            }
            if {$board($r,$c) == $who} {
                set score [expr {$score << 3}]
            } else {
                if {$board($r,$c) != ""} {
                    set leftbound 0
                }
                break
            }
        }
        for {set c [expr {$col + $ex}]; set r [expr {$row + $ey}]; set i 0} \
            {$i < 3} \
            {incr c $ex; incr r $ey; incr i} {
            if {![info exists board($r,$c)]} {
                set rightbound 0
                break
            }
            if {$board($r,$c) == $who} {
                set score [expr {$score << 3}]
            } else {
                if {$board($r,$c) != ""} {
                    set rightbound 0
                }
                break
            }
        }
        if {$score >= 256} {
            return 10000
        }
        if {$leftbound == 0 && $rightbound == 0} {
            set score 0
        } else {
            set score [expr {$score + $leftbound * 2 + $rightbound * 2}]
        }
        incr sum $score
    }
    return $sum
}

######################## AI GUI Stuff below  ##############################

proc opponentchg {c s} {
        if {$s == "Computer"} {
                if {".f.2s" == $c && $::g(toPlay) == "red"} { playerchange}
                if {".f.3s" == $c && $::g(toPlay) == "yellow"} { playerchange}
        }
}

trace add variable g(toPlay) write playerchange
trace add variable g(pred) write playerstatus
trace add variable g(pyellow) write playerstatus

proc playerstatus {array var type} {
        if {"$::g(p$::g(toPlay))" == $::g($var)} {
                if {$::OLD == "Computer" && [string range $::g($var) 0 end-1] == "Player"} {
                        set ::ABORT 1
                }
        }
}
set OLD ""
proc playerchange {args} {
        set ::OLD $::g(p$::g(toPlay))
        if { $::OLD == "Computer" } {
                return [after 100 [list bestMove $::g(toPlay)]]
        }
}


###  Code sized to look better on a PocketPC
wm geometry . 240x268+0+1

. config -menu [menu .m]
.m add casc -label File -menu [menu .m.file -tearoff 0]
.m.file add comm -label Exit -comm exit
.m add casc -label Hardness -menu [menu .m.ai -tearoff 0]
.m.ai add radio -label {Stupid} -variable DIFFICULTY -value 0
.m.ai add radio -label {Dumb} -variable DIFFICULTY -value 1
.m.ai add radio -label {Easy} -variable DIFFICULTY -value 2
.m.ai add radio -label {Medium} -variable DIFFICULTY -value 3
.m.ai add radio -label {Hard} -variable DIFFICULTY -value 4
.m.ai add radio -label {Best} -variable DIFFICULTY -value 5
.m add casc -label Help -menu [menu .m.help -tearoff 0]
.m.help add comm -label About -comm  {tk_messageBox -message $info}
  
bind . <F2> {console show}
reset .c all

gold added pix, ref older dead link

game screenshot png