isolation

rdt 2006.06.08 - removed the spam here.

Isolation is a simple board game. The object is to "isolate" the opponent such that he cannot make any legal moves. Here is a screenshot:

    A  B  C  D  E  F  G  H

 1  *  -  -  -  -  O  *  *
 2  *  *  *  *  -  -  *  *
 3  -  *  *  *  -  *  -  *
 4  -  -  *  *  *  -  -  -
 5  -  X  *  *  *  *  *  -
 6  *  *  *  *  *  *  *  *

 X, move a square>

I challenge someone to write for me a nifty Tk interface.

The code design is based on RS's TkAlign4. The AI is a variant of my alpha-beta search from iConnect4.

Blah blah blah copylefted GPL blah.

Source code below. For your convenience you can also download it from http://tcl.jtang.org/isolation/isolation.tcl

 #!/usr/bin/tclsh
 
 # An implementation of the Isolation board game by Jason Tang
 # ([email protected]).
 #
 # Rules of the game:
 #
 # Two players ("X" and "O") each pick a starting square on an 8x6
 # board.  They then take alternating turns moving their piece any
 # number of squares in a straight line, horizontally, vertically, or
 # diagonally.  When their piece leaves a square that originating
 # square is marked as "captured".  A move may not jump over any
 # captured squares nor over the opponent's piece.  The object of the
 # game is to isolate the opponent such that on his turn he is unable
 # to move anywhere.
 
 ######################################################################
 # Model
 
 # creates and returns a new isolation board
 proc initBoard {} {
     set row {0 0 0 0 0 0 0 0}
     for {set i 0} {$i < 6} {incr i} {
         lappend board $row
     }
     return $board
 }
 
 # Given the board and two tuples, starting and destination square in
 # {r c} form, attempts to move the piece to the destination.  If the
 # move is legal (i.e., in a straight line and not over any captured
 # squares) then returns the new board and a status of 0.  Otherwise
 # return the original board and a status of -1.
 proc makeMove {board pos newPos} {
     foreach {or oc} $pos {}
     foreach {nr nc} $newPos {}
     set dy [expr {$or - $nr}]
     set dx [expr {$oc - $nc}]
     if {($dy == 0 && $dx != 0) || ($dx == 0 && $dy != 0) || (abs($dx) == abs($dy))} {
         # ensure that no intervening squares are filled
         set y $or
         set x $oc
         set ex [expr {$dx > 0 ? -1 : $dx < 0 ? 1 : 0}]
         set ey [expr {$dy > 0 ? -1 : $dy < 0 ? 1 : 0}]
         while {$dx != 0 || $dy != 0} {
             incr x $ex
             incr y $ey
             if {[lindex2 $board $y $x] == 1} {
                 return [list $board -1]
             }
             incr dx $ex
             incr dy $ey
         }
         set board [lsetBoard $board $nr $nc 1]
         return [list $board 0]
     } else {
         return [list $board -1]
     }
 }
 
 # Given a board and a {r c} tuple returns 1 if the player cannot move
 # (i.e., dead), or 0 if still alive.
 proc isDead {board pos} {
     foreach {r c} $pos {}
     return [isDead2 $board $r $c]
 }
 
 # Given a board, a row, and a column, returns 1 if the player cannot
 # move (i.e., dead), or 0 if still alive.
 proc isDead2 {board r c} {
     foreach {dx dy} {1 0 -1 0   0 1 0 -1   1 1 -1 -1   1 -1 -1 1} {
         set x [expr {$c + $dx}]
         set y [expr {$r + $dy}]
         if {$x >= 0 && $x < 8 && $y >= 0 && $y < 6 && 
                 [lindex2 $board $y $x] == 0} {
             return 0
         }
     }
     return 1
 }
 
 ######################################################################
 # View
 
 # Pretty-prints the board with super-spiffy column and row header.
 proc showBoard {board p1 p2} {
     puts "   A  B  C  D  E  F  G  H"
     puts ""
     set rowNum 0
     foreach row $board {
         puts -nonewline [expr {$rowNum + 1}]
         set colNum 0
         foreach col $row {
             puts -nonewline "  "
             set coord [list $rowNum $colNum]
             if {$p1 == $coord} {
                 puts -nonewline "X"
             } elseif {$p2 == $coord} {
                 puts -nonewline "O"
             } else {
                 switch -- $col {
                     0  { puts -nonewline "-" }
                     1  { puts -nonewline "*" }
                     default { puts stderr "Illegal board"; exit -1 }
                 }
             }
             incr colNum
         }
         puts ""
         incr rowNum
     }
 }
 
 ######################################################################
 # Controller
 
 # Fetches a starting legal square from a player (human or AI).
 proc initSquare {board p1 p2 player} {
     if {$player == 1} {
          return [getSquare $board "X, pick a starting square> " $p1 $p2]
 #        return [getInitAI $board]
     } else {
 #        return [getSquare $board "O, pick a starting square> " $p1 $p2]
         return [getInitAI $board]
     }
 }
 
 # Fetches a move from a player -- does not actually check if move is
 # legal, only that the square specified is on the board and is not
 # already occupied.
 proc getMove {board p1 p2 player} {
     if {$player == 1} {
         set square [getSquare $board "X, move a square> " $p1 $p2]
 #        return [getMoveAI $board $player $p1 $p2]
     } else {
 #        set square [getSquare $board "O, move a square> " $p1 $p2]        
         return [getMoveAI $board $player $p2 $p1]
     }
     return $square
 }
 
 # Prompts the user for a square.
 proc getSquare {board prompt p1 p2} {
     set legalSquare 0
     showBoard $board $p1 $p2
     puts ""
     while {!$legalSquare} {
         puts -nonewline $prompt
         flush stdout
         set line [gets stdin]
         if {$line == "?"} {
             showBoard $board $p1 $p2
             puts ""
         } else {
             set col [string index $line 0]
             set row [string index $line 1]
             set legalSquare 1
             switch -- $col {
                 "a" - "A" { set col 0 }
                 "b" - "B" { set col 1 }
                 "c" - "C" { set col 2 }
                 "d" - "D" { set col 3 }
                 "e" - "E" { set col 4 }
                 "f" - "F" { set col 5 }
                 "g" - "G" { set col 6 }
                 "h" - "H" { set col 7 }
                 default {
                     puts "Illegal column specified"
                     set legalSquare 0
                 }
             }
             if {$row == "" || ![string is digit $row] || $row < 1 || $row > 6} {
                 puts "Illegal row specified"
                 set legalSquare 0
             } else {
                 incr row -1
             }
             if $legalSquare {
                 set square [list $row $col]
                 if {[lindex2 $board $row $col] != 0} {
                     puts "Specified location already taken"
                     set legalSquare 0
                 }
             }
         }
     }
     return $square
 }
 
 ######################################################################
 # AI stuff
 
 # The static board evaluator.  Returns an integer where bigger number
 # is better for the owner at location {row col}.
 proc getScore {board row col} {
     set sum 0
     foreach {dx dy} {-1 0 1 0   0 -1 0 1   1 -1 -1 1   -1 -1 1 1} {
         set y [expr {$row + $dy}]
         set x [expr {$col + $dx}]
         set score 1
         while {$x >= 0 && $x <= 8 && $y >= 0 && $y <= 6} {
             if {[lindex2 $board $y $x] == 0} {
                 set score [expr {$score << 1}]
             } else {
                 break
             }
             incr x $dx
             incr y $dy
         }
         incr sum $score
     }
     if {$sum == 8} {
         return -10000
     }        
     return $sum
 }
 
 # Randomly pick a starting square
 proc getInitAI {board} {
     while {1} {
         set x [expr {int (rand () * 8)}]
         set y [expr {int (rand () * 6)}]
         if {[lindex2 $board $y $x] == 0} {
             return [list $y $x]
         }
     }
 }
 
 
 # Returns a tuple of where to move given the current board state and
 # which player to examine.
 proc getMoveAI {board player myrc opprc} {
     # MAXDEPTH:  number of plies to search
     set MAXDEPTH 3
     puts "Computer is thinking hard (using depth $MAXDEPTH)..."
     
     # keep track of the best moves found thus far
     set scores {}
     # and keep track of the number of expanded nodes
     set ::numNodesExpanded 0
 
     foreach {row col} $myrc {}
     foreach {row2 col2} $opprc {}
     set opp [expr {-1 * $player}]
     # only try positions not already taken
     foreach {dx dy} {-1 0 1 0   0 -1 0 1   1 -1 -1 1   -1 -1 1 1} {
         set y [expr {$row + $dy}]
         set x [expr {$col + $dx}]
         while {$x >= 0 && $x <= 8 && $y >= 0 && $y <= 6} {
             if {[lindex2 $board $y $x] == 0} {
                 set dupBoard [lsetBoard $board $y $x 1]
                 set result [getMoveAB $dupBoard $row2 $col2 $y $x 
                                 $player $opp -100001 100001 $MAXDEPTH]
                 lappend scores [list $result $y $x]
             } else {
                 break
             }
             incr x $dx
             incr y $dy
         }
     }
     # now pick the best score; in case of tie randomly choose one
     set bestMoves [list [lindex $scores 0]]
     set bestScore [lindex2 $scores 0 0]
     foreach currentTuple [lrange $scores 1 end] {
         set currentScore [lindex $currentTuple 0]
         if {$currentScore > $bestScore} {
             set bestMoves [list $currentTuple]
             set bestScore $currentScore
         } elseif {$currentScore == $bestScore} {
             lappend bestMoves $currentTuple
         }
     }
     set choiceTuple [lindex $bestMoves [expr {int (rand () * [llength $bestMoves])}]]
     puts "After searching $::numNodesExpanded nodes, best score was $bestScore"
     return [list [lindex $choiceTuple 1] [lindex $choiceTuple 2]]
 }
 
 
 # Perform a somewhat modified alpha-beta search on the board --
 # modified in that the algorithm will short-circuit whenever it
 # detects an ending condition.
 proc getMoveAB {board r c r2 c2 me current alpha beta depth} {
     # because this node was expanded increment the counter
     incr ::numNodesExpanded
     # check if search is at a terminal state
     if {$depth <= 0} {
         set myscore [getScore $board $r $c]
         if {$me != $current} {
             set myscore [expr {-1 * $myscore}]
         }
         return $myscore
     }
     if {[isDead2 $board $r $c]} {
         if {$me == $current} {
             set myscore -10000
         } else {
             set myscore 10000
         }
         return $myscore
     }
     
     # else continue recursing by making another move
     incr depth -1
     set newCurrent [expr {-1 * $current}]
     if {$me == $current} {
         # examining a max node -- do alpha pruning
         foreach {dx dy} {-1 0 1 0   0 -1 0 1   1 -1 -1 1   -1 -1 1 1} {
             set y [expr {$r + $dy}]
             set x [expr {$c + $dx}]
             while {$x >= 0 && $x <= 8 && $y >= 0 && $y <= 6} {
                 if {[lindex2 $board $y $x] == 0} {
                     set dupBoard [lsetBoard $board $y $x 1]
                     set score [getMoveAB $dupBoard $r2 $c2 $y $x 
                                    $me $newCurrent $alpha $beta $depth]
                     if {$score > $alpha} {
                         set alpha $score
                     }
                     if {$alpha >= $beta} {
                         return $alpha
                     }
                 } else {
                     break
                 }
                 incr x $dx
                 incr y $dy
             }
         }
         return $alpha
     } else {
         # examining a min node -- do beta pruning
         foreach {dx dy} {-1 0 1 0   0 -1 0 1   1 -1 -1 1   -1 -1 1 1} {
             set y [expr {$r + $dy}]
             set x [expr {$c + $dx}]
             while {$x >= 0 && $x <= 8 && $y >= 0 && $y <= 6} {
                 if {[lindex2 $board $y $x] == 0} {
                     set dupBoard [lsetBoard $board $y $x 1]
                     set score [getMoveAB $dupBoard $r2 $c2 $y $x 
                                    $me $newCurrent $alpha $beta $depth]
                     if {$score < $beta} {
                         set beta $score
                     }
                     if {$beta <= $alpha} {
                         return $beta
                     }
                 } else {
                     break
                 }
                 incr x $dx
                 incr y $dy
             }
         }
         return $beta
     }
 }
 
 
 ######################################################################
 # Functions needed for tcl8.3 compatibility
 
 proc lindex2 {list ind1 ind2} {
     return [lindex [lindex $list $ind1] $ind2]
 }
 
 proc lsetBoard {board row column newValue} {
     set oldRow [lindex $board $row]
     set newRow [lrange $oldRow 0 [expr {$column - 1}]]
     lappend newRow $newValue
     set newRow [concat $newRow [lrange $oldRow [expr {$column + 1}] end]]
     set newBoard [lrange $board 0 [expr {$row - 1}]]
     lappend newBoard $newRow
     set newBoard [concat $newBoard [lrange $board [expr {$row + 1}] end]]
     return $newBoard
 }
 
 ######################################################################
 # main script
 
 set board [initBoard]
 set p(1) {}
 set p(-1) {}
 
 # Get initial positions
 set p(1) [initSquare $board $p(1) $p(-1) 1]
 set board [lsetBoard $board [lindex $p(1) 0] [lindex $p(1) 1] 1]
 set p(-1) [initSquare $board $p(1) $p(-1) -1]
 set board [lsetBoard $board [lindex $p(-1) 0] [lindex $p(-1) 1] 1]
 
 # Start game.
 set gameOver 0
 set player 1
 while {1} {
     if {[isDead $board $p($player)]} {
         break
     }
     set square [getMove $board $p(1) $p(-1) $player]
     foreach {board result} [makeMove $board $p($player) $square] {}
     if {$result == -1} {
         puts "Illegal move."
     } else {
         set p($player) $square
         set player [expr {-1 * $player}]
     }
 }
 if {$player == 1} {
     puts "O is the winner!"
 } else {
     puts "X is the winner!"
 }    
 showBoard $board $p(1) $p(-1)