├── .gitignore ├── AUTHORS ├── FyneApp.toml ├── Icon.png ├── LICENSE ├── README.md ├── card.go ├── card_test.go ├── data.go ├── deck.go ├── deck_test.go ├── faces ├── 10C.svg ├── 10D.svg ├── 10H.svg ├── 10S.svg ├── 2C.svg ├── 2D.svg ├── 2H.svg ├── 2S.svg ├── 3C.svg ├── 3D.svg ├── 3H.svg ├── 3S.svg ├── 4C.svg ├── 4D.svg ├── 4H.svg ├── 4S.svg ├── 5C.svg ├── 5D.svg ├── 5H.svg ├── 5S.svg ├── 6C.svg ├── 6D.svg ├── 6H.svg ├── 6S.svg ├── 7C.svg ├── 7D.svg ├── 7H.svg ├── 7S.svg ├── 8C.svg ├── 8D.svg ├── 8H.svg ├── 8S.svg ├── 9C.svg ├── 9D.svg ├── 9H.svg ├── 9S.svg ├── AC.svg ├── AD.svg ├── AH.svg ├── AS.svg ├── JC.svg ├── JD.svg ├── JH.svg ├── JS.svg ├── KC.svg ├── KD.svg ├── KH.svg ├── KS.svg ├── QC.svg ├── QD.svg ├── QH.svg ├── QS.svg ├── about.txt ├── back.svg ├── bundled.go ├── faces.go └── space.svg ├── game.go ├── game_test.go ├── go.mod ├── go.sum ├── img ├── iphone.png ├── iphone_landscape.png └── solitaire.png ├── main.go ├── render.go ├── rules.go ├── rules_test.go ├── table.go └── theme.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | solitaire 5 | 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Andy Williams 2 | -------------------------------------------------------------------------------- /FyneApp.toml: -------------------------------------------------------------------------------- 1 | [Details] 2 | Name = "Solitaire" 3 | ID = "io.fyne.solitaire" 4 | Build = 7 5 | 6 | [Migrations] 7 | fyneDo = true -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/solitaire/6eda66959dd8fd8dd95fef8862db0221eff0f198/Icon.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Fyne.io developers (see AUTHORS) 2 | All rights reserved. 3 | 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Fyne.io nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Code Status 3 | Join us on Slack 4 |

5 | 6 | # Fyne Solitaire 7 | 8 | A simple solitaire application built using the Fyne toolkit. 9 | 10 | ![](img/solitaire.png) 11 | -------------------------------------------------------------------------------- /card.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "fyne.io/fyne/v2" 7 | 8 | "github.com/fyne-io/solitaire/faces" 9 | ) 10 | 11 | // Suit encodes one of the four possible suits for a playing card 12 | type Suit int 13 | 14 | // SuitColor represents the red/black of a suit 15 | type SuitColor int 16 | 17 | const ( 18 | // SuitClubs is the "Clubs" playing card suit 19 | SuitClubs Suit = iota 20 | // SuitDiamonds is the "Diamonds" playing card suit 21 | SuitDiamonds 22 | // SuitHearts is the "Hearts" playing card suit 23 | SuitHearts 24 | // SuitSpades is the "Spades" playing card suit 25 | SuitSpades 26 | 27 | // SuitColorBlack is returned from Color() if the suit is Clubs or Spades 28 | SuitColorBlack SuitColor = iota 29 | // SuitColorRed is returned from Color() if the suit is Diamonds or Hearts 30 | SuitColorRed 31 | ) 32 | 33 | const ( 34 | // ValueJack is a convenience for the card 1 higher than 10 35 | ValueJack = 11 36 | // ValueQueen is the value for a queen face card 37 | ValueQueen = 12 38 | // ValueKing is the value for a king face card 39 | ValueKing = 13 40 | ) 41 | 42 | // Card is a single playing card, it has a face value and a suit associated with it. 43 | type Card struct { 44 | Value int 45 | Suit Suit 46 | 47 | FaceUp bool 48 | } 49 | 50 | // Face returns a resource that can be used to render the associated card 51 | func (c *Card) Face() fyne.Resource { 52 | return faces.ForCard(c.Value, int(c.Suit)) 53 | } 54 | 55 | // TurnFaceUp sets the FaceUp field to true - so the card value can be seen 56 | func (c *Card) TurnFaceUp() { 57 | c.FaceUp = true 58 | } 59 | 60 | // TurnFaceDown sets the FaceUp field to false - so the card should be hidden 61 | func (c *Card) TurnFaceDown() { 62 | c.FaceUp = false 63 | } 64 | 65 | // Color returns the red or black color of the card suit 66 | func (c *Card) Color() SuitColor { 67 | if c.Suit == SuitClubs || c.Suit == SuitSpades { 68 | return SuitColorBlack 69 | } 70 | 71 | return SuitColorRed 72 | } 73 | 74 | // NewCard returns a new card instance with the specified suit and value (1 based for Ace, 2 is 2 and so on). 75 | func NewCard(value int, suit Suit) *Card { 76 | if value < 1 || value > 13 { 77 | log.Fatal("Invalid card face value") 78 | } 79 | 80 | return &Card{Value: value, Suit: suit} 81 | } 82 | -------------------------------------------------------------------------------- /card_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewCard(t *testing.T) { 10 | card := NewCard(3, SuitClubs) 11 | 12 | assert.False(t, card.FaceUp) 13 | } 14 | 15 | func TestCard_TurnFaceUp(t *testing.T) { 16 | card := NewCard(3, SuitClubs) 17 | card.TurnFaceUp() 18 | 19 | assert.True(t, card.FaceUp) 20 | } 21 | 22 | func TestCard_TurnFaceDown(t *testing.T) { 23 | card := NewCard(3, SuitClubs) 24 | card.TurnFaceUp() 25 | card.TurnFaceDown() 26 | 27 | assert.False(t, card.FaceUp) 28 | } 29 | -------------------------------------------------------------------------------- /deck.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // Deck is standard playing card collection, it contains up to 52 unique cards. 9 | type Deck struct { 10 | Cards []*Card 11 | } 12 | 13 | // Shuffle reorganises the cards in the deck to a random order 14 | func (d *Deck) Shuffle() { 15 | d.ShuffleFromSeed(time.Now().UnixNano()) 16 | } 17 | 18 | // ShuffleFromSeed reorganises the cards in the deck to a random order using 19 | // the specified seed for rand.Seed(). 20 | func (d *Deck) ShuffleFromSeed(seed int64) { 21 | rand.Seed(seed) 22 | for c := 0; c < len(d.Cards); c++ { 23 | swap := rand.Intn(len(d.Cards)) 24 | if swap != c { 25 | d.Cards[swap], d.Cards[c] = d.Cards[c], d.Cards[swap] 26 | } 27 | } 28 | } 29 | 30 | // Push adds the specified card to the top of the deck 31 | func (d *Deck) Push(card *Card) { 32 | d.Cards = append(d.Cards, card) 33 | } 34 | 35 | // Pop removes the top card from the deck and returns it 36 | func (d *Deck) Pop() *Card { 37 | card := d.Cards[0] 38 | d.Cards = d.Cards[1:] 39 | 40 | return card 41 | } 42 | 43 | // Remove takes the specified card out of the deck 44 | func (d *Deck) Remove(card *Card) { 45 | for i, c := range d.Cards { 46 | if cardEquals(c, card) { 47 | d.Cards = append(d.Cards[:i], d.Cards[i+1:]...) 48 | } 49 | } 50 | } 51 | 52 | // NewSortedDeck returns a standard deck in sorted order - starting with Ace of Clubs, ending with King of Spades. 53 | func NewSortedDeck() *Deck { 54 | deck := &Deck{} 55 | 56 | c := 0 57 | suit := SuitClubs 58 | for i := 0; i < 4; i++ { 59 | for value := 1; value <= ValueKing; value++ { 60 | deck.Cards = append(deck.Cards, NewCard(value, suit)) 61 | c++ 62 | } 63 | suit++ 64 | } 65 | 66 | return deck 67 | } 68 | 69 | // NewShuffledDeck returns a 52 card deck in random order. 70 | func NewShuffledDeck() *Deck { 71 | deck := NewSortedDeck() 72 | deck.Shuffle() 73 | 74 | return deck 75 | } 76 | 77 | // NewShuffledDeckFromSeed returns a 52 card deck in random order. 78 | // The randomness is seeded using the seed parameter. 79 | func NewShuffledDeckFromSeed(seed int64) *Deck { 80 | deck := NewSortedDeck() 81 | deck.ShuffleFromSeed(seed) 82 | 83 | return deck 84 | } 85 | -------------------------------------------------------------------------------- /deck_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func assertNotEqualCard(t *testing.T, value int, suit Suit, card *Card) { 10 | if value != card.Value || suit != card.Suit { 11 | return 12 | } 13 | 14 | t.Fail() 15 | } 16 | 17 | func TestNewDeck(t *testing.T) { 18 | deck := NewSortedDeck() 19 | 20 | assert.Equal(t, 52, len(deck.Cards)) 21 | } 22 | 23 | func TestNewSortedDeck(t *testing.T) { 24 | deck := NewSortedDeck() 25 | 26 | assert.Equal(t, 1, deck.Cards[0].Value) 27 | assert.Equal(t, SuitClubs, deck.Cards[0].Suit) 28 | 29 | assert.Equal(t, 13, deck.Cards[12].Value) 30 | assert.Equal(t, SuitClubs, deck.Cards[0].Suit) 31 | 32 | assert.Equal(t, 1, deck.Cards[13].Value) 33 | assert.Equal(t, SuitDiamonds, deck.Cards[13].Suit) 34 | 35 | assert.Equal(t, 5, deck.Cards[30].Value) 36 | assert.Equal(t, SuitHearts, deck.Cards[30].Suit) 37 | 38 | assert.Equal(t, 11, deck.Cards[49].Value) 39 | assert.Equal(t, SuitSpades, deck.Cards[49].Suit) 40 | } 41 | 42 | func TestNewShuffledDeck(t *testing.T) { 43 | deck := NewShuffledDeckFromSeed(1337) 44 | 45 | assertNotEqualCard(t, 1, SuitClubs, deck.Cards[0]) 46 | assertNotEqualCard(t, 13, SuitClubs, deck.Cards[12]) 47 | assertNotEqualCard(t, 1, SuitDiamonds, deck.Cards[13]) 48 | } 49 | 50 | func TestNewShuffledDeckFromSeed(t *testing.T) { 51 | deck1 := NewShuffledDeckFromSeed(1337) 52 | deck2 := NewShuffledDeckFromSeed(0xcafe) 53 | 54 | assertNotEqualCard(t, deck1.Cards[0].Value, deck1.Cards[0].Suit, deck2.Cards[0]) 55 | assertNotEqualCard(t, deck1.Cards[1].Value, deck1.Cards[1].Suit, deck2.Cards[1]) 56 | assertNotEqualCard(t, deck1.Cards[2].Value, deck1.Cards[2].Suit, deck2.Cards[2]) 57 | } 58 | 59 | func TestDeck_Push(t *testing.T) { 60 | deck := Deck{} 61 | 62 | assert.Equal(t, 0, len(deck.Cards)) 63 | card := NewCard(1, SuitDiamonds) 64 | deck.Push(card) 65 | 66 | assert.Equal(t, 1, len(deck.Cards)) 67 | assert.Equal(t, card, deck.Pop()) 68 | } 69 | 70 | func TestDeck_Pop(t *testing.T) { 71 | deck := NewSortedDeck() 72 | 73 | assert.Equal(t, 52, len(deck.Cards)) 74 | card := deck.Pop() 75 | 76 | assert.NotNil(t, card) 77 | assert.Equal(t, 51, len(deck.Cards)) 78 | } 79 | -------------------------------------------------------------------------------- /faces/2C.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 161 | 164 | 168 | 171 | 173 | 174 | 175 | 176 | 177 | 179 | 180 | 181 | 183 | 184 | 185 | 188 | 190 | 191 | 192 | 193 | 194 | 197 | 199 | 200 | 201 | 202 | 203 | 204 | 211 | 212 | 213 | 214 | 215 | 216 | 223 | 224 | 225 | 226 | 239 | -------------------------------------------------------------------------------- /faces/2S.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 92 | 95 | 99 | 102 | 104 | 105 | 106 | 107 | 108 | 110 | 111 | 112 | 114 | 115 | 116 | 119 | 121 | 122 | 123 | 124 | 125 | 128 | 130 | 131 | 132 | 133 | 134 | 135 | 142 | 143 | 144 | 152 | 160 | 161 | 169 | -------------------------------------------------------------------------------- /faces/3C.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 161 | 164 | 168 | 171 | 173 | 174 | 175 | 176 | 177 | 179 | 180 | 181 | 183 | 184 | 185 | 188 | 190 | 191 | 192 | 193 | 194 | 197 | 199 | 200 | 201 | 202 | 203 | 204 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 224 | 225 | 226 | 227 | 228 | 247 | -------------------------------------------------------------------------------- /faces/3S.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 92 | 95 | 99 | 102 | 104 | 105 | 106 | 107 | 108 | 110 | 111 | 112 | 114 | 115 | 116 | 119 | 121 | 122 | 123 | 124 | 125 | 128 | 130 | 131 | 132 | 133 | 134 | 135 | 142 | 143 | 144 | 152 | 160 | 161 | 176 | -------------------------------------------------------------------------------- /faces/4C.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 161 | 164 | 168 | 171 | 173 | 174 | 175 | 176 | 177 | 179 | 180 | 181 | 183 | 184 | 185 | 188 | 190 | 191 | 192 | 193 | 194 | 197 | 199 | 200 | 201 | 202 | 203 | 204 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 224 | 225 | 226 | 227 | 228 | 253 | -------------------------------------------------------------------------------- /faces/4S.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 92 | 95 | 99 | 102 | 104 | 105 | 106 | 107 | 108 | 110 | 111 | 112 | 114 | 115 | 116 | 119 | 121 | 122 | 123 | 124 | 125 | 128 | 130 | 131 | 132 | 133 | 134 | 135 | 142 | 143 | 144 | 145 | 153 | 161 | 162 | 163 | 185 | -------------------------------------------------------------------------------- /faces/5S.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 92 | 95 | 99 | 102 | 104 | 105 | 106 | 107 | 108 | 110 | 111 | 112 | 114 | 115 | 116 | 119 | 121 | 122 | 123 | 124 | 125 | 128 | 130 | 131 | 132 | 133 | 134 | 135 | 142 | 143 | 144 | 145 | 153 | 161 | 162 | 163 | 192 | -------------------------------------------------------------------------------- /faces/6S.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 92 | 95 | 99 | 102 | 104 | 105 | 106 | 107 | 108 | 110 | 111 | 112 | 114 | 115 | 116 | 119 | 121 | 122 | 123 | 124 | 125 | 128 | 130 | 131 | 132 | 133 | 134 | 135 | 142 | 143 | 144 | 145 | 153 | 161 | 162 | 163 | 199 | -------------------------------------------------------------------------------- /faces/7S.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 92 | 95 | 99 | 102 | 104 | 105 | 106 | 107 | 108 | 110 | 111 | 112 | 114 | 115 | 116 | 119 | 121 | 122 | 123 | 124 | 125 | 128 | 130 | 131 | 132 | 133 | 134 | 135 | 142 | 143 | 144 | 145 | 146 | 154 | 162 | 163 | 164 | 165 | 208 | -------------------------------------------------------------------------------- /faces/AC.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 131 | 134 | 138 | 141 | 143 | 144 | 145 | 146 | 147 | 149 | 150 | 151 | 153 | 154 | 155 | 158 | 160 | 161 | 162 | 163 | 164 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 181 | 182 | 183 | 202 | 203 | -------------------------------------------------------------------------------- /faces/AH.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 209 | 212 | 216 | 219 | 221 | 222 | 223 | 224 | 225 | 227 | 228 | 229 | 231 | 232 | 233 | 236 | 238 | 239 | 240 | 241 | 242 | 245 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 262 | 263 | 282 | 283 | -------------------------------------------------------------------------------- /faces/JC.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 131 | 134 | 138 | 141 | 143 | 144 | 145 | 146 | 147 | 149 | 150 | 151 | 153 | 154 | 155 | 158 | 160 | 161 | 162 | 163 | 164 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 181 | 182 | 183 | 184 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /faces/JH.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 209 | 212 | 216 | 219 | 221 | 222 | 223 | 224 | 225 | 227 | 228 | 229 | 231 | 232 | 233 | 236 | 238 | 239 | 240 | 241 | 242 | 245 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 262 | 263 | 264 | 265 | 266 | 285 | 286 | 287 | 288 | 289 | -------------------------------------------------------------------------------- /faces/JS.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 185 | 188 | 192 | 195 | 197 | 198 | 199 | 200 | 201 | 203 | 204 | 205 | 207 | 208 | 209 | 212 | 214 | 215 | 216 | 217 | 218 | 221 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 241 | 242 | 243 | 244 | 245 | 246 | 260 | -------------------------------------------------------------------------------- /faces/KC.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 131 | 134 | 138 | 141 | 143 | 144 | 145 | 146 | 147 | 149 | 150 | 151 | 153 | 154 | 155 | 158 | 160 | 161 | 162 | 163 | 164 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 181 | 182 | 183 | 184 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /faces/KH.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 209 | 212 | 216 | 219 | 221 | 222 | 223 | 224 | 225 | 227 | 228 | 229 | 231 | 232 | 233 | 236 | 238 | 239 | 240 | 241 | 242 | 245 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 262 | 263 | 264 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /faces/KS.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 185 | 188 | 192 | 195 | 197 | 198 | 199 | 200 | 201 | 203 | 204 | 205 | 207 | 208 | 209 | 212 | 214 | 215 | 216 | 217 | 218 | 221 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 241 | 242 | 256 | -------------------------------------------------------------------------------- /faces/QC.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 131 | 134 | 138 | 141 | 143 | 144 | 145 | 146 | 147 | 149 | 150 | 151 | 153 | 154 | 155 | 158 | 160 | 161 | 162 | 163 | 164 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 181 | 182 | 183 | 184 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /faces/QS.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | image/svg+xml 185 | 188 | 192 | 195 | 197 | 198 | 199 | 200 | 201 | 203 | 204 | 205 | 207 | 208 | 209 | 212 | 214 | 215 | 216 | 217 | 218 | 221 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 241 | 242 | 243 | 244 | 258 | -------------------------------------------------------------------------------- /faces/about.txt: -------------------------------------------------------------------------------- 1 | Vector Playing Cards by Byron Knoll, 2 | who kindly donated their work to the public domain. 3 | 4 | http://byronknoll.blogspot.com/2011/03/vector-playing-cards.html 5 | 6 | -------------------------------------------------------------------------------- /faces/faces.go: -------------------------------------------------------------------------------- 1 | //go:generate fyne bundle --package=faces --prefix=card -o bundled.go . 2 | 3 | package faces 4 | 5 | import "fyne.io/fyne/v2" 6 | 7 | // ForCard returns the face resource for the specified card value and suit. 8 | func ForCard(card, suit int) fyne.Resource { 9 | return faceResources[card-1+(suit*13)] 10 | } 11 | 12 | // ForBack returns a face resource for the back of a card 13 | func ForBack() fyne.Resource { 14 | return cardBackSvg 15 | } 16 | 17 | // ForSpace returns a special resource to use when a vacant spot should be indicated 18 | func ForSpace() fyne.Resource { 19 | return cardSpaceSvg 20 | } 21 | 22 | var faceResources = [52]fyne.Resource{ 23 | cardACSvg, 24 | card2CSvg, 25 | card3CSvg, 26 | card4CSvg, 27 | card5CSvg, 28 | card6CSvg, 29 | card7CSvg, 30 | card8CSvg, 31 | card9CSvg, 32 | card10CSvg, 33 | cardJCSvg, 34 | cardQCSvg, 35 | cardKCSvg, 36 | 37 | cardADSvg, 38 | card2DSvg, 39 | card3DSvg, 40 | card4DSvg, 41 | card5DSvg, 42 | card6DSvg, 43 | card7DSvg, 44 | card8DSvg, 45 | card9DSvg, 46 | card10DSvg, 47 | cardJDSvg, 48 | cardQDSvg, 49 | cardKDSvg, 50 | 51 | cardAHSvg, 52 | card2HSvg, 53 | card3HSvg, 54 | card4HSvg, 55 | card5HSvg, 56 | card6HSvg, 57 | card7HSvg, 58 | card8HSvg, 59 | card9HSvg, 60 | card10HSvg, 61 | cardJHSvg, 62 | cardQHSvg, 63 | cardKHSvg, 64 | 65 | cardASSvg, 66 | card2SSvg, 67 | card3SSvg, 68 | card4SSvg, 69 | card5SSvg, 70 | card6SSvg, 71 | card7SSvg, 72 | card8SSvg, 73 | card9SSvg, 74 | card10SSvg, 75 | cardJSSvg, 76 | cardQSSvg, 77 | cardKSSvg, 78 | } 79 | -------------------------------------------------------------------------------- /faces/space.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 88 | 91 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 101 | 103 | 104 | 105 | 108 | 110 | 111 | 112 | 113 | 114 | 117 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /game.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | // Stack represents a number of cards in a particular order 6 | type Stack struct { 7 | Cards []*Card 8 | } 9 | 10 | // Push adds a new card to the top of the stack 11 | func (s *Stack) Push(card *Card) { 12 | s.Cards = append(s.Cards, card) 13 | } 14 | 15 | // Top gets the top card of a stack, or nil if the stack is empty 16 | func (s *Stack) Top() *Card { 17 | if len(s.Cards) == 0 { 18 | return nil 19 | } 20 | 21 | return s.Cards[len(s.Cards)-1] 22 | } 23 | 24 | // Pop removes the top card of a stack, turning up any card immediately underneath and returning the removed card 25 | func (s *Stack) Pop() *Card { 26 | if len(s.Cards) == 0 { 27 | return nil 28 | } 29 | 30 | ret := s.Top() 31 | s.Cards = s.Cards[0 : len(s.Cards)-1] 32 | 33 | if len(s.Cards) > 0 { 34 | s.Cards[len(s.Cards)-1].TurnFaceUp() 35 | } 36 | return ret 37 | } 38 | 39 | // Contains will return true if the stack contains the specified card 40 | func (s *Stack) Contains(card *Card) bool { 41 | for _, c := range s.Cards { 42 | if cardEquals(c, card) { 43 | return true 44 | } 45 | } 46 | 47 | return false 48 | } 49 | 50 | // Game represents a full solitaire game, starting from a standard draw 51 | type Game struct { 52 | Hand *Deck 53 | 54 | Draw1, Draw2, Draw3 *Card 55 | Drawn *Deck 56 | 57 | Build1 *Stack 58 | Build2 *Stack 59 | Build3 *Stack 60 | Build4 *Stack 61 | 62 | Stack1 *Stack 63 | Stack2 *Stack 64 | Stack3 *Stack 65 | Stack4 *Stack 66 | Stack5 *Stack 67 | Stack6 *Stack 68 | Stack7 *Stack 69 | 70 | OnWin func() 71 | } 72 | 73 | func pushToStack(s *Stack, d *Deck, count int) { 74 | for i := 0; i < count; i++ { 75 | card := d.Pop() 76 | if i == count-1 { 77 | card.FaceUp = true 78 | } 79 | s.Push(card) 80 | } 81 | } 82 | 83 | func (g *Game) deal() { 84 | pushToStack(g.Stack1, g.Hand, 1) 85 | pushToStack(g.Stack2, g.Hand, 2) 86 | pushToStack(g.Stack3, g.Hand, 3) 87 | pushToStack(g.Stack4, g.Hand, 4) 88 | pushToStack(g.Stack5, g.Hand, 5) 89 | pushToStack(g.Stack6, g.Hand, 6) 90 | pushToStack(g.Stack7, g.Hand, 7) 91 | } 92 | 93 | // AutoBuild attempts to place the passed card onto one of the build stacks 94 | func (g *Game) AutoBuild(c *Card) { 95 | for _, b := range []*Stack{g.Build1, g.Build2, g.Build3, g.Build4} { 96 | if !g.ruleCanMoveToBuild(b, c) { 97 | continue 98 | } 99 | 100 | g.MoveCardToBuild(b, c) 101 | break 102 | } 103 | } 104 | 105 | // ResetDraw resets the draw pile to be completely available (no cards drawn) 106 | func (g *Game) ResetDraw() { 107 | for ; len(g.Hand.Cards) > 0; g.DrawThree() { 108 | } 109 | 110 | // Reset the draw pile 111 | g.DrawThree() 112 | } 113 | 114 | func (g *Game) drawCard() *Card { 115 | if len(g.Hand.Cards) == 0 { 116 | return nil 117 | } 118 | 119 | popped := g.Hand.Pop() 120 | popped.FaceUp = true 121 | g.Drawn.Push(popped) 122 | return popped 123 | } 124 | 125 | // DrawThree draws three cards from the deck and adds them to the draw pile(s). 126 | // If there are no cards available to be drawn it will cycle back to the beginning and draw the first three. 127 | func (g *Game) DrawThree() { 128 | if len(g.Hand.Cards) == 0 { 129 | g.Draw1 = nil 130 | g.Draw2 = nil 131 | g.Draw3 = nil 132 | 133 | g.Hand = g.Drawn 134 | for _, card := range g.Hand.Cards { 135 | card.TurnFaceDown() 136 | } 137 | g.Drawn = &Deck{} 138 | return 139 | } 140 | 141 | g.Draw1 = g.drawCard() 142 | g.Draw2 = g.drawCard() 143 | g.Draw3 = g.drawCard() 144 | } 145 | 146 | // MoveCardToBuild attempts to move the currently selected card to a build stack. 147 | // If the move is not possible it will return. 148 | func (g *Game) MoveCardToBuild(build *Stack, card *Card) { 149 | if !g.ruleCanMoveToBuild(build, card) { 150 | return 151 | } 152 | 153 | g.removeCard(card) 154 | build.Push(card) 155 | 156 | if len(g.Build1.Cards) == 13 && len(g.Build2.Cards) == 13 && 157 | len(g.Build3.Cards) == 13 && len(g.Build4.Cards) == 13 { 158 | 159 | if g.OnWin != nil { 160 | g.OnWin() 161 | } 162 | } 163 | } 164 | 165 | // MoveCardToStack attempts to move the currently selected card to a table stack. 166 | // If the move is not possible it will return. 167 | func (g *Game) MoveCardToStack(stack *Stack, card *Card) { 168 | if !g.ruleCanMoveToStack(stack, card) { 169 | return 170 | } 171 | 172 | oldStack := g.stackForCard(card) 173 | if oldStack == nil { 174 | g.removeCard(card) 175 | stack.Push(card) 176 | return 177 | } 178 | 179 | found := false 180 | for _, c := range oldStack.Cards { 181 | if cardEquals(c, card) { 182 | found = true 183 | } 184 | 185 | if found { 186 | // noinspection GoDeferInLoop 187 | defer oldStack.Pop() 188 | stack.Push(c) 189 | } 190 | } 191 | } 192 | 193 | func (g *Game) stackForCard(card *Card) *Stack { 194 | if g.Stack1.Contains(card) { 195 | return g.Stack1 196 | } 197 | if g.Stack2.Contains(card) { 198 | return g.Stack2 199 | } 200 | if g.Stack3.Contains(card) { 201 | return g.Stack3 202 | } 203 | if g.Stack4.Contains(card) { 204 | return g.Stack4 205 | } 206 | if g.Stack5.Contains(card) { 207 | return g.Stack5 208 | } 209 | if g.Stack6.Contains(card) { 210 | return g.Stack6 211 | } 212 | if g.Stack7.Contains(card) { 213 | return g.Stack7 214 | } 215 | 216 | return nil 217 | } 218 | 219 | func (g *Game) removeCard(card *Card) { 220 | if cardEquals(card, g.Draw3) { 221 | g.Drawn.Remove(card) 222 | g.Draw3 = nil 223 | } else if cardEquals(card, g.Draw2) { 224 | g.Drawn.Remove(card) 225 | g.Draw2 = nil 226 | } else if cardEquals(card, g.Draw1) { 227 | // TODO what if it's empty - the previous draw? 228 | g.Drawn.Remove(card) 229 | g.Draw1 = nil 230 | 231 | } else if cardEquals(card, g.Build1.Top()) { 232 | g.Build1.Pop() 233 | } else if cardEquals(card, g.Build2.Top()) { 234 | g.Build2.Pop() 235 | } else if cardEquals(card, g.Build3.Top()) { 236 | g.Build3.Pop() 237 | } else if cardEquals(card, g.Build4.Top()) { 238 | g.Build4.Pop() 239 | 240 | } else if cardEquals(card, g.Stack1.Top()) { 241 | g.Stack1.Pop() 242 | } else if cardEquals(card, g.Stack2.Top()) { 243 | g.Stack2.Pop() 244 | } else if cardEquals(card, g.Stack3.Top()) { 245 | g.Stack3.Pop() 246 | } else if cardEquals(card, g.Stack4.Top()) { 247 | g.Stack4.Pop() 248 | } else if cardEquals(card, g.Stack5.Top()) { 249 | g.Stack5.Pop() 250 | } else if cardEquals(card, g.Stack6.Top()) { 251 | g.Stack6.Pop() 252 | } else if cardEquals(card, g.Stack7.Top()) { 253 | g.Stack7.Pop() 254 | } 255 | } 256 | 257 | // NewGame starts a new solitaire game and draws to the standard configuration. 258 | func NewGame() *Game { 259 | return NewGameFromSeed(time.Now().UnixNano()) 260 | } 261 | 262 | // NewGameFromSeed starts a new solitaire game and draws to the standard configuration. 263 | // The randomness of the desk is seeded using the specified value. 264 | func NewGameFromSeed(seed int64) *Game { 265 | game := &Game{} 266 | game.Hand = NewShuffledDeckFromSeed(seed) 267 | 268 | game.Drawn = &Deck{} 269 | 270 | game.Stack1 = &Stack{} 271 | game.Stack2 = &Stack{} 272 | game.Stack3 = &Stack{} 273 | game.Stack4 = &Stack{} 274 | game.Stack5 = &Stack{} 275 | game.Stack6 = &Stack{} 276 | game.Stack7 = &Stack{} 277 | 278 | game.Build1 = &Stack{} 279 | game.Build2 = &Stack{} 280 | game.Build3 = &Stack{} 281 | game.Build4 = &Stack{} 282 | 283 | game.deal() 284 | return game 285 | } 286 | -------------------------------------------------------------------------------- /game_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func newTestGame() *Game { 10 | return NewGameFromSeed(0xace) 11 | } 12 | 13 | func TestStack_Push(t *testing.T) { 14 | stack := &Stack{} 15 | card := NewCard(1, SuitSpades) 16 | 17 | assert.Equal(t, 0, len(stack.Cards)) 18 | stack.Push(card) 19 | assert.Equal(t, 1, len(stack.Cards)) 20 | } 21 | 22 | func TestGame_Deal(t *testing.T) { 23 | game := newTestGame() 24 | 25 | assert.Equal(t, 1, len(game.Stack1.Cards)) 26 | assert.Equal(t, 2, len(game.Stack2.Cards)) 27 | assert.Equal(t, 7, len(game.Stack7.Cards)) 28 | assert.Equal(t, 24, len(game.Hand.Cards)) 29 | } 30 | 31 | func TestGame_Deal_FaceUp(t *testing.T) { 32 | game := newTestGame() 33 | 34 | assert.Equal(t, true, game.Stack1.Cards[0].FaceUp) 35 | 36 | assert.Equal(t, false, game.Stack2.Cards[0].FaceUp) 37 | assert.Equal(t, true, game.Stack2.Cards[1].FaceUp) 38 | } 39 | 40 | func TestGame_Draw(t *testing.T) { 41 | game := newTestGame() 42 | 43 | assert.Equal(t, 24, len(game.Hand.Cards)) 44 | 45 | game.DrawThree() 46 | assert.Equal(t, 21, len(game.Hand.Cards)) 47 | assert.NotNil(t, game.Draw1) 48 | assert.True(t, game.Draw1.FaceUp) 49 | assert.NotNil(t, game.Draw2) 50 | assert.True(t, game.Draw2.FaceUp) 51 | assert.NotNil(t, game.Draw3) 52 | assert.True(t, game.Draw3.FaceUp) 53 | } 54 | 55 | func TestGameDrawEnd(t *testing.T) { 56 | game := newTestGame() 57 | 58 | assert.Equal(t, 24, len(game.Hand.Cards)) 59 | 60 | game.DrawThree() 61 | game.DrawThree() 62 | game.DrawThree() 63 | game.DrawThree() 64 | game.DrawThree() 65 | game.DrawThree() 66 | game.DrawThree() 67 | game.DrawThree() 68 | assert.Equal(t, 0, len(game.Hand.Cards)) 69 | } 70 | 71 | func TestGame_DrawCycles(t *testing.T) { 72 | game := newTestGame() 73 | 74 | assert.Equal(t, 24, len(game.Hand.Cards)) 75 | 76 | game.DrawThree() 77 | game.DrawThree() 78 | game.DrawThree() 79 | game.DrawThree() 80 | game.DrawThree() 81 | game.DrawThree() 82 | game.DrawThree() 83 | game.DrawThree() 84 | 85 | // This is the extra one... 86 | game.DrawThree() 87 | assert.Equal(t, 24, len(game.Hand.Cards)) 88 | } 89 | 90 | func TestGame_ResetDraw(t *testing.T) { 91 | game := newTestGame() 92 | 93 | assert.Equal(t, 24, len(game.Hand.Cards)) 94 | 95 | game.DrawThree() 96 | game.ResetDraw() 97 | assert.Equal(t, 24, len(game.Hand.Cards)) 98 | } 99 | 100 | func TestGame_DrawSymmetric(t *testing.T) { 101 | game := newTestGame() 102 | 103 | assert.Equal(t, 24, len(game.Hand.Cards)) 104 | 105 | game.DrawThree() 106 | first := game.Draw1 107 | game.ResetDraw() 108 | 109 | // first draw again 110 | game.DrawThree() 111 | assert.Equal(t, first, game.Draw1) 112 | } 113 | 114 | func TestGame_MoveCardToBuildFromHand(t *testing.T) { 115 | game := newTestGame() 116 | game.DrawThree() 117 | game.Draw3.Value = 1 118 | 119 | game.MoveCardToBuild(game.Build1, game.Draw3) 120 | assert.Equal(t, 1, len(game.Build1.Cards)) 121 | assert.Nil(t, game.Draw3) 122 | } 123 | 124 | func TestGame_MoveCardToBuildFromStack2(t *testing.T) { 125 | game := newTestGame() 126 | game.Stack2.Cards[1].Value = 1 127 | 128 | assert.Equal(t, 2, len(game.Stack2.Cards)) 129 | assert.False(t, game.Stack2.Cards[0].FaceUp) 130 | 131 | game.MoveCardToBuild(game.Build2, game.Stack2.Cards[1]) 132 | assert.Equal(t, 1, len(game.Build2.Cards)) 133 | assert.Equal(t, 1, len(game.Stack2.Cards)) 134 | assert.True(t, game.Stack2.Cards[0].FaceUp) 135 | } 136 | 137 | func TestGame_MoveCardToStack(t *testing.T) { 138 | game := newTestGame() 139 | 140 | game.Stack1.Cards[0].Value = 3 141 | game.Stack1.Cards[0].Suit = SuitClubs 142 | game.Stack2.Cards[1].Value = 2 143 | game.Stack2.Cards[1].Suit = SuitDiamonds 144 | assert.False(t, game.Stack2.Cards[0].FaceUp) 145 | 146 | game.MoveCardToStack(game.Stack1, game.Stack2.Cards[1]) 147 | assert.Equal(t, 2, len(game.Stack1.Cards)) 148 | assert.Equal(t, 1, len(game.Stack2.Cards)) 149 | assert.True(t, game.Stack2.Cards[0].FaceUp) 150 | } 151 | 152 | func TestGame_MoveCardToStack_Empty(t *testing.T) { 153 | game := newTestGame() 154 | 155 | game.Stack1.Cards = []*Card{} 156 | game.Stack2.Cards[1].Value = ValueKing 157 | game.Stack2.Cards[1].Suit = SuitDiamonds 158 | 159 | game.MoveCardToStack(game.Stack1, game.Stack2.Cards[1]) 160 | assert.Equal(t, 1, len(game.Stack1.Cards)) 161 | assert.Equal(t, 1, len(game.Stack2.Cards)) 162 | assert.True(t, game.Stack2.Cards[0].FaceUp) 163 | } 164 | 165 | func TestGame_MoveCardToStack_EmptyEmpty(t *testing.T) { 166 | game := newTestGame() 167 | 168 | game.Stack3.Cards = []*Card{} 169 | king := NewCard(ValueKing, SuitDiamonds) 170 | game.Stack2.Cards = []*Card{king} 171 | 172 | game.MoveCardToStack(game.Stack3, game.Stack2.Cards[0]) 173 | assert.Equal(t, 1, len(game.Stack3.Cards)) 174 | assert.Equal(t, 0, len(game.Stack2.Cards)) 175 | } 176 | 177 | func TestGame_MoveCardToStack_Stack(t *testing.T) { 178 | game := newTestGame() 179 | 180 | game.Stack1.Cards[0].Value = 7 181 | game.Stack1.Cards[0].Suit = SuitClubs 182 | game.Stack2.Cards[0].Value = 6 183 | game.Stack2.Cards[0].Suit = SuitDiamonds 184 | game.Stack2.Cards[1].Value = 5 185 | game.Stack2.Cards[1].Suit = SuitSpades 186 | 187 | game.MoveCardToStack(game.Stack1, game.Stack2.Cards[0]) 188 | assert.Equal(t, 3, len(game.Stack1.Cards)) 189 | assert.Equal(t, 0, len(game.Stack2.Cards)) 190 | } 191 | 192 | func TestGame_MoveCardToStack_KingStack(t *testing.T) { 193 | game := newTestGame() 194 | 195 | game.Stack1.Cards = []*Card{} 196 | game.Stack3.Cards[1].Value = ValueKing 197 | game.Stack3.Cards[1].Suit = SuitDiamonds 198 | game.Stack3.Cards[2].Value = ValueQueen 199 | game.Stack3.Cards[2].Suit = SuitSpades 200 | 201 | game.MoveCardToStack(game.Stack1, game.Stack3.Cards[1]) 202 | assert.Equal(t, 2, len(game.Stack1.Cards)) 203 | assert.Equal(t, 1, len(game.Stack3.Cards)) 204 | } 205 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fyne-io/solitaire 2 | 3 | go 1.17 4 | 5 | require ( 6 | fyne.io/fyne/v2 v2.6.1-0.20250421105627-dc6ccce03e23 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | fyne.io/systray v1.11.0 // indirect 12 | github.com/BurntSushi/toml v1.4.0 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/fredbi/uri v1.1.0 // indirect 15 | github.com/fsnotify/fsnotify v1.7.0 // indirect 16 | github.com/fyne-io/gl-js v0.1.0 // indirect 17 | github.com/fyne-io/glfw-js v0.2.0 // indirect 18 | github.com/fyne-io/image v0.1.1 // indirect 19 | github.com/fyne-io/oksvg v0.1.0 // indirect 20 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect 21 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect 22 | github.com/go-text/render v0.2.0 // indirect 23 | github.com/go-text/typesetting v0.2.1 // indirect 24 | github.com/godbus/dbus/v5 v5.1.0 // indirect 25 | github.com/hack-pad/go-indexeddb v0.3.2 // indirect 26 | github.com/hack-pad/safejs v0.1.0 // indirect 27 | github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect 28 | github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect 29 | github.com/kr/text v0.2.0 // indirect 30 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 31 | github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | github.com/rymdport/portal v0.4.1 // indirect 34 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect 35 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect 36 | github.com/yuin/goldmark v1.7.8 // indirect 37 | golang.org/x/image v0.24.0 // indirect 38 | golang.org/x/net v0.35.0 // indirect 39 | golang.org/x/sys v0.30.0 // indirect 40 | golang.org/x/text v0.22.0 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /img/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/solitaire/6eda66959dd8fd8dd95fef8862db0221eff0f198/img/iphone.png -------------------------------------------------------------------------------- /img/iphone_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/solitaire/6eda66959dd8fd8dd95fef8862db0221eff0f198/img/iphone_landscape.png -------------------------------------------------------------------------------- /img/solitaire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/solitaire/6eda66959dd8fd8dd95fef8862db0221eff0f198/img/solitaire.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:generate fyne bundle --package=main -o data.go Icon.png 2 | 3 | // Package main launches the solitaire app 4 | package main 5 | 6 | import ( 7 | "fyne.io/fyne/v2" 8 | "fyne.io/fyne/v2/app" 9 | "fyne.io/fyne/v2/container" 10 | "fyne.io/fyne/v2/dialog" 11 | "fyne.io/fyne/v2/theme" 12 | "fyne.io/fyne/v2/widget" 13 | ) 14 | 15 | // show creates a new game and loads a table rendered in a new window. 16 | func show(app fyne.App) { 17 | game := NewGame() 18 | table := NewTable(game) 19 | 20 | w := app.NewWindow("Solitaire") 21 | shuffle := widget.NewToolbarAction(theme.ViewRefreshIcon(), func() { 22 | table.game.Hand.Shuffle() 23 | }) 24 | table.shuffle = shuffle 25 | bar := widget.NewToolbar( 26 | widget.NewToolbarAction(theme.ContentAddIcon(), func() { 27 | checkRestart(table, w) 28 | }), 29 | shuffle) 30 | w.SetContent(container.NewBorder(bar, nil, nil, nil, table)) 31 | w.Resize(fyne.NewSize(minWidth, minHeight)) 32 | 33 | game.OnWin = func() { 34 | table.finishAnimation() 35 | d := dialog.NewInformation("You Win!", "Congratulations", w) 36 | d.SetOnClosed(table.Restart) 37 | d.Show() 38 | } 39 | w.Show() 40 | } 41 | 42 | func checkRestart(t *Table, w fyne.Window) { 43 | dialog.ShowConfirm("New Game", "Start a new game?", func(ok bool) { 44 | if !ok { 45 | return 46 | } 47 | 48 | t.Restart() 49 | }, w) 50 | } 51 | 52 | func shuffleDraw(t *Table) { 53 | 54 | } 55 | 56 | func main() { 57 | a := app.New() 58 | a.SetIcon(resourceIconPng) 59 | a.Settings().SetTheme(newGameTheme()) 60 | 61 | show(a) 62 | a.Run() 63 | } 64 | -------------------------------------------------------------------------------- /rules.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func (g *Game) ruleCanMoveToBuild(build *Stack, card *Card) bool { 4 | if len(build.Cards) == 0 { 5 | return card.Value == 1 6 | } 7 | 8 | top := build.Top() 9 | return card.Suit == top.Suit && card.Value == top.Value+1 10 | } 11 | 12 | func (g *Game) ruleCanMoveToStack(stack *Stack, card *Card) bool { 13 | if len(stack.Cards) == 0 { 14 | return card.Value == ValueKing 15 | } 16 | 17 | top := stack.Top() 18 | if top.Color() == card.Color() { 19 | return false 20 | } 21 | return card.Value == top.Value-1 22 | } 23 | -------------------------------------------------------------------------------- /rules_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRuleCanMoveToBuild_Empty(t *testing.T) { 10 | g := NewGame() 11 | card := NewCard(1, SuitClubs) 12 | 13 | assert.True(t, g.ruleCanMoveToBuild(g.Build1, card)) 14 | card.Value = 3 15 | assert.False(t, g.ruleCanMoveToBuild(g.Build1, card)) 16 | } 17 | 18 | func TestRuleCanMoveToBuild_Over(t *testing.T) { 19 | g := NewGame() 20 | card := NewCard(1, SuitClubs) 21 | g.Build1.Push(card) 22 | 23 | card = NewCard(2, SuitClubs) 24 | assert.True(t, g.ruleCanMoveToBuild(g.Build1, card)) 25 | card.Suit = SuitDiamonds 26 | assert.False(t, g.ruleCanMoveToBuild(g.Build1, card)) 27 | } 28 | 29 | func TestRuleCanMoveToStack_Empty(t *testing.T) { 30 | g := NewGame() 31 | card := NewCard(ValueKing, SuitClubs) 32 | g.Stack1.Cards = []*Card{} 33 | 34 | assert.True(t, g.ruleCanMoveToStack(g.Stack1, card)) 35 | card.Value = 3 36 | assert.False(t, g.ruleCanMoveToStack(g.Build1, card)) 37 | } 38 | 39 | func TestRuleCanMoveToStack_Over(t *testing.T) { 40 | g := NewGame() 41 | card := NewCard(10, SuitClubs) 42 | g.Stack1.Cards = []*Card{card} 43 | 44 | card = NewCard(9, SuitHearts) 45 | assert.True(t, g.ruleCanMoveToStack(g.Stack1, card)) 46 | card.Value = 3 47 | assert.False(t, g.ruleCanMoveToStack(g.Stack1, card)) 48 | card.Value = 9 49 | card.Suit = SuitSpades 50 | assert.False(t, g.ruleCanMoveToStack(g.Stack1, card)) 51 | card.Suit = SuitDiamonds 52 | assert.True(t, g.ruleCanMoveToStack(g.Stack1, card)) 53 | } 54 | -------------------------------------------------------------------------------- /table.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "time" 7 | 8 | "fyne.io/fyne/v2" 9 | "fyne.io/fyne/v2/canvas" 10 | "fyne.io/fyne/v2/container" 11 | "fyne.io/fyne/v2/test" 12 | "fyne.io/fyne/v2/theme" 13 | "fyne.io/fyne/v2/widget" 14 | 15 | "github.com/fyne-io/solitaire/faces" 16 | ) 17 | 18 | // Table represents the rendering of a game in progress 19 | type Table struct { 20 | widget.BaseWidget 21 | 22 | game *Game 23 | selected *Card 24 | 25 | float []*canvas.Image 26 | floatSource []*canvas.Image 27 | floatPos fyne.Position 28 | 29 | shuffle *widget.ToolbarAction 30 | 31 | findCard func(fyne.Position) ([]*Card, []*canvas.Image, bool) 32 | stackPos func(int) fyne.Position 33 | } 34 | 35 | // CreateRenderer gets the widget renderer for this table - internal use only 36 | func (t *Table) CreateRenderer() fyne.WidgetRenderer { 37 | return newTableRender(t) 38 | } 39 | 40 | // find card from an image, easier than keeping them in sync 41 | func (t *Table) cardForPos(pos *canvas.Image) *Card { 42 | deck := NewSortedDeck() 43 | 44 | for i, face := range deck.Cards { 45 | if face.Face() == pos.Resource { 46 | card := NewCard((i%13)+1, Suit(math.Floor(float64(i)/13))) 47 | card.FaceUp = true // we know this as we checked the face 48 | return card 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func cardEquals(card1, card2 *Card) bool { 56 | if card1 == nil || card2 == nil { 57 | return card1 == nil && card2 == nil 58 | } 59 | 60 | return card1.Value == card2.Value && card1.Suit == card2.Suit 61 | } 62 | 63 | func (t *Table) cardTapped(cardPos *canvas.Image, pos fyne.Position, move func()) bool { 64 | if !withinCardBounds(cardPos, pos) { 65 | return false 66 | } 67 | 68 | card := t.cardForPos(cardPos) 69 | if cardPos.Resource != faces.ForSpace() && (card == nil || !card.FaceUp) { 70 | t.selected = nil 71 | t.Refresh() 72 | 73 | return true 74 | } 75 | 76 | if t.selected == nil { 77 | t.selected = card 78 | } else { 79 | if cardEquals(t.selected, card) { 80 | t.game.AutoBuild(card) 81 | } else { 82 | if move != nil { 83 | move() 84 | } 85 | } 86 | 87 | t.selected = nil 88 | } 89 | 90 | t.Refresh() 91 | return true 92 | } 93 | 94 | func (t *Table) checkStackTapped(render *stackRender, stack *Stack, pos fyne.Position) bool { 95 | for i := len(stack.Cards) - 1; i >= 0; i-- { 96 | // card := stack.Cards[i] 97 | 98 | if t.cardTapped(render.cards[i], pos, func() { 99 | t.game.MoveCardToStack(stack, t.selected) 100 | }) { 101 | return true 102 | } 103 | } 104 | 105 | return t.cardTapped(render.cards[0], pos, func() { 106 | t.game.MoveCardToStack(stack, t.selected) 107 | }) 108 | } 109 | 110 | func (t *Table) Restart() { 111 | oldWin := t.game.OnWin 112 | t.game = NewGame() 113 | t.game.OnWin = oldWin 114 | t.shuffle.Enable() 115 | 116 | test.WidgetRenderer(t).(*tableRender).game = t.game 117 | t.Refresh() 118 | } 119 | 120 | // Dragged is called when the user drags on the table widget 121 | func (t *Table) Dragged(event *fyne.DragEvent) { 122 | t.floatPos = event.Position 123 | if !t.float[0].Hidden { // existing drag 124 | 125 | for i := 0; i < len(t.float); i++ { 126 | if t.float[i] != nil && !t.float[i].Hidden { 127 | t.float[i].Move(t.float[i].Position().Add(event.Dragged)) 128 | } 129 | } 130 | return 131 | } 132 | 133 | if t.selected != nil { 134 | return 135 | } 136 | 137 | card, source, last := t.findCard(event.Position) 138 | if card == nil { 139 | return 140 | } 141 | if !card[0].FaceUp { // only drag visible cards 142 | return 143 | } 144 | 145 | t.selected = card[0] 146 | 147 | for i := 0; i < len(source); i++ { 148 | t.floatSource[i] = source[i] 149 | t.float[i].Resource = source[i].Resource 150 | if last && i == 0 { 151 | source[i].Resource = faces.ForSpace() 152 | } else { 153 | source[i].Resource = nil 154 | } 155 | source[i].Image = nil 156 | source[i].Refresh() 157 | 158 | t.float[i].Hidden = false 159 | t.float[i].Image = nil 160 | t.float[i].Refresh() 161 | updateCardPosition(t.float[i], source[i].Position().X, source[i].Position().Y) 162 | } 163 | } 164 | 165 | // DragEnd is called when the user stops dragging on the table widget 166 | func (t *Table) DragEnd() { 167 | for i := 0; i < ValueKing; i++ { 168 | t.float[i].Hide() 169 | } 170 | 171 | if t.dropCard(t.floatPos) { 172 | return 173 | } 174 | 175 | for i := 0; i < ValueKing; i++ { 176 | if t.floatSource[i] != nil { 177 | t.floatSource[i].Resource = t.float[i].Resource 178 | t.floatSource[i].Refresh() 179 | } 180 | } 181 | } 182 | 183 | // Tapped is called when the user taps the table widget 184 | func (t *Table) Tapped(event *fyne.PointEvent) { 185 | render := test.WidgetRenderer(t).(*tableRender) 186 | 187 | if withinCardBounds(render.deck, event.Position) { 188 | t.selected = nil 189 | t.game.DrawThree() 190 | 191 | if len(t.game.Drawn.Cards) == 0 { 192 | t.shuffle.Enable() 193 | } else { 194 | t.shuffle.Disable() 195 | } 196 | 197 | render.Refresh() 198 | return 199 | } 200 | 201 | t.dropCard(event.Position) 202 | } 203 | 204 | func (t *Table) dropCard(pos fyne.Position) bool { 205 | render := test.WidgetRenderer(t).(*tableRender) 206 | 207 | if t.game.Draw3 != nil { 208 | if t.cardTapped(render.pile3, pos, nil) { 209 | return true 210 | } 211 | } else if t.game.Draw2 != nil { 212 | if t.cardTapped(render.pile2, pos, nil) { 213 | return true 214 | } 215 | } else if t.game.Draw1 != nil { 216 | if t.cardTapped(render.pile1, pos, nil) { 217 | return true 218 | } 219 | } 220 | 221 | if t.cardTapped(render.build1, pos, func() { 222 | t.game.MoveCardToBuild(t.game.Build1, t.selected) 223 | }) { 224 | return true 225 | } else if t.cardTapped(render.build2, pos, func() { 226 | t.game.MoveCardToBuild(t.game.Build2, t.selected) 227 | }) { 228 | return true 229 | } else if t.cardTapped(render.build3, pos, func() { 230 | t.game.MoveCardToBuild(t.game.Build3, t.selected) 231 | }) { 232 | return true 233 | } else if t.cardTapped(render.build4, pos, func() { 234 | t.game.MoveCardToBuild(t.game.Build4, t.selected) 235 | }) { 236 | return true 237 | } 238 | 239 | if t.checkStackTapped(render.stack1, t.game.Stack1, pos) { 240 | return true 241 | } else if t.checkStackTapped(render.stack2, t.game.Stack2, pos) { 242 | return true 243 | } else if t.checkStackTapped(render.stack3, t.game.Stack3, pos) { 244 | return true 245 | } else if t.checkStackTapped(render.stack4, t.game.Stack4, pos) { 246 | return true 247 | } else if t.checkStackTapped(render.stack5, t.game.Stack5, pos) { 248 | return true 249 | } else if t.checkStackTapped(render.stack6, t.game.Stack6, pos) { 250 | return true 251 | } else if t.checkStackTapped(render.stack7, t.game.Stack7, pos) { 252 | return true 253 | } 254 | 255 | t.selected = nil // clicked elsewhere 256 | t.Refresh() 257 | return false 258 | } 259 | 260 | func (t *Table) finishAnimation() { 261 | anim := container.NewWithoutLayout() 262 | anim.Resize(t.Size()) 263 | fyne.CurrentApp().Driver().CanvasForObject(t).Overlays().Add(anim) 264 | wg := &sync.WaitGroup{} 265 | 266 | for _, p := range []*Stack{t.game.Build1, t.game.Build2, t.game.Build3, t.game.Build4} { 267 | c := len(p.Cards) 268 | if c == 0 { 269 | continue 270 | } 271 | wg.Add(c) 272 | } 273 | go func() { 274 | for i := ValueKing; i > 0; i-- { 275 | for j, p := range []*Stack{t.game.Build1, t.game.Build2, t.game.Build3, t.game.Build4} { 276 | card := p.Pop() 277 | if card == nil { 278 | break 279 | } 280 | pos := t.stackPos(j) 281 | 282 | off := fyne.Delta{DX: -2, DY: 1} 283 | switch j { 284 | case 0: 285 | off.DX = 2 286 | case 1: 287 | off.DX = 2 288 | off.DY = -1 289 | case 2: 290 | off.DY = -1 291 | } 292 | fyne.Do(func() { 293 | image := t.startCardAnimation(card, pos, off, wg) 294 | anim.Objects = append([]fyne.CanvasObject{image}, anim.Objects...) 295 | 296 | t.Refresh() 297 | }) 298 | time.Sleep(time.Second / 6) 299 | } 300 | } 301 | 302 | wg.Wait() 303 | anim.Objects = []fyne.CanvasObject{} 304 | fyne.CurrentApp().Driver().CanvasForObject(t).Overlays().Remove(anim) 305 | }() 306 | } 307 | 308 | func (t *Table) startCardAnimation(card *Card, pos fyne.Position, off fyne.Delta, wg *sync.WaitGroup) fyne.CanvasObject { 309 | bounds := t.Size() 310 | pad := theme.Padding() 311 | i := canvas.NewImageFromResource(faces.ForCard(card.Value, int(card.Suit))) 312 | i.Resize(cardSize) 313 | i.Move(pos) 314 | 315 | go func() { 316 | for pos.X > -cardSize.Width-pad && pos.Y > -cardSize.Height-pad && pos.X < bounds.Width && pos.Y < bounds.Height { 317 | pos = pos.Add(off) 318 | fyne.Do(func() { 319 | i.Move(pos) 320 | }) 321 | 322 | time.Sleep(time.Millisecond * 12) 323 | } 324 | 325 | fyne.Do(func() { 326 | i.Hide() 327 | wg.Done() 328 | }) 329 | }() 330 | 331 | return i 332 | } 333 | 334 | // NewTable creates a new table widget for the specified game 335 | func NewTable(g *Game) *Table { 336 | table := &Table{game: g} 337 | table.ExtendBaseWidget(table) 338 | 339 | table.float = make([]*canvas.Image, ValueKing) 340 | for i := 0; i < ValueKing; i++ { 341 | table.float[i] = &canvas.Image{} 342 | table.float[i].Hide() 343 | } 344 | table.floatSource = make([]*canvas.Image, ValueKing) 345 | 346 | return table 347 | } 348 | -------------------------------------------------------------------------------- /theme.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | 6 | "fyne.io/fyne/v2" 7 | "fyne.io/fyne/v2/theme" 8 | ) 9 | 10 | type gameTheme struct { 11 | fyne.Theme 12 | } 13 | 14 | func newGameTheme() fyne.Theme { 15 | return &gameTheme{theme.DefaultTheme()} 16 | } 17 | 18 | func (g *gameTheme) Color(n fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color { 19 | switch n { 20 | case theme.ColorNameBackground: 21 | return color.RGBA{R: 0x07, G: 0x63, B: 0x24, A: 0xff} 22 | case theme.ColorNameSeparator: 23 | return color.RGBA{R: 0x02, G: 0x52, B: 0x10, A: 0xff} 24 | } 25 | 26 | return g.Theme.Color(n, theme.VariantDark) 27 | } 28 | --------------------------------------------------------------------------------