Skip to content

Commit

Permalink
Klondikegame (#9)
Browse files Browse the repository at this point in the history
* GH-4: Implemented NewKlondikeGame, Deal, undoDeal, and test coverage.

* GH-4: Added test coverage for replenish when stock is replenished.

* GH-4: Added test coverage for KlondikeGame.adjustScore.

* GH-4: Added SelectFoundation, undoSelectionFoundation, and test coverage.

* GH-4: Implemented SelectWaste, undoSelectWaste, and test coverage

* GH-4: Implemented SelectTableau, undoSelectTableau, and test coverage.

* GH-4: Implemented IsSolvable and test coverage

* GH-4: Implemented IsSolved and test coverage

* GH-4: Implemented seekTableauToFoundation and test coverage.

Also added test coverage for Solve() for when the game is not yet solvable.

* GH-4: Added test coverage for undoSelectTableau.  Fixed undoSelectTableau bug.
  • Loading branch information
jamesboehmer authored May 27, 2020
1 parent 7f76f9d commit 58c2d8f
Show file tree
Hide file tree
Showing 2 changed files with 751 additions and 34 deletions.
262 changes: 228 additions & 34 deletions pkg/games/solitaire/klondike.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package solitaire

import (
"errors"
"github.com/jamesboehmer/gopatience/pkg/cards"
"github.com/jamesboehmer/gopatience/pkg/cards/pip"
"github.com/jamesboehmer/gopatience/pkg/cards/suit"
Expand All @@ -17,66 +18,277 @@ type KlondikeGame struct {
Tableau Tableau
}

const (
PointsWasteFoundation int = 10
PointsWasteTableau int = 5
PointsTableauFoundation int = 15
)

func (k *KlondikeGame) Deal() error {
replenished := false
if k.Stock.Remaining() == 0 {
if len(k.Waste) > 0 {
for _, card := range k.Waste {
k.Stock.Cards = append(k.Stock.Cards, *card.Conceal())
}
k.Waste = []cards.Card{}
replenished = true
} else {
return errors.New("no cards remaining")
}
}
k.Waste = append(k.Waste, *k.Stock.Deal().Reveal())
k.UndoStack = append(k.UndoStack, util.UndoAction{
Function: k.undoDeal,
Args: []interface{}{replenished},
})
return nil
}

func (k *KlondikeGame) undoDeal() error {
func (k *KlondikeGame) undoDeal(args ...interface{}) error {
replenished := args[0].(bool)
card := k.Waste[len(k.Waste)-1]
k.Waste = k.Waste[:len(k.Waste)-1]
k.Stock.Cards = append([]cards.Card{*card.Reveal()}, k.Stock.Cards...)
if replenished {
for card := range k.Stock.DealAll() {
k.Waste = append(k.Waste, *card.Reveal())
}
}
return nil
}

func (k *KlondikeGame) adjustScore(points int) {
k.Score += points
}

func (k *KlondikeGame) SelectFoundation(suit suit.Suit, pileNum ...int) error {
return nil
func (k *KlondikeGame) SelectFoundation(suit suit.Suit, tableauDestinations ...int) error {
card, err := k.Foundation.Get(suit)
if err != nil {
return err
}
if tableauDestinations == nil || len(tableauDestinations) == 0 {
for i := 0; i < len(k.Tableau.Piles); i++ {
tableauDestinations = append(tableauDestinations, i)
}
}
for _, pileNum := range tableauDestinations {
err := k.Tableau.Put([]*cards.Card{card}, pileNum)
if err == nil {
k.adjustScore(-PointsTableauFoundation)
k.UndoStack = append(k.UndoStack, util.UndoAction{
Function: k.undoSelectFoundation,
Args: nil,
})
return nil
}
}
k.Foundation.Undo()
return errors.New("no tableau fit")
}

func (k *KlondikeGame) undoSelectFoundation() error {
func (k *KlondikeGame) undoSelectFoundation(...interface{}) error {
k.adjustScore(PointsTableauFoundation)
k.Tableau.Undo()
k.Foundation.Undo()
return nil
}

func (k *KlondikeGame) SelectWaste(pileNum ...int) error {
return nil
func (k *KlondikeGame) SelectWaste(tableauDestinations ...int) error {
if len(k.Waste) == 0 {
return errors.New("no cards left in the waste pile")
}
topCard := k.Waste[len(k.Waste)-1]
k.Waste = k.Waste[:len(k.Waste)-1]

// try moving from the waste to the foundation if there was no tableau pile specified
if tableauDestinations == nil {
err := k.Foundation.Put(topCard)
if err == nil {
k.adjustScore(PointsWasteFoundation)
k.UndoStack = append(k.UndoStack, util.UndoAction{
Function: k.undoSelectWaste,
Args: []interface{}{true, topCard},
})
return nil
}
}
// if there was no fit, then try the tableau
if tableauDestinations == nil || len(tableauDestinations) == 0 {
for i := 0; i < len(k.Tableau.Piles); i++ {
tableauDestinations = append(tableauDestinations, i)
}
}

for _, pileNum := range tableauDestinations {
err := k.Tableau.Put([]*cards.Card{&topCard}, pileNum)
if err == nil {
k.adjustScore(PointsWasteTableau)
k.UndoStack = append(k.UndoStack, util.UndoAction{
Function: k.undoSelectWaste,
Args: []interface{}{false, topCard},
})
return nil
}
}
// if there was no fit, put the card back on the waste pile
k.Waste = append(k.Waste, topCard)
return errors.New("no tableau fit")
}

func (k *KlondikeGame) undoSelectWaste() error {
func (k *KlondikeGame) undoSelectWaste(args ...interface{}) error {
undoFoundation, card := args[0].(bool), args[1].(cards.Card)
if undoFoundation {
k.adjustScore(-PointsWasteFoundation)
k.Foundation.Undo()
} else {
k.adjustScore(-PointsWasteTableau)
k.Tableau.Undo()
}
k.Waste = append(k.Waste, card)
return nil
}

func (k *KlondikeGame) seekTableauToFoundation() error {
return nil
// Seek a tableau pile whose top card fits in the foundation
for pileNum, _ := range k.Tableau.Piles {
cards, err := k.Tableau.Get(pileNum, len(k.Tableau.Piles[pileNum])-1)
if err == nil { // we got a card from the tableau, now let's find a foundation fit
fErr := k.Foundation.Put(*cards[0])
if fErr == nil { // yay it was a tableau fit! push the undo stack and return
k.adjustScore(PointsTableauFoundation)
k.UndoStack = append(k.UndoStack, util.UndoAction{
Function: k.undoSeekTableauToFoundation,
Args: nil,
})
return nil
}
// it wasn't a fit in the tableau, so put the card back and move on to the next tableau pile
k.Tableau.Undo()
}
}
return errors.New("no tableau cards fit the foundation")
}

func (k *KlondikeGame) undoSeekTableauToFoundation() error {
func (k *KlondikeGame) undoSeekTableauToFoundation(...interface{}) error {
k.adjustScore(-PointsTableauFoundation)
k.Foundation.Undo()
k.Tableau.Undo()
return nil
}

func (k *KlondikeGame) SelectTableau(pileCardDestination ...int) error {
//TODO: pileNum int, cardNum int, destination int from pileCardDestination
return nil
func (k *KlondikeGame) SelectTableau(pileNum int, cardDestination ...int) error {
if pileNum < 0 || pileNum > len(k.Tableau.Piles)-1 {
return errors.New("invalid pileNum")
}
if len(k.Tableau.Piles[pileNum]) == 0 {
return errors.New("empty pile")
}
cardNum := -1
if len(cardDestination) > 0 {
cardNum = cardDestination[0]
}
if len(cardDestination) > 1 {
if cardDestination[1] == pileNum || cardDestination[1] < 0 { //|| cardDestination[1] > len(k.Tableau.Piles)-1{
return errors.New("invalid destination")
}
}
if cardNum < 0 {
// it's valid to ask for a negative index, just convert it to the positive offset
cardNum = len(k.Tableau.Piles[pileNum]) + cardNum
if cardNum < 0 {
return errors.New("invalid cardNum")
}
}
cards, err := k.Tableau.Get(pileNum, cardNum) //needs to be undone if we can't find a fit
if err != nil {
return err
}
// If there's only 1 card selected from the tableau, and no destination specified, try to fit it in the foundation
if len(cards) == 1 && len(cardDestination) < 2 {
err = k.Foundation.Put(*cards[0])
if err == nil {
k.adjustScore(PointsTableauFoundation)
k.UndoStack = append(k.UndoStack, util.UndoAction{
Function: k.undoSelectTableau,
Args: []interface{}{true},
})
return nil
}
// don't quit here just because we didn't find a foundation fit.
}

// if there was no fit, then try the tableau
var tableauDestinations []int
if len(cardDestination) > 1 {
tableauDestinations = append(tableauDestinations, cardDestination[1])
} else {
for i := 0; i < len(k.Tableau.Piles); i++ {
tableauDestinations = append(tableauDestinations, i)
}
}

for _, pileNum := range tableauDestinations {
err := k.Tableau.Put(cards, pileNum)
if err == nil {
k.adjustScore(PointsWasteTableau)
k.UndoStack = append(k.UndoStack, util.UndoAction{
Function: k.undoSelectTableau,
Args: []interface{}{false},
})
return nil
}
}
// We couldn't find a card in the tableau that fit in the foundation
// OR The chosen tableau card didn't fit in the foundation
// OR The chosen tableau card didn't fit anywhere in the tableau
// OR the chosen tableau card didn't fit in the chosen tableau pile
k.Tableau.Undo()
return errors.New("no fit for chosen card(s)")
}

func (k *KlondikeGame) undoSelectTableau() error {
func (k *KlondikeGame) undoSelectTableau(args ...interface{}) error {
undoFoundation := args[0].(bool)
if undoFoundation {
k.adjustScore(-PointsTableauFoundation)
k.Foundation.Undo() //undo put
} else {
k.Tableau.Undo() //undo put
k.Tableau.Undo() //undo get
}
return nil
}

func (k *KlondikeGame) IsSolvable() bool {
return false
if k.Stock.Remaining()+len(k.Waste) > 0 {
return false
}
for _, pile := range k.Tableau.Piles {
if len(pile) > 0 && !pile[0].Revealed {
return false
}
}
return true
}

func (k *KlondikeGame) IsSolved() bool {
return false
return k.Foundation.IsFull()
}

func (k *KlondikeGame) Solve() error {
if !k.IsSolvable() {
return errors.New("game is not solvable yet")
}

if !k.IsSolved() {
k.seekTableauToFoundation()
}
return nil
}

func NewKlondikeGame() *KlondikeGame {
game := new(KlondikeGame)
game.Stock = *cards.NewDeck(1, 0)
game.Stock = *cards.NewDeck(1, 0).Shuffle()
game.Foundation = *NewFoundation([]suit.Suit{suit.Hearts, suit.Diamonds, suit.Clubs, suit.Spades})
game.Tableau = *NewTableau(7, &game.Stock)
return game
Expand All @@ -97,21 +309,3 @@ var PipValue = map[pip.Pip]int{
pip.Queen: 12,
pip.King: 13,
}
//
//func PipValue(p pip.Pip) int {
// return map[pip.Pip]int{
// pip.Ace: 1,
// pip.Two: 2,
// pip.Three: 3,
// pip.Four: 4,
// pip.Five: 5,
// pip.Six: 6,
// pip.Seven: 7,
// pip.Eight: 8,
// pip.Nine: 9,
// pip.Ten: 10,
// pip.Jack: 11,
// pip.Queen: 12,
// pip.King: 13,
// }[p]
//}
Loading

0 comments on commit 58c2d8f

Please sign in to comment.