Updated 2016-05-03 19:36:03 by gold

Keith Vetter 2004-09-10 : Here's another version of the children's game Tic-Tac-Toe. My 5-year old daughter has just discovered this game and so I threw this together this morning. There's another tic-tac-toe page--A little Tic Tac Toe game--but I wanted a sexier interface.

MG - A nice little game :)

KPV - Added two levels of computer opponent: random and smart--which will win if possible, block if necessary or else do a random move. Playing perfect tic-tac-toe is non-trivial problem, and so is left as an exersize for the reader (plus my 5 year old likes to win when playing).
##+##########################################################################
 #
 # tictactoe.tcl - plays tic tac toe
 # by Keith Vetter  Sept 10, 2004
 #
 # Sept 11 : added 2 levels of computer opponent: random and smart--which
 #            will win if possible, block if necessary or do a random move
 
 package require Tk
 
 array set S {title "Tic Tac Toe" who,1 "X" who,0 "" who,-1 "O" robot "0"}
 array set C {bars red X blue O green win yellow}
 namespace eval ::Robot {
    variable skill Smart
 }
 
 proc DoDisplay {} {
    wm title . $::S(title)
    frame .ctrl -relief ridge -bd 2 -padx 5 -pady 5
    canvas .c -relief raised -bd 2 -height 500 -width 500 -highlightthickness 0
    pack .c -side top -fill both -expand 1
    pack .ctrl -side top -fill both
 
    bind all <Key-F2> {console show}
    bind .c <Configure> {ReCenter %W %h %w}
    DoCtrlFrame
 }
 proc DrawBoard {{redraw 0}} {
    global S B GAME C
 
    if {$redraw} {                              ;# Must redraw everything
        .c delete all
        set w2 [expr {$B(w2) - 15}]             ;# Make a little margins
        set h2 [expr {$B(h2) - 15}]
        set hbar [expr {$h2 / 3.0}]
        set vbar [expr {$w2 / 3.0}]
 
        set B(0) [list -$w2   -$h2   -$vbar -$hbar] ;# All 9 cells
        set B(1) [list -$vbar -$h2    $vbar -$hbar]
        set B(2) [list  $vbar -$h2    $w2   -$hbar]
        set B(3) [list -$w2   -$hbar -$vbar  $hbar]
        set B(4) [list -$vbar -$hbar  $vbar  $hbar]
        set B(5) [list  $vbar -$hbar  $w2    $hbar]
        set B(6) [list -$w2    $hbar -$vbar  $h2]
        set B(7) [list -$vbar  $hbar  $vbar  $h2]
        set B(8) [list  $vbar  $hbar  $w2    $h2]
 
        for {set i 0} {$i < 9} {incr i} {       ;# Rectangle for each cell
            .c create rect $B($i) -tag b$i -fill {} -outline {}
            .c bind b$i <Button-1> [list DoClick $i]
            set B($i) [ShrinkBox $B($i) 25]
        }
        .c create line -$w2 $hbar $w2 $hbar -tag bar ;# Draw the cross bars
        .c create line -$w2 -$hbar $w2 -$hbar -tag bar
        .c create line $vbar -$h2 $vbar $h2 -tag bar
        .c create line -$vbar -$h2 -$vbar $h2 -tag bar
        .c itemconfig bar -width 20 -fill $::C(bars) -capstyle round
    }
    .new config -state [expr {$GAME(tcnt) == 0 ? "disabled" : "normal"}]
 
    for {set i 0} {$i < 9} {incr i} {
        .c itemconfig b$i -fill {}              ;# Erase any win lines
        DrawXO $GAME(board,$i) $i
    }
    foreach i $GAME(win) {                      ;# Do we have a winner???
        .c itemconfig b$i -fill $C(win)
    }
 }
 proc DoCtrlFrame {} {
    button .new -text "New Game" -command NewGame -bd 4
    .new configure -font "[font actual [.new cget -font]] -weight bold"
    option add *Button.font [.new cget -font]
    label .status -textvariable S(msg) -font {Times 36 bold} -bg white \
        -bd 5 -relief ridge
    button .about -text About -command \
        [list tk_messageBox -message "$::S(title)\nby Keith Vetter, Sept 2004"]
 
    frame .r -bd 2 -relief ridge
    pack .r -side bottom 
    label .r.lc -text "Computer" -font [.new cget -font]
    label .r.lrobot -text "Plays: "
    spinbox .r.robot -values {O None X} -textvariable S(robot) -wrap 1 \
        -width 6 -justify center -command ::Robot::IsTurn
    label .r.llevel -text "Level: "
    spinbox .r.level -values {Smart Random} -textvariable ::Robot::skill \
        -wrap 1 -width 8 -justify center
    grid .r.lc - -row 0
    grid .r.lrobot .r.robot -sticky we
    grid .r.llevel .r.level -sticky we
 
 
    pack .status -in .ctrl -side right -fill both -expand 1
    pack .r -in .ctrl -side right -fill both -padx 5
    pack .new .about -in .ctrl -side top -fill x -pady 2 
 }
 proc ShrinkBox {xy d} {
    foreach {x y x1 y1} $xy break
    return [list [expr {$x+$d}] [expr {$y+$d}] [expr {$x1-$d}] [expr {$y1-$d}]]
 }
 ##+##########################################################################
 # 
 # Recenter -- keeps 0,0 at the center of the canvas during resizing
 # 
 proc ReCenter {W h w} {                   ;# Called by configure event
    set ::B(h2) [expr {$h / 2}]
    set ::B(w2) [expr {$w / 2}]
    $W config -scrollregion [list -$::B(w2) -$::B(h2) $::B(w2) $::B(h2)]
    DrawBoard 1
 }
 ##+##########################################################################
 # 
 # DrawXO -- draws appropriate mark in a given cell
 # 
 proc DrawXO {who where} {
    global S B C
 
    if {$S(who,$who) eq "X"} {          
        foreach {x0 y0 x1 y1} $B($where) break
        .c create line $x0 $y0 $x1 $y1 -width 20 -fill $C(X) -capstyle round \
            -tag xo$where
        .c create line $x0 $y1 $x1 $y0 -width 20 -fill $C(X) -capstyle round \
            -tag xo$where
    } elseif {$S(who,$who) eq "O"} {
        .c create oval $B($where) -width 20 -outline $C(O) -tag xo$where
    } else {
        .c delete xo$where
    }
 }
 ##+##########################################################################
 # 
 # InitGame -- resets all variables to start a new game
 # 
 proc InitGame {} {
    global GAME S
 
    set GAME(state) play
    set GAME(turn) 1
    set GAME(tcnt) 0
    set GAME(win) {}
    for {set i 0} {$i < 9} {incr i} {
        set GAME(board,$i) 0
    }
    set S(msg) "X starts"
 }
 ##+##########################################################################
 # 
 # NewGame -- starts a new game
 # 
 proc NewGame {} {
    InitGame
    DrawBoard
    if {$::S(who,$::GAME(turn)) == $::S(robot)} {
        after idle ::Robot::Go
    }
 }
 ##+##########################################################################
 # 
 # DoClick -- handles button click in a cell
 # 
 proc DoClick {where} {
    global GAME S
 
    if {$GAME(state) ne "play"} return          ;# Game over
    if {$GAME(board,$where) != 0} return        ;# Not empty
    set GAME(board,$where) $GAME(turn)
    set GAME(turn) [expr {- $GAME(turn)}]
    incr GAME(tcnt)
    set S(msg) "$S(who,$GAME(turn))'s turn"
 
    set n [WhoWon]                              ;# Do we have a winner???
    if {$n != 0} {
        set GAME(state) finished
        set GAME(win) [lrange $n 1 end]
        set S(msg) "$S(who,[lindex $n 0]) Wins!"
    } elseif {$GAME(tcnt) == 9} {               ;# Is the game a draw???
        set GAME(state) finished
        set S(msg) "Draw"
    }
    DrawBoard
    if {$S(who,$GAME(turn)) == $S(robot)} {
        after idle ::Robot::Go
    }
 }
 ##+##########################################################################
 # 
 # WhoWon -- determines if anyone has won the game
 # 
 proc WhoWon {} {
    foreach {a b c} {0 1 2 3 4 5 6 7 8 0 3 6 1 4 7 2 5 8 0 4 8 2 4 6} {
        set who $::GAME(board,$a)
        if {$who == 0} continue
        if {$who != $::GAME(board,$b) || $who != $::GAME(board,$c)} continue
        return [list $who $a $b $c]
    }
    return 0
 }
 ##+##########################################################################
 # 
 # ::Robot::Go -- gets and does robot's move
 # 
 proc ::Robot::Go {} {
    variable skill
    if {$::GAME(state) ne "play"} return        ;# Game over
    set move [::Robot::$skill]
    if {$move == {}} return
    ::DoClick $move
 }
 proc ::Robot::Random {} {                       ;# Picks a random move
    set empty {}
    for {set i 0} {$i < 9} {incr i} {
        if {$::GAME(board,$i) == 0} {
            lappend empty $i
        }
    }
    return [lindex $empty [expr {int(rand() * [llength $empty])}]]
 }
 ##+##########################################################################
 # 
 # ::Robot::Smart -- does winning move if possible, blocks if necessary
 # or does a random move
 # 
 proc ::Robot::Smart {} {
    global GAME
 
    set blockers {}
    foreach {aa bb cc} {0 1 2 3 4 5 6 7 8 0 3 6 1 4 7 2 5 8 0 4 8 2 4 6} {
        set a $GAME(board,$aa)
        set b $GAME(board,$bb)
        set c $GAME(board,$cc)
        if {$a * $b * $c != 0} continue         ;# No empty slots
        if {$a + $b + $c == 2*$GAME(turn)} {    ;# Winning move
            if {$a == 0} { return $aa}
            if {$b == 0} { return $bb}
            if {$c == 0} { return $cc}
            error "no empty spot"               ;# Can't happen
        }
        if {$a + $b + $c == -2*$GAME(turn)} {   ;# Blocking move
            if {$a == 0} { lappend blockers $aa}
            if {$b == 0} { lappend blockers $bb}
            if {$c == 0} { lappend blockers $cc}
        }
    }
    if {$blockers != {}} {
        return [lindex $blockers [expr {int(rand() * [llength $blockers])}]]
    }
    return [::Robot::Random]
 }
 ##+##########################################################################
 # 
 # ::Robot::IsTurn -- called when who robot is changes and we may need to move
 # 
 proc ::Robot::IsTurn {} {
    if {$::S(who,$::GAME(turn)) == $::S(robot)} {
        after idle ::Robot::Go
    }
 }    
 
 InitGame
 DoDisplay
 NewGame


Screenshots

gold added pix

uniquename 2013jul29

In case the image above (stored at 'external site' flickr.com) disappears, here is a 'locally stored' image.

(Thanks to 'gnome-screenshot', 'mtpaint', and ImageMagick 'convert' on Linux for, respectively, capturing the screen in a PNG file, cropping the image, and converting the resulting PNG file to a JPEG file less than 10% the size of the PNG file. Thanks to FOSS developers everywhere --- including Linux kernel and Gnu developers. The 'mv' command and the ImageMagick 'identify' command were used in a shell script to easily rename the cropped image file to contain the image dimensions in pixels. And the 'mtpaint', convert-to-JPEG, and rename programs/scripts were triggered by use of the 'Open with' and 'Nautilus scripts' capabilities of the Nautilus 2.28.1 file manager --- thanks to the old Gnome 2.x developers, who have been unfortunately replaced by Gnome 3.x developers, who have no idea what the Gnome 2.x developers wrought and are decimating a great desktop environment.)

This is an image of the Tic Tac Toe GUI when it first comes up ---- before any controls on the GUI are used.