mirror of
https://github.com/brianstrauch/solitaire-tui.git
synced 2024-12-25 21:58:52 +01:00
initial commit
This commit is contained in:
parent
3bb413dfa2
commit
3f5f55ccbe
8 changed files with 626 additions and 0 deletions
29
go.mod
Normal file
29
go.mod
Normal 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
61
go.sum
Normal 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=
|
257
internal/solitaire/solitaire.go
Normal file
257
internal/solitaire/solitaire.go
Normal 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
19
main.go
Normal 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
73
pkg/card.go
Normal 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
14
pkg/card_test.go
Normal 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
146
pkg/deck.go
Normal 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
27
pkg/deck_test.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue