initial commit

This commit is contained in:
Brian Strauch 2023-01-02 23:24:59 -06:00
parent 3bb413dfa2
commit 3f5f55ccbe
8 changed files with 626 additions and 0 deletions

29
go.mod Normal file
View file

@ -0,0 +1,29 @@
module github.com/brianstrauch/solitaire-tui
go 1.19
require (
github.com/charmbracelet/bubbletea v0.23.1
github.com/charmbracelet/lipgloss v0.6.0
github.com/stretchr/testify v1.8.1
)
require (
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.13.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

61
go.sum Normal file
View file

@ -0,0 +1,61 @@
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/charmbracelet/bubbletea v0.23.1 h1:CYdteX1wCiCzKNUlwm25ZHBIc1GXlYFyUIte8WPvhck=
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,257 @@
package solitaire
import (
"fmt"
"strings"
"github.com/brianstrauch/solitaire-tui/pkg"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type deckType int
const (
stock deckType = 0
waste deckType = 1
foundation deckType = 2
tableau deckType = 6
)
var deckTypes = []deckType{
stock,
waste,
foundation,
foundation,
foundation,
foundation,
tableau,
tableau,
tableau,
tableau,
tableau,
tableau,
tableau,
}
var deckLocations = []cell{
{0, 0},
{6, 0},
{18, 0},
{24, 0},
{30, 0},
{36, 0},
{0, 5},
{6, 5},
{12, 5},
{18, 5},
{24, 5},
{30, 5},
{36, 5},
}
type Solitaire struct {
message string
decks []*pkg.Deck
selected *index
mouse tea.MouseMsg
windowHeight int
maxHeight int
}
type cell struct {
x int
y int
}
type index struct {
deck int
card int
}
func New() *Solitaire {
decks := make([]*pkg.Deck, 13)
decks[stock] = pkg.NewFullDeck()
for i := 1; i < len(decks); i++ {
decks[i] = pkg.NewEmptyDeck()
}
for i := 0; i < len(decks)-int(tableau); i++ {
deck := decks[int(tableau)+i]
for j := 0; j <= i; j++ {
deck.Add(decks[stock].Pop())
}
deck.Top().Flip()
deck.Expand()
}
return &Solitaire{
message: "Solitaire",
decks: decks,
}
}
func (s *Solitaire) Init() tea.Cmd {
return nil
}
func (s *Solitaire) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
return s, tea.Quit
}
case tea.WindowSizeMsg:
s.windowHeight = msg.Height
case tea.MouseMsg:
switch msg.Type {
case tea.MouseLeft:
if s.mouse.Type != tea.MouseLeft {
s.mouse = msg
}
case tea.MouseRelease:
if s.mouse.Type == tea.MouseLeft && msg.X == s.mouse.X && msg.Y == s.mouse.Y {
height := lipgloss.Height(s.View())
if height > s.maxHeight {
s.maxHeight = height
}
y := msg.Y - (s.windowHeight - s.maxHeight)
s.click(msg.X, y)
}
s.mouse = msg
}
}
return s, nil
}
func (s *Solitaire) click(x, y int) {
s.message = fmt.Sprintf("(%d, %d)", x, y)
for i, deck := range s.decks {
loc := deckLocations[i]
if ok, j := deck.IsClicked(x-loc.x, y-loc.y); ok {
switch deckTypes[i] {
case stock:
if deck.Size() > 0 {
s.draw(3, deck, s.decks[waste])
} else {
s.draw(s.decks[waste].Size(), s.decks[waste], deck)
}
case waste:
if deck.Size() > 0 {
s.toggleSelect(&index{deck: i, card: deck.Size() - 1})
}
case foundation:
if s.selected != nil && s.selected.deck != i {
ok := s.move(&index{deck: i})
if !ok {
s.toggleSelect(&index{deck: i, card: deck.Size() - 1})
}
} else if deck.Size() > 0 {
s.toggleSelect(&index{deck: i, card: deck.Size() - 1})
}
case tableau:
s.message = fmt.Sprintf("%d %d", j, deck.Size()-1)
if j == deck.Size()-1 && !deck.Top().IsVisible {
if s.selected != nil {
s.toggleSelect(s.selected)
}
deck.Top().Flip()
} else if s.selected != nil && s.selected.deck != i {
ok := s.move(&index{deck: i, card: j})
if !ok {
s.toggleSelect(&index{deck: i, card: j})
s.toggleSelect(&index{deck: i, card: j})
}
} else if deck.Get(j).IsVisible {
if s.selected != nil && s.selected.deck == i && s.selected.card != j {
s.toggleSelect(&index{deck: i, card: j})
}
s.toggleSelect(&index{deck: i, card: j})
}
}
break
}
}
}
func (s *Solitaire) draw(n int, from, to *pkg.Deck) {
if s.selected != nil {
s.toggleSelect(s.selected)
}
for i := 0; i < n; i++ {
if card := from.Pop(); card != nil {
s.message += card.String()
card.Flip()
to.Add(card)
}
}
}
func (s *Solitaire) move(to *index) bool {
toDeck := s.decks[to.deck]
fromDeck := s.decks[s.selected.deck]
fromCards := fromDeck.GetFrom(s.selected.card)
switch deckTypes[to.deck] {
case foundation:
if s.selected.card == fromDeck.Size()-1 && toDeck.Size() == 0 && fromDeck.Top().Value == 0 || toDeck.Size() > 0 && fromDeck.Top().Value == toDeck.Top().Value+1 && fromDeck.Top().Suit == toDeck.Top().Suit {
s.toggleSelect(s.selected)
toDeck.Add(fromDeck.Pop())
s.selected = nil
return true
}
case tableau:
s.message = fmt.Sprintf("%d %d", toDeck.Size(), fromCards[0].Value)
if toDeck.Size() == 0 && fromCards[0].Value == 12 || toDeck.Size() > 0 && fromCards[0].Value+1 == toDeck.Top().Value && fromCards[0].Color() != toDeck.Top().Color() {
idx := s.selected.card
s.toggleSelect(s.selected)
toDeck.Add(fromDeck.PopFrom(idx)...)
s.selected = nil
return true
}
}
return false
}
func (s *Solitaire) toggleSelect(selected *index) {
if s.selected != nil {
s.decks[s.selected.deck].Get(s.selected.card).IsSelected = false
s.selected = nil
} else {
s.selected = selected
s.decks[s.selected.deck].Get(s.selected.card).IsSelected = true
}
}
func (s *Solitaire) View() string {
view := lipgloss.JoinHorizontal(lipgloss.Top,
s.decks[0].View(),
s.decks[1].View(),
strings.Repeat(" ", 6),
s.decks[2].View(),
s.decks[3].View(),
s.decks[4].View(),
s.decks[5].View(),
) + "\n"
view += lipgloss.JoinHorizontal(lipgloss.Top,
s.decks[6].View(),
s.decks[7].View(),
s.decks[8].View(),
s.decks[9].View(),
s.decks[10].View(),
s.decks[11].View(),
s.decks[12].View(),
) + "\n"
return view
}

19
main.go Normal file
View file

@ -0,0 +1,19 @@
package main
import (
"log"
"math/rand"
"time"
"github.com/brianstrauch/solitaire-tui/internal/solitaire"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
rand.Seed(time.Now().UnixNano())
p := tea.NewProgram(solitaire.New(), tea.WithMouseCellMotion())
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}

73
pkg/card.go Normal file
View file

@ -0,0 +1,73 @@
package pkg
import (
"strings"
"github.com/charmbracelet/lipgloss"
)
var (
values = []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}
suits = []string{"♠", "♦", "♥", "♣"}
)
const (
width = 6
height = 5
)
type Card struct {
Value int
Suit int
IsVisible bool
IsSelected bool
}
func NewCard(value, suit int) *Card {
return &Card{
Value: value,
Suit: suit,
}
}
func (c *Card) View() string {
if !c.IsVisible {
return viewCard("", "", c.IsSelected)
}
style := lipgloss.NewStyle().Foreground(lipgloss.Color(c.Color()))
return viewCard(" ", style.Render(c.String()), c.IsSelected)
}
func (c *Card) Flip() {
c.IsVisible = !c.IsVisible
}
func (c *Card) Color() string {
if c.Suit == 1 || c.Suit == 2 {
return "#FF0000"
} else {
return "#000000"
}
}
func (c *Card) String() string {
return values[c.Value] + suits[c.Suit]
}
func viewCard(design, shorthand string, isSelected bool) string {
style := lipgloss.NewStyle()
if isSelected {
style = style.Foreground(lipgloss.Color("#FFFF00"))
}
padding := strings.Repeat("─", width-2-lipgloss.Width(shorthand))
view := style.Render("╭") + shorthand + style.Render(padding+"╮") + "\n"
for i := 1; i < height-1; i++ {
view += style.Render("│"+strings.Repeat(design, width-2)+"│") + "\n"
}
view += style.Render("╰"+padding) + shorthand + style.Render("╯")
return view
}

14
pkg/card_test.go Normal file
View file

@ -0,0 +1,14 @@
package pkg
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestFlip(t *testing.T) {
card := new(Card)
card.Flip()
require.True(t, card.IsVisible)
}

146
pkg/deck.go Normal file
View file

@ -0,0 +1,146 @@
package pkg
import (
"math/rand"
"strings"
)
type Deck struct {
cards []*Card
isExpanded bool
}
func NewDeck(cards []*Card) *Deck {
return &Deck{cards: cards}
}
func NewFullDeck() *Deck {
cards := make([]*Card, len(values)*len(suits))
for i := range values {
for j := range suits {
cards[i*len(suits)+j] = NewCard(i, j)
}
}
deck := NewDeck(cards)
deck.Shuffle()
return deck
}
func NewEmptyDeck() *Deck {
return NewDeck(make([]*Card, 0))
}
func (d *Deck) Shuffle() {
rand.Shuffle(d.Size(), func(i, j int) {
d.cards[i], d.cards[j] = d.cards[j], d.cards[i]
})
}
func (d *Deck) Expand() {
d.isExpanded = true
}
func (d *Deck) View() string {
// Outline
if d.Size() == 0 {
return viewCard(" ", "", false)
}
// Expanded cards
if d.isExpanded {
var view string
for i := 0; i < d.Size()-1; i++ {
view += strings.Split(d.cards[i].View(), "\n")[0] + "\n"
}
return view + d.cards[d.Size()-1].View()
}
// Top card only
return d.cards[d.Size()-1].View()
}
func (d *Deck) IsClicked(x, y int) (bool, int) {
if d.Size() == 0 {
return x >= 0 && x < width && y >= 0 && y < height, 0
}
if d.isExpanded {
for i := d.Size() - 1; i >= 0; i-- {
if x >= 0 && x < width && y >= i && y < i+height {
return true, i
}
}
return false, 0
}
return x >= 0 && x < width && y >= 0 && y < height, 0
}
func (d *Deck) Add(cards ...*Card) {
d.cards = append(d.cards, cards...)
}
func (d *Deck) Top() *Card {
return d.Get(d.Size() - 1)
}
func (d *Deck) Bottom() *Card {
return d.Get(0)
}
func (d *Deck) Get(idx int) *Card {
return d.cards[idx]
}
func (d *Deck) GetFrom(idx int) []*Card {
return d.cards[idx:]
}
func (d *Deck) Pop() *Card {
if len(d.cards) > 0 {
return d.PopFrom(d.Size() - 1)[0]
}
return nil
}
func (d *Deck) PopFrom(idx int) []*Card {
cards := d.cards[idx:]
d.cards = d.cards[:idx]
return cards
}
func (d *Deck) Size() int {
return len(d.cards)
}
// TestDeck is a helper function to simplify testing.
func TestDeck(shorthands ...string) *Deck {
cards := make([]*Card, len(shorthands))
for i, shorthand := range shorthands {
cards[i] = testCard(shorthand)
}
return &Deck{cards: cards}
}
func testCard(shorthand string) *Card {
card := &Card{IsVisible: !strings.HasSuffix(shorthand, "?")}
for i, value := range values {
if strings.HasPrefix(shorthand, value) {
card.Value = i
break
}
}
for i, suit := range suits {
if strings.Contains(shorthand, suit) {
card.Suit = i
break
}
}
return card
}

27
pkg/deck_test.go Normal file
View file

@ -0,0 +1,27 @@
package pkg
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNewDeck(t *testing.T) {
deck := NewFullDeck()
expected := TestDeck(
"A♠?", "2♠?", "3♠?", "4♠?", "5♠?", "6♠?", "7♠?", "8♠?", "9♠?", "10♠?", "J♠?", "Q♠?", "K♠?",
"A♦?", "2♦?", "3♦?", "4♦?", "5♦?", "6♦?", "7♦?", "8♦?", "9♦?", "10♦?", "J♦?", "Q♦?", "K♦?",
"A♥?", "2♥?", "3♥?", "4♥?", "5♥?", "6♥?", "7♥?", "8♥?", "9♥?", "10♥?", "J♥?", "Q♥?", "K♥?",
"A♣?", "2♣?", "3♣?", "4♣?", "5♣?", "6♣?", "7♣?", "8♣?", "9♣?", "10♣?", "J♣?", "Q♣?", "K♣?",
)
require.ElementsMatch(t, expected.cards, deck.cards)
}
func TestShuffle(t *testing.T) {
deck := TestDeck("A♠", "2♠")
deck.Shuffle()
require.ElementsMatch(t, TestDeck("A♠", "2♠").cards, deck.cards)
}