HJG This is Montana Solitaire from Keith Vetter, with an added AutoPlayer, and some tweaks to fit the display better on a 1024x768 screen.
To save space, the common card images from card_img are used.
KPV Just curious, have you tried different strategies or noticed one being better than another? Ultimately, just how much skill is there and how much is it just mechanical?
HJG: it is a really simple, purely mechanical strategy:
There are some simple improvements / heuristics I can think of, e.g.:
gap xx 4h yy zz ...
should get the two of hearts, thus saving one move.
A more complex improvement: simulate all possible plays until the next redeal, building a tree of moves and note for each of these sequences of play how many cards are in their final position. Then select the best of these. This amounts to a complete enumeration, but I think the tree would be small enough to be feasible.
But, for testing such things, it would be nice to have selectable seed for the random numbers, and a dump-routine for the card-positions (logging), and record/replay of moves, etc. But that would need lots of additional code, and I wanted this version to get out as soon as possible :-)
##+########################################################################## # # Montana -- plays Montana solitaire # by Keith Vetter, April 2006 / May 2006 # 2006-04-30 HaJo Gurt: Resize+wm geom, card_img.tcl # 2006-05-01 HaJo Gurt: Find2, Print # 2006-05-02 HaJo Gurt: Autoplay1 # 2006-05-03 HaJo Gurt: update to KPV 2005-05-01, F1, focus -force, Auto # TODO: AutoPlay2 package require Tk array set S {title "Montana Solitaire" auto 0 delay 500 lm 10 bm 10 tm 70 padx 5 pady 5 color green4 gcolor cyan gwidth 6} wm title . $S(title) wm geom . 1000x552+8+16 proc DoDisplay {} { global S if {! [catch {package require tile 0.7.2}]} { namespace import -force ::ttk::button namespace import -force ::ttk::scrollbar } eval destroy [winfo child .] DoMenus canvas .c -width $S(w) -height $S(h) -bg $S(color) -highlightthickness 0 frame .bottom -bd 2 -relief ridge label .lmoves -text "Moves:" -anchor e .lmoves configure -font "[font actual [.lmoves cget -font]] -weight bold" option add *Label.font [.lmoves cget -font] label .vmoves -textvariable ::STATS(moves) -anchor e label .lgood -text "Good:" -anchor e label .vgood -textvariable ::STATS(good) -anchor e label .lredeals -text "Redeals:" -anchor e label .vredeals -textvariable ::STATS(redeals) -anchor e grid .lgood .vgood -in .bottom -sticky ew grid .lmoves .vmoves -in .bottom -sticky ew grid .lredeals .vredeals -in .bottom -sticky ew grid columnconfigure .bottom 2 -weight 1 pack .c -side top -fill both -expand 1 pack .bottom -side top -fill x bind all <Key-F1> Help bind all <Key-F2> StartGame bind all <Key-F3> {console show} bind all <Control-z> Undo bind . <Key-1> {Auto 1} bind . <Key-2> {Auto R} bind . <Key-3> {Auto End} bind . <Key-0> {Auto Off} .c create text [expr {$S(w)/2}] 0 -text $S(title) -fill red \ -font {Times 42 bold} -anchor n -tag title GetCardPlacement focus -force . } ##+########################################################################## # # DoMenus -- isn't installing menus really verbose and clunky? # proc DoMenus {} { option add *Menu.tearOff 0 . config -menu [menu .menu] menu .menu.game .menu add cascade -label "Game" -underline 0 -menu .menu.game .menu.game add command -label "New Game" -underline 0 -command StartGame \ -accelerator "F2" .menu.game add command -label "Restart" -underline 0 -command [list StartGame 1] .menu.game add separator .menu.game add command -label "Undo" -underline 0 -command Undo \ -accelerator "Ctrl-Z" .menu.game add separator .menu.game add command -label "Exit" -underline 1 -command exit menu .menu.help .menu add cascade -label "Help" -underline 0 -menu .menu.help .menu.help add command -label "Help" -underline 0 -command Help .menu.help add command -label "About" -underline 0 -command About .menu.help add separator .menu.help add command -label "Autoplay 1 move" -underline 9 -command {Auto 1} .menu.help add command -label "Autoplay 2 until Redeal" -underline 9 -command {Auto R} .menu.help add command -label "Autoplay 3 to end" -underline 9 -command {Auto End} .menu.help add separator .menu.help add command -label "Autoplay 0ff" -underline 9 -command {Auto Off} } ####+####1####+####2####+####3####+####4####+####5####+####6####+####7####+####8 # # GetCardPlacement -- sets up board with lots of empty tagged items # proc GetCardPlacement {} { global S for {set idx 0} {$idx < 52} {incr idx} { set row [expr {$idx / 13}] set col [expr {$idx % 13}] set x [expr {$S(lm) + $col * ($S(cw)+$S(padx))}] set y [expr {$S(tm) + $row * ($S(ch)+$S(pady))}] set x1 [expr {$x+$S(cw)}] set y1 [expr {$y+$S(ch)}] .c create line $x $y $x1 $y $x1 $y1 $x $y1 $x $y $x1 $y -fill {} \ -tag [list m m$row,$col] -width $S(gwidth) -joinstyle miter incr x1 -1 .c create rect $x $y $x1 $y1 -tag g$row,$col -fill $S(color) \ -outline $S(color) .c create image $x $y -tag c$row,$col -anchor nw .c bind c$row,$col <Button-1> [list Click $row $col] .c bind c$row,$col <Button-3> [list Hint $row $col] #.c bind g$row,$col <Button-1> [list Hint2 $row $col] .c bind g$row,$col <Double-Button-1> [list Hint2 $row $col double] .c bind g$row,$col <Button-3> [list Hint2 $row $col] bind all <ButtonPress-2> [list Hint3 down] bind all <ButtonRelease-2> [list Hint3 up] } } ##+########################################################################## # # Click -- handles moving a card after clicking on it # proc Click {row col} { global B .c delete flash set card $B($row,$col) if {$card eq "gap"} return ;# Be safe, shouldn't happen if {$card eq "X" } return ;# Be safe, shouldn't happen set pred [CardPredecessor $card] if {! [string match "2?" $card]} { foreach {r c} $B(r,$pred) break incr c } else { set c 0 for {set r 0} {$r < 3} {incr r} { if {$B($r,$c) eq "gap"} break } } if {$B($r,$c) eq "gap"} { lappend B(undo) [list $row $col $r $c] .menu.game entryconfig "Undo" -state normal incr ::STATS(moves) MoveCardToGap $row $col $r $c } else { Flash bad $row $col } } ##+########################################################################## # # Flash -- temporarily highlights a card for either bad move or hint # proc Flash {how args} { array set delays {bad 300 good 1000 all 15000} array set clr {bad red good magenta all yellow} foreach aid [after info] { after cancel $aid } .c delete flash foreach {row col} $args { foreach {x0 y0 x1 y1} [.c bbox c$row,$col] break .c create line $x0 $y0 $x1 $y0 $x1 $y1 $x0 $y1 $x0 $y0 $x1 $y0 \ -tag flash -width 7 -fill $clr($how) -capstyle round if {$how eq "bad"} { .c create line $x0 $y0 $x1 $y1 -tag flash -width 10 -fill $clr($how) .c create line $x0 $y1 $x1 $y0 -tag flash -width 10 -fill $clr($how) } } after $delays($how) .c delete flash } ##+########################################################################## # # CanMove -- returns true if a valid move still exists # proc CanMove {} { global B foreach gap $B(gaps) { foreach {row col} $gap break if {$col == 0} { return 1 } set left $B($row,[expr {$col-1}]) if {$left eq "gap"} continue if {! [string match "k?" $left]} { return 1 } } return 0 } ##+########################################################################## # # MoveCardToGap -- moves card from row/col to the gap at r/c # proc MoveCardToGap {row col r c} { global B set card $B($row,$col) set B($row,$col) "gap" set B($r,$c) $card set B(r,$card) [list $r $c] set n [lsearch $B(gaps) [list $r $c]] lset B(gaps) $n [list $row $col] .c itemconfig c$r,$c -image ::img::$card .c itemconfig c$row,$col -image {} EndTurn } ##+########################################################################## # # EndTurn -- Handles end-of-turn logic # proc EndTurn {} { HighlightGood set ::STATS(good) [llength [FindGood]] if {[CanMove]} return if {$::STATS(good) == 48} { set ::S(auto) 0 Print "Finished: $::STATS(moves) moves, $::STATS(redeals) redeals.\n\n" tk_messageBox -title $::S(title) -message "You Won!" } else { if {$::S(auto) == 2} { set ::S(auto) 0 } Print "Redeal\n" tk_messageBox -title $::S(title) -message "No more moves.\n\nRedeal" Redeal } } ##+########################################################################## # # HighlightGood -- highlight all cards in their proper position # proc HighlightGood {} { global B .c itemconfig m -fill {} foreach card [FindGood] { foreach {row col} $B(r,$card) break .c itemconfig m$row,$col -fill $::S(gcolor) } } ##+########################################################################## # # FindGood -- finds all cards that are in their proper position # proc FindGood {} { global B set pos {2 3 4 5 6 7 8 9 t j q k} set good {} for {set row 0} {$row < 4} {incr row} { set head $B($row,0) if {! [string match "2?" $head]} continue set hsuit [string index $head 1] for {set col 0} {$col < 13} {incr col} { foreach {pip suit} [split $B($row,$col) ""] break if {$suit ne $hsuit} break if {$pip ne [lindex $pos $col]} break lappend good $B($row,$col) } } return $good } ##+########################################################################## # # About -- tell something about us # proc About {} { set txt "$::S(title)\n\nby Keith Vetter\nApril, 2006" append txt "\n\nAutoplay by HaJo Gurt" tk_messageBox -icon info -message $txt -title "About $::S(title)" } ##+########################################################################## # # Help -- a simple help screen # proc Help {} { catch {destroy .help} toplevel .help wm title .help "$::S(title) Help" #wm geom .help "+[expr {[winfo x .] + [winfo width .] + 10}]+[winfo y .]" set t .help.t text $t -relief raised -wrap word -width 70 -height 30 \ -padx 10 -pady 10 -cursor {} -yscrollcommand {.help.sb set} scrollbar .help.sb -orient vertical -command [list $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 $t -side top -expand 1 -fill both set bold "[font actual [$t cget -font]] -weight bold" set italic "[font actual [$t cget -font]] -slant italic" $t tag config title -justify center -foregr red -font "Times 20 bold" $t tag configure title2 -justify center -font "Times 12 bold" $t tag configure header -font $bold $t tag configure n -lmargin1 10 -lmargin2 10 $t tag configure bullet -lmargin1 20 -lmargin2 30 $t insert end "$::S(title)\n" title $t insert end "by Keith Vetter\n\n" title2 $t insert end "$::S(title) is a simple solitaire game that goes by " $t insert end "a variety of names including \x22Gaps\x22, \x22Rangoon\x22, " $t insert end "\"BlueMoon\", \x22Station\x22 and \x22Montana Aces\x22.\n\n" $t insert end "Tableau\n" header $t insert end "At the start of the game, all 52 cards are shuffled and " $t insert end "dealt face up in four rows of thirteen cards. The four aces " $t insert end "are removed creating four gaps.\n\n" $t insert end "Object\n" header $t insert end "The objective is to rearrange the cards so that each row " $t insert end "contains the cards of a single suit ordered from deuce to " $t insert end "king. (The last column in each row will contain a gap " $t insert end "instead of the ace.)\n\n" $t insert end "The Play\n" header $t insert end "If a gap appears in the first column, you can move any " $t insert end "deuce to that position. If a gap appears elsewhere, you " $t insert end "move there only the card with same suit and one higher " $t insert end "rank than the card to the left of the gap. For example, " $t insert end "if the 5 of Hearts appears to the left of a gap, you " $t insert end "can move the 6 of Hearts to that gap. If the card to " $t insert end "the left of the gap is a King or another gap, you cannot " $t insert end "move any card to that gap.\n\n" $t insert end "Whenever you move a card, you'll fill one gap, but create " $t insert end "a new one.\n\n" $t insert end "Mechanics\n" header $t insert end "o Click on a card to move it to a gap (if legal)\n" bullet $t insert end "o Right-click on card to highlight its predecessor\n" bullet $t insert end "o Right-click on a gap to highlight legal move\n" bullet $t insert end "o Double-click on a gap to fill gap\n" bullet $t insert end "o Hold middle-button down to highlight all legal moves\n" bullet $t insert end "\n" $t insert end "Redeal\n" header $t insert end "If no move is possible, a redeal occurs automatically. " $t insert end "All cards which are not in their correct positions are " $t insert end "picked up, shuffled and redealt. Again the four aces are " $t insert end "removed creating four gaps.\n\n" $t insert end "See Also\n" header $t insert end "For more details about all the different variants in " $t insert end "family of solitaire games, see " $t insert end "http://web.inter.nl.net/hcc/Rudy.Muller/ranrules.html\n\n" $t insert end "Autoplay\n" header $t insert end "Key 1: make a single automatic move,\n" $t insert end "Key 2: autoplay until next redeal,\n" $t insert end "Key 3: autoplay until end of game.\n" $t insert end "Key 0: Stop autoplay.\n\n" $t config -state disabled focus -force .help.dismiss } ##+########################################################################## # # MakeCards -- makes a deck and cards # proc MakeCards {} { global S set S(deck) {} foreach suit {s d c h} { foreach pip {a 2 3 4 5 6 7 8 9 t j q k} { lappend S(deck) "$pip$suit" } } if {[info commands ::img::as] eq ""} { if {! [file exists cimages.tcl]} { wm withdraw . set emsg "Error: missing card images\n\n" tk_messageBox -icon error -message $emsg \ -title "$S(title) Error" exit } source cimages.tcl } set S(cw) [image width ::img::as] set S(ch) [image height ::img::as] set S(w) [expr {2*$S(lm) + 13*$S(cw) + 12*$S(padx)}] set S(h) [expr { $S(tm) + 4*$S(ch) + 3*$S(pady) + $S(bm)}] } ##+########################################################################## # # Shuffle -- Shuffles a list # proc Shuffle { l } { set len [llength $l] set len2 $len for {set i 0} {$i < $len-1} {incr i} { set n [expr {int($i + $len2 * rand())}] incr len2 -1 # Swap elements at i & n set temp [lindex $l $i] lset l $i [lindex $l $n] lset l $n $temp } return $l } ##+########################################################################## # # StartGame -- starts a new game # proc StartGame {{noShuffle 0}} { global S B STATS array unset STATS array set STATS {moves 0 redeals 0 good 0} array unset B array set B {0,13 X 1,13 X 2,13 X 3,13 X 4,0 X} ;# Sentinels .menu.game entryconfig "Undo" -state disabled if {! $noShuffle} { set S(cards) [Shuffle $S(deck)] } # Deal all the cards for {set idx 0} {$idx < 52} {incr idx} { set row [expr {$idx / 13}] set col [expr {$idx % 13}] set card [lindex $S(cards) $idx] if {[string match "a?" $card]} { ;# Ace, leave a gap set B($row,$col) "gap" lappend B(gaps) [list $row $col] .c itemconfig c$row,$col -image {} } else { set B($row,$col) $card set B(r,$card) [list $row $col] .c itemconfig c$row,$col -image ::img::$B($row,$col) } } Print "New Game" EndTurn } ##+########################################################################## # # CardPredecessor -- returns previous card in sequence # proc CardPredecessor {card} { set n [lsearch $::S(deck) $card] return [lindex $::S(deck) [expr {$n-1}]] } ##+########################################################################## # # CardSuccessor -- returns next card in sequence # proc CardSuccessor {card} { set n [lsearch $::S(deck) $card] return [lindex $::S(deck) [expr {$n+1}]] } ##+########################################################################## # # Redeal -- deals out all cards that are not in their proper position # proc Redeal {} { global S B incr ::STATS(redeals) set good [FindGood] set bad {} ;# All the cards to deal set cells $B(gaps) ;# Where to deal to foreach card $S(deck) { if {[lsearch $good $card] > -1} continue lappend bad $card catch {lappend cells $B(r,$card)} } set B(undo) {} .menu.game entryconfig "Undo" -state disabled while {1} { set B(gaps) {} set cards [Shuffle $bad] foreach card $cards cell $cells { foreach {row col} $cell break if {[string match "a?" $card]} { ;# Ace, leave a gap set B($row,$col) "gap" lappend B(gaps) [list $row $col] .c itemconfig c$row,$col -image {} } else { set B($row,$col) $card set B(r,$card) [list $row $col] .c itemconfig c$row,$col -image ::img::$B($row,$col) } } if {[CanMove]} break } EndTurn } ##+########################################################################## proc Undo {} { global B if {$B(undo) eq {}} return foreach {r c row col} [lindex $B(undo) end] break set B(undo) [lrange $B(undo) 0 end-1] MoveCardToGap $row $col $r $c incr ::STATS(moves) if {$B(undo) eq {}} { .menu.game entryconfig "Undo" -state disabled } } ##+########################################################################## # # Hint -- shows predecessor for a given card # proc Hint {row col} { global B set pred [CardPredecessor $B($row,$col)] if {! [info exists B(r,$pred)]} return foreach {r c} $B(r,$pred) break Flash good $r $c } ##+########################################################################## # # Hint2 -- shows which card goes into a gap # proc Hint2 {row col {how single}} { global B if {$B($row,$col) ne "gap"} return incr col -1 if {$col < 0} return set card $B($row,$col) if {$card eq "gap"} return if {[string match "k?" $card]} return set succ [CardSuccessor $card] if {! [info exists B(r,$succ)]} return if {$how eq "single"} { eval Flash good $B(r,$succ) } else { ;# Double click--do actual move .c delete flash eval Click $B(r,$succ) } } ##+########################################################################## # proc Hint3 {updown} { global B if {$updown eq "up"} { Flash all ;# Turn off highlighting return } set moves {} foreach pos $B(gaps) { foreach {row col} $pos break if {$col == 0} { ;# Empty in left column foreach card {2h 2c 2d 2s} { foreach {row col} $B(r,$card) break if {$col > 0} { lappend moves $row $col } } continue } incr col -1 if {$col < 0} continue set card $B($row,$col) if {$card eq "gap"} continue ;# Left is gap if {[string match "k?" $card]} continue ;# Left is a king set succ [CardSuccessor $card] if {! [info exists B(r,$succ)]} continue ;# Shouldn't happen eval lappend moves $B(r,$succ) } eval Flash all $moves } ################################################################ # Card images from Patience card game, see # http://uebb.cs.tu-berlin.de/~krischan/patience/patience-english.html # https://www.tcl-lang.org/starkits/patience.kit if { [catch { source card_img.tcl }]} { wm withdraw . tk_messageBox -icon error -title "$S(title) Error" \ -message "File with cardimages not found: card_img.tcl" exit } ####+####1####+####2####+####3####+####4####+####5####+####6####+####7####+ proc Print { x } { #: Debug-Log : Output to console #puts $x } proc Auto {Mode} { #: Start/Stop autoplay global S switch -- $Mode { "1" {AutoPlay1} "2" {AutoPlay2} "R" {set S(auto) 2; AutoPlay1} "End" {set S(auto) 3; AutoPlay1} "Off" {set S(auto) 0; foreach a [after info] {after cancel $a} } } Print "Auto=$S(auto)" } proc Find2 {} { #: Find rightmost 2-card ("Deuce") global B set col9 -1 foreach card { 2h 2d 2c 2s } { foreach {row col} $B(r,$card) break if {$col>$col9} { set c9 $card set col9 $col set row9 $row } } return [list $c9 $row9 $col9 ] } proc AutoPlay1 {} { #: Play one automatic move, use Strategy #1: # # Simple Strategy: # 1: See if a card can be played to its correct position # 1b: If first column has a gap, put Deuce from the rightmost column there # 2: Otherwise: Select the rightmost playable card # global B S STATS if { $STATS(good) >= 48 } return #set txt "$STATS(moves) - AutoPlay:" set good [FindGood] set col9 -1 foreach gap $B(gaps) { foreach {row col} $gap break if {$col == 0} { foreach {card r c} [Find2] break #append txt "\n $row $col ! $card $r $c" Print "Auto: $card $r $c --> $row $col" Click $r $c break } set left $B($row,[expr {$col-1}]) if { $left eq "gap" } { continue } if { [string match "k?" $left] } { continue } #append txt "\n $row $col" if {$col>$col9} { set col9 $col set row9 $row } if { [lsearch $good $left] > -1} { ;# append txt " G" Print "Auto: xx --> $row9 $col9" Hint2 $row $col double set col -1 break } } if {$col > 0} { #append txt "\n ==> $row9 $col9" Print "Auto: $row9 $col9" Hint2 $row9 $col9 double } #tk_messageBox -icon info -title "Auto1" -message $txt if {$S(auto) > 0} { after $S(delay) AutoPlay1 } } proc AutoPlay2 {} { #: Play one automatic move, use Strategy #2 # TODO } ####+####1####+####2####+####3####+####4####+####5####+####6####+####7####+ MakeCards DoDisplay StartGame return
tony - 2014-07-13 00:04:39
Creating a tree with all possible plays is a big task. I tried it once for a Montana game I wrote for the Mac. I got a better clue on the Internet which got me just creating random moves over and over until no more moves were possible. Repeating this process would often find a solution in under 10s.
I like the auto player heuristic, I might try it out on games I know to have a solution from my own code. Note that the code I wrote would allow moves of a 2 to a new row, not sure if this is really totally right.