Keith Vetter 2002-10-18 - I was surfing the net and came across a shareware game from Astatix called Overload that looked a bit interesting to play. However, that game has a very annoying nag screen. So I decided to write my own version.
See the help screen for full playing rules, but the idea is that each player alternate turns placing pieces on a 6x6 board. Once a square has four pieces in it, it
explodes and scatters the pieces north, south, east and west, taking over any opponent pieces. If the scattered pieces causes a square to contain four or more pieces, it too will explode. Such chain reactions are an integral part of the game and make the position very volatile.
Keith Vetter 2002-10-22 - Added a computer opponent. Stole a min-max game tree search algorithm with alpha-beta pruning from a TkAtaxx game I wrote many years ago. The "smartness" of the computer is controlled by the strength of the evaluation function and by how far ahead it searches. In this game the evaluation function is trivial--just the difference in number of pieces--but you can adjust the search depth. But beware, time required grows exponentially with increased depth; on my machine, level 5 is about the useable limit.
One interesting implementation note: originally I had the board as a hash, but it was faster to use a list instead. Also, it was faster to use lreplace and copying the list than to use upvar and lset.
package require Tk
proc Init {} {
array set ::S {
sz 40
rows 4
cols 4
size 0
robot -1
color,1 Red
color,-1 Blue
turn,1 "Red Player's turn"
turn,-1 "Blue Player's turn"
anim 60
turn 1
moves 0
level 3
level,max 8
won 0}
array set ::M {1 0 -1 1 2 2 -2 3}
}
proc DoDisplay {} {
wm title . "TkOverload"
wm minsize . 240 290
DoMenus
frame .fmsg -relief ridge -bd 2
label .msg -bd 0 -textvariable S(msg) -padx 5
label .msg3 -bd 0 -textvariable S(msg3) -padx 5
label .msg2 -relief ridge -textvariable S(msg2)
scale .level -orient horiz -from 1 -to $::S(level,max) -relief ridge \
-command DoLevel -showvalue 0 -variable S(level)
button .new -text "New Game" -command NewBoard
button .hint -text Hint -command Hint
frame .ftop
frame .ftop2
grid .ftop - -sticky news -row 0
grid .fmsg - -sticky ew -row 1
grid .msg2 - -sticky ew -row 2
grid .level .new -row 3
grid ^ .hint -sticky ew -row 4 -padx 5
grid configure .new -sticky ew -padx 5
grid rowconfigure . 0 -weight 1
grid columnconfigure . {0 1} -weight 1
pack .msg -in .fmsg -side left -fill x -expand 1
pack .msg3 -in .fmsg -side right
DisplayBoard
}
proc DisplayBoard {} {
global S
foreach w [grid slaves .ftop2] { destroy $w }
catch {destroy .ftop2}
frame .ftop2
pack .ftop2 -in .ftop -expand 1 -fill both
for {set row 0} {$row < $S(rows)} {incr row} {
for {set col 0} {$col < $S(cols)} {incr col} {
set w ".c$row,$col"
canvas $w -width $S(sz) -height $S(sz) -bd 2 -relief sunken
$w config -highlightthickness 0
bind $w <ButtonRelease-1> [list ButtonUp %W %X %Y $row $col]
bind $w <Configure> {ReCenter %W %h %w}
grid $w -row $row -in .ftop2 -column $col -sticky news
}
grid rowconfigure .ftop2 $row -weight 1
grid columnconfigure .ftop2 $row -weight 1
}
}
proc DoMenus {} {
. configure -menu [menu .m -tearoff 0]
.m add cascade -menu [menu .m.game -tearoff 0] -label "Game" -underline 0
.m add cascade -menu [menu .m.help -tearoff 0] -label "Help" -underline 0
.m.game add command -label "New Game" -under 0 -command NewBoard
.m.game add separator
.m.game add checkbutton -label "Computer Opponent" -under 0 \
-command GoRobot -variable S(robot) -onvalue -1 -offvalue 0
.m.game add command -label Hint -under 0 -command Hint
.m.game add separator
.m.game add checkbutton -label "Beginner" -under 0 -command Resize \
-variable S(size) -onvalue 0 -offvalue 1
.m.game add checkbutton -label "Expert" -under 0 -command Resize \
-variable S(size) -onvalue 1 -offvalue 0
.m.game add separator
.m.game add command -label Exit -under 0 -command exit
.m.help add command -label Help -under 0 -command Help
}
proc ReCenter {W h w} { ;# Called by configure event
set h [expr {$h / 2.0}]
set w [expr {$w / 2.0}]
$W config -scrollregion [list -$w -$h $w $h];# Recenter everything
if {[regexp {^.c(\d+),(\d+)$} $W -> r c]} { DoCircle $::B $r $c } ;# Resize
}
proc IDX {r c} {expr {$::S(cols)*$r+$c+4}}
proc GET {b r c} {lindex $b [expr {$::S(cols) * $r + $c + 4}]}
proc GETM {b x} {lindex $b $::M($x)}
proc SET {b r c v} {set i [expr {$::S(cols)*$r+$c+4}];lreplace $b $i $i $v}
proc SETM {b x v} {lreplace $b $::M($x) $::M($x) $v}
proc INCR {b r c {d 1}} {
set i [expr {$::S(cols)*$r+$c+4}]
lreplace $b $i $i [expr {[lindex $b $i] + $d}]
}
proc INCRM {b x {d 1}} {
lreplace $b $::M($x) $::M($x) [expr {[lindex $b $::M($x)] + $d}]
}
proc INFO {msg {who ""}} {
set ::S(msg$who) $msg
update idletasks
}
proc DoLevel {lvl} { .level config -label "Skill: $::S(level)" }
proc Resize {} {
global S
set S(rows) [set S(cols) [expr {$S(size) == 0 ? 4 : 6}]]
DisplayBoard ;# Redo the board
NewBoard
}
proc DoMove {row col} { ;# Move piece to row,col
global S B
Unhint ;# Turn off any hint
AnimateBox $row $col ;# Make it look a bit sexy
set B [AddOne $B $S(turn) $row $col 1] ;# Add the piece
WinOrLose $B ;# Is the game over???
NewTurn ;# Make it next player's turn
}
proc ButtonUp {w X Y row col} { ;# Called on mouse click
global S B
if {$S(won)} return ;# Game already over
if {$w != [winfo containing $X $Y]} return ;# Mouse moved out of cell
set val [GET $B $row $col]
set turn $S(turn)
if {$val < 0 && $turn > 0} return ;# Opponent's cell
if {$val > 0 && $turn < 0} return
DoMove $row $col
}
proc NewBoard {} {
global B S
set cnt [expr {$S(rows) * $S(cols) - 2}] ;# How many empty cells
set B "1 1 1 1 " ;# Metadata
append B "1 [string repeat "0 " $cnt]-1" ;# ...actual board
set S(turn) 1 ;# Player 1 goes first
set S(won) 0 ;# Game not over yet
set S(moves) 0 ;# How many turns
ShowBoard
}
proc ShowBoard {} {
global S B
for {set row 0} {$row < $S(rows)} {incr row} {
for {set col 0} {$col < $S(cols)} {incr col} {
DoCircle $B $row $col
}
}
INFO "Welcome to TkOverload"
INFO "" 2
INFO "" 3
}
proc DoCircle {brd row col} { ;# Draws the circles for a cell
global S
set w ".c$row,$col"
$w delete all
set val [GET $brd $row $col]
set size [expr {abs($val)}]
if {$size == 0} return
set width [winfo width $w]
set height [winfo height $w]
set min [expr {$width < $height ? $width : $height}]
set r [expr {$min / 6}] ;# Radius of circle
set r4 [expr {($min / 4) - 2}] ;# Position of circle
set fill $S(color,[expr {$val / $size}])
if {$size == 1} {
$w create oval -$r -$r $r $r -fill $fill -outline {}
} elseif {$size == 2} {
$w create oval [MakeBox -$r4 0 $r] -fill $fill -outline {}
$w create oval [MakeBox $r4 0 $r] -fill $fill -outline {}
} else {
$w create oval [MakeBox -$r4 $r $r] -fill $fill -outline {}
$w create oval [MakeBox $r4 $r $r] -fill $fill -outline {}
$w create oval [MakeBox 0 -$r $r] -fill $fill -outline {}
}
}
proc MakeBox {x y r} {
return [list [expr {$x-$r}] [expr {$y-$r}] [expr {$x+$r}] [expr {$y+$r}]]
}
proc AddOne {brd who row col show} {
global S
set val [GET $brd $row $col]
if {$val == 0} {
set brd [INCRM $brd $who] ;# Count cells occupied
}
if {$val == 3 || $val == -3} { ;# Will it explode
set brd [DoExplode $brd $who $row $col $show]
} else {
set brd [INCR $brd $row $col $who] ;# One more piece in cell
set brd [INCRM $brd [expr {2*$who}] 1] ;# One more total pieces
if {$show} {
DoCircle $brd $row $col
}
}
return $brd
}
proc DoExplode {brd who row col show} {
global S
set who2 [expr {2*$who}]
set brd [SET $brd $row $col 0] ;# Exploded cell is empty
set brd [INCRM $brd $who -1] ;# One less cell occupied
set brd [INCRM $brd $who2 -3] ;# Fewer total pieces
if {$show} {DoCircle $brd $row $col} ;# Erase it
foreach {dr dc} {-1 0 1 0 0 -1 0 1} { ;# Scatter in 4 directions
set r [expr {$row + $dr}]
set c [expr {$col + $dc}]
if {$r < 0 || $r >= $S(rows) || $c < 0 || $c >= $S(cols)} continue
set val [GET $brd $r $c] ;# Current cell value
set aval [expr {abs($val)}]
if {$who * $val < 0} { ;# Take ownership
set brd [INCRM $brd $who 1] ;# One more cell owned
set brd [INCRM $brd [expr {-$who}] -1] ;# One fewer cell owned
set brd [INCRM $brd $who2 $aval] ;# More total pieces
set brd [INCRM $brd [expr {-$who2}] [expr {-$aval}]]
set brd [SET $brd $r $c [expr {$who * $aval}]] ;# Update board
}
set brd [AddOne $brd $who $r $c $show] ;# Add another piece
}
return $brd
}
proc AnimateBox {row col} {
set w ".c$row,$col"
$w config -relief sunken
set width [winfo width $w] ; set height [winfo height $w]
set min [expr {($width < $height ? $width : $height)/2}]
for {set r 2} {$r < $min} {incr r 2} {
set start [clock clicks -milliseconds]
$w create rect -$r -$r $r $r -tag box
update idletasks
set remaining [expr {$::S(anim)-([clock clicks -milliseconds]-$start)}]
if {$remaining > 0} { after $remaining }
}
}
proc WinOrLose {brd} {
if {[GETM $brd 1] == 0} {
INFO "$::S(color,-1) Player won"
} elseif {[GETM $brd -1] == 0} {
INFO "$::S(color,1) Player won"
} else return
set ::S(won) 1
}
proc NewTurn {} {
global B S
if {$S(won)} return ;# Game already over
incr S(moves) ;# One more total moves
set S(turn) [expr {-$S(turn)}] ;# Other player's turn
if {$S(turn) == $S(robot)} {INFO "Computer's turn"} {INFO $S(turn,$S(turn))}
INFO "moves: $S(moves)" 3
GoRobot ;# Do possible robot move
}
proc Help {} {
catch {destroy .help}
toplevel .help
wm title .help "TkOverload Help"
wm geom .help "+[expr {[winfo x .] + [winfo width .] + 10}]+[winfo y .]"
text .help.t -relief raised -wrap word -width 70 -height 23 \
-padx 10 -pady 10 -cursor {} -yscrollcommand {.help.sb set}
scrollbar .help.sb -orient vertical -command {.help.t yview}
button .help.dismiss -text Dismiss -command {destroy .help}
pack .help.dismiss -side bottom -pady 10
pack .help.sb -side right -fill y
pack .help.t -side top -expand 1 -fill both
set bold "[font actual [.help.t cget -font]] -weight bold"
set italic "[font actual [.help.t cget -font]] -slant italic"
.help.t tag config title -justify center -foregr red -font "Times 20 bold"
.help.t tag configure title2 -justify center -font "Times 12 bold"
.help.t tag configure bullet -font $bold
.help.t tag configure n -lmargin1 15 -lmargin2 15
.help.t tag configure ital -font $italic
.help.t insert end "TkOverload\n" title
.help.t insert end "by Keith Vetter\n\n" title2
set m "TkOverload is a logic game for two people based on the game by "
append m "Overload by Astatix (see http://www.astatix.com/overload.php). "
append m "That games is shareware with a really annoying nag screen, so I "
append m "decided to write my own version. "
append m "The object is to capture all your opponents pieces.\n\n"
.help.t insert end "Overview\n" bullet $m n
set m "Each player alternate turns by clicking on either an empty cell or "
append m "a cell already containing his pieces. Each click increases the "
append m "number of pieces in the cell. When the number of pieces "
append m "reaches four, the cell "
.help.t insert end "How to Play\n" bullet $m n explodes.\n\n ital
set m "When the number of pieces in the cell reaches four, it explodes, "
append m "scattering the four pieces north, south, east and west. "
append m "Those four cells immediately change ownership to the current "
append m "player, and, if the addition of the piece causes a "
append m "cell to have four pieces, it too will explode. "
append m "Such chain reactions are a major part of the game, "
append m "and can cause the momentum in the game to change quickly."
.help.t insert end "Exploding Cells\n" bullet $m n
.help.t config -state disabled
}
################################################################
#
# Computer player code
#
# Game-tree min-max search with alpha-beta pruning. See _Fundamentals of
# Data Structures_, Horowitz, page 268.
#
proc veb {who brd lvl d} {
incr ::S(veb)
if {$lvl == 0 || [GETM $brd 1] == 0 || [GETM $brd -1] == 0} { ;# Terminal?
return [e $who $brd] ;# ...just evaluate position
}
set ans -100000 ;# Lower bound on value
set best "" ;# Current best move
set l $lvl
incr lvl -1 ;# Go down a level
set moves [AllMoves $who $brd] ;# Get all legal moves
foreach m $moves { ;# Try each possible move
foreach {row col} $m break
set brd2 [AddOne $brd $who $row $col 0] ;# Do the move
set e [veb [expr {-$who}] $brd2 $lvl [expr {-1 * $ans}]]
foreach {a bm} $e break
set a [expr {-$a}]
if {$a >= $ans} { ;# Is it a better move?
set ans $a ;# Yep, so use it
set best [concat $bm [list $m]]
}
if {$ans >= $d} break ;# BETA rule
}
return [list $ans $best]
}
proc e {who brd} { ;# Evaluate a board
if {[GETM $brd [expr {-$who}]] == 0} { return 10000 }
if {[GETM $brd $who] == 0} { return -10000 }
set me2 [GETM $brd [expr {2*$who}]]
set you2 [GETM $brd [expr {-2*$who}]]
return [expr {$me2 - $you2}]
}
proc Robot {lvl} { ;# Figure out the best move
global S B
if {$S(won)} return ;# Game already over
INFO "thinking (depth $lvl)..." 2
set S(veb) 0 ;# Count number of calls
set t [time {set mv [veb $S(turn) $B $lvl 10000]}]
foreach {val S(best)} $mv break
foreach {row col} [lindex $S(best) end] break
set tt [expr {[lindex $t 0] / 1000000.0}]
if {$tt > .001} {set tt [expr {round($tt * 1000) / 1000.0}]}
set m [expr {1000 * $tt / $S(veb)}]
INFO "Rating: $val ($S(veb) in $tt seconds)" 2
return [list $row $col]
}
proc AllMoves {who brd} { ;# Get all possible moves
global S
set moves {}
for {set row 0} {$row < $S(rows)} {incr row} {
for {set col 0} {$col < $S(cols)} {incr col} {
set v [GET $brd $row $col]
if {$v == 0} {
lappend moves [list $row $col]
} elseif {$v / abs($v) == $who} {
lappend moves [list $row $col]
}
}
}
# Randomly rearrange order of the move list
set n [expr {int(rand() * [llength $moves])}]
set moves [concat [lrange $moves $n end] [lrange $moves 0 [expr {$n - 1}]]]
return $moves
}
proc Hint {{lvl 4}} {
Unhint
if {$::S(won)} return ;# Game already over
foreach {row col} [Robot $lvl] break
set w ".c$row,$col"
set ::S(hint) [list $w [$w cget -bg]]
$w configure -bg green
after 10000 Unhint
return $::S(best)
}
proc Unhint {} { ;# Turn off hint highlighting
global S
foreach a [after info] {after cancel $a}
if {! [info exists S(hint)]} return
foreach {w bg} $S(hint) break
$w configure -bg $bg
}
proc GoRobot {} { ;# Do the robot turn
global S
if {$S(turn) == $S(robot)} {
eval DoMove [Robot $S(level)]
}
}
################################################################
################################################################
################################################################
Init
DoDisplay
NewBoard
gold added pix