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)