mirror of
https://github.com/brianstrauch/solitaire-tui.git
synced 2025-01-13 08:01:26 +01:00
257 lines
5.2 KiB
Go
257 lines
5.2 KiB
Go
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
|
|
}
|