306 lines
7.2 KiB
Go
306 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// klondikePileRef — ссылка на стопку: колонна (0-6), отбой (-1) или
|
|
// "ничего не выбрано" (-2).
|
|
type klondikePileRef int
|
|
|
|
const (
|
|
klondikeNoSelection klondikePileRef = -2
|
|
klondikeWasteRef klondikePileRef = -1
|
|
)
|
|
|
|
// KlondikeModel — TUI поверх KlondikeGameState. Управление в два
|
|
// шага: сначала выбираем источник (стопку, откуда берём карты),
|
|
// затем — куда их переложить.
|
|
type KlondikeModel struct {
|
|
game *KlondikeGameState
|
|
selected klondikePileRef
|
|
|
|
message string
|
|
isError bool
|
|
|
|
quitting bool
|
|
backToMenu bool
|
|
}
|
|
|
|
// NewKlondikeModel создаёт новую партию пасьянса.
|
|
func NewKlondikeModel() KlondikeModel {
|
|
return KlondikeModel{
|
|
game: NewKlondikeGame(),
|
|
selected: klondikeNoSelection,
|
|
}
|
|
}
|
|
|
|
func (m KlondikeModel) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
func (m *KlondikeModel) setInfo(format string, args ...interface{}) {
|
|
m.message = fmt.Sprintf(format, args...)
|
|
m.isError = false
|
|
}
|
|
|
|
func (m *KlondikeModel) setError(err error) {
|
|
m.message = T(err.Error())
|
|
m.isError = true
|
|
}
|
|
|
|
func (m *KlondikeModel) clearSelection() {
|
|
m.selected = klondikeNoSelection
|
|
}
|
|
|
|
func (m KlondikeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
keyMsg, ok := msg.(tea.KeyMsg)
|
|
if !ok {
|
|
return m, nil
|
|
}
|
|
return m.handleKey(keyMsg)
|
|
}
|
|
|
|
func (m KlondikeModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
key := msg.String()
|
|
|
|
if key == "ctrl+c" {
|
|
m.quitting = true
|
|
return m, tea.Quit
|
|
}
|
|
if key == "q" {
|
|
m.backToMenu = true
|
|
return m, nil
|
|
}
|
|
|
|
if m.game.Won {
|
|
if key == "n" {
|
|
m.game = NewKlondikeGame()
|
|
m.clearSelection()
|
|
m.message = ""
|
|
m.isError = false
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
switch key {
|
|
case "n":
|
|
m.game = NewKlondikeGame()
|
|
m.clearSelection()
|
|
m.message = ""
|
|
m.isError = false
|
|
return m, nil
|
|
case "u":
|
|
if err := m.game.Undo(); err != nil {
|
|
m.setError(err)
|
|
return m, nil
|
|
}
|
|
m.clearSelection()
|
|
m.setInfo(T("klondike.msg.undone"))
|
|
return m, nil
|
|
case "esc":
|
|
m.clearSelection()
|
|
m.message = ""
|
|
return m, nil
|
|
case "d":
|
|
m.clearSelection()
|
|
if err := m.game.DrawStock(); err != nil {
|
|
m.setError(err)
|
|
return m, nil
|
|
}
|
|
m.setInfo(T("klondike.msg.drew"))
|
|
return m, nil
|
|
case "1", "2", "3", "4", "5", "6", "7":
|
|
col := int(key[0] - '1')
|
|
return m.handlePileKey(klondikePileRef(col))
|
|
case "0":
|
|
return m.handlePileKey(klondikeWasteRef)
|
|
case "f":
|
|
return m.handleFoundationKey()
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// handlePileKey обрабатывает нажатие клавиши колонны (1-7) или
|
|
// отбоя (0): если ничего не выбрано — пытается выбрать эту стопку
|
|
// как источник; если что-то уже выбрано — пытается переложить туда.
|
|
func (m KlondikeModel) handlePileKey(ref klondikePileRef) (tea.Model, tea.Cmd) {
|
|
if m.selected == klondikeNoSelection {
|
|
if !m.pileHasMovableCard(ref) {
|
|
m.setError(ErrKlondikeEmptySource)
|
|
return m, nil
|
|
}
|
|
m.selected = ref
|
|
m.message = ""
|
|
return m, nil
|
|
}
|
|
|
|
if m.selected == ref {
|
|
m.clearSelection()
|
|
return m, nil
|
|
}
|
|
|
|
var err error
|
|
if m.selected == klondikeWasteRef {
|
|
err = m.game.MoveWasteToTableau(int(ref))
|
|
} else {
|
|
err = m.game.MoveTableauToTableau(int(m.selected), int(ref))
|
|
}
|
|
if err != nil {
|
|
m.setError(err)
|
|
return m, nil
|
|
}
|
|
m.setInfo(T("klondike.msg.moved"))
|
|
m.clearSelection()
|
|
return m, nil
|
|
}
|
|
|
|
func (m KlondikeModel) handleFoundationKey() (tea.Model, tea.Cmd) {
|
|
if m.selected == klondikeNoSelection {
|
|
m.setError(ErrKlondikeEmptySource)
|
|
return m, nil
|
|
}
|
|
var err error
|
|
if m.selected == klondikeWasteRef {
|
|
err = m.game.MoveWasteToFoundation()
|
|
} else {
|
|
if m.game.movableRun(int(m.selected)) != 1 {
|
|
err = ErrKlondikeMultiToFoundation
|
|
} else {
|
|
err = m.game.MoveTableauToFoundation(int(m.selected))
|
|
}
|
|
}
|
|
if err != nil {
|
|
m.setError(err)
|
|
return m, nil
|
|
}
|
|
m.setInfo(T("klondike.msg.to_foundation"))
|
|
m.clearSelection()
|
|
return m, nil
|
|
}
|
|
|
|
func (m KlondikeModel) pileHasMovableCard(ref klondikePileRef) bool {
|
|
if ref == klondikeWasteRef {
|
|
return len(m.game.Waste) > 0
|
|
}
|
|
return m.game.movableRun(int(ref)) > 0
|
|
}
|
|
|
|
// --- Отрисовка -----------------------------------------------------
|
|
|
|
func (m KlondikeModel) renderFoundations() string {
|
|
var parts []string
|
|
suits := []Suit{Clubs, Diamonds, Hearts, Spades}
|
|
for _, s := range suits {
|
|
rank := m.game.Foundations[s]
|
|
if rank == 0 {
|
|
parts = append(parts, dimStyle.Render("["+s.String()+" --]"))
|
|
continue
|
|
}
|
|
card := Card{Suit: s, Rank: Rank(rank)}
|
|
parts = append(parts, "["+renderCard(card)+"]")
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func (m KlondikeModel) renderStockWaste() string {
|
|
stockStr := dimStyle.Render(fmt.Sprintf("(%d)", len(m.game.Stock)))
|
|
wasteStr := dimStyle.Render(T("klondike.empty"))
|
|
if len(m.game.Waste) > 0 {
|
|
top := m.game.Waste[len(m.game.Waste)-1]
|
|
wasteStr = renderCard(top)
|
|
if m.selected == klondikeWasteRef {
|
|
wasteStr = cursorStyle.Render(top.String())
|
|
}
|
|
}
|
|
return fmt.Sprintf(T("klondike.status_line"), stockStr, wasteStr)
|
|
}
|
|
|
|
func (m KlondikeModel) renderTableau() string {
|
|
var rows []string
|
|
maxLen := 0
|
|
for _, col := range m.game.Tableau {
|
|
if len(col) > maxLen {
|
|
maxLen = len(col)
|
|
}
|
|
}
|
|
|
|
header := make([]string, 7)
|
|
for i := range header {
|
|
label := fmt.Sprintf(" %d ", i+1)
|
|
if m.selected == klondikePileRef(i) {
|
|
label = cursorStyle.Render(label)
|
|
} else {
|
|
label = dimStyle.Render(label)
|
|
}
|
|
header[i] = label
|
|
}
|
|
rows = append(rows, strings.Join(header, " "))
|
|
|
|
for row := 0; row < maxLen; row++ {
|
|
cells := make([]string, 7)
|
|
for col := 0; col < 7; col++ {
|
|
if row >= len(m.game.Tableau[col]) {
|
|
cells[col] = " "
|
|
continue
|
|
}
|
|
kc := m.game.Tableau[col][row]
|
|
if !kc.FaceUp {
|
|
cells[col] = dimStyle.Render("[?]")
|
|
continue
|
|
}
|
|
text := renderCard(kc.Card)
|
|
isTopOfSelectedRun := m.selected == klondikePileRef(col) &&
|
|
row >= len(m.game.Tableau[col])-m.game.movableRun(col)
|
|
if isTopOfSelectedRun {
|
|
text = cursorStyle.Render(kc.Card.String())
|
|
}
|
|
cells[col] = text
|
|
}
|
|
rows = append(rows, strings.Join(cells, " "))
|
|
}
|
|
return strings.Join(rows, "\n")
|
|
}
|
|
|
|
func (m KlondikeModel) View() string {
|
|
if m.quitting {
|
|
return T("common.goodbye")
|
|
}
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintln(&b, titleStyle.Render(T("klondike.title")))
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintf(&b, T("klondike.foundations_line"), m.renderFoundations())
|
|
fmt.Fprintln(&b, m.renderStockWaste())
|
|
fmt.Fprintf(&b, T("klondike.moves_line"), m.game.Moves)
|
|
fmt.Fprintln(&b, m.renderTableau())
|
|
fmt.Fprintln(&b)
|
|
|
|
if m.game.Won {
|
|
fmt.Fprintln(&b, winStyle.Render(Tf("klondike.won", m.game.Moves)))
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintln(&b, dimStyle.Render(T("klondike.hint.gameover")))
|
|
return b.String()
|
|
}
|
|
|
|
if m.selected == klondikeNoSelection {
|
|
fmt.Fprintln(&b, T("klondike.hint.noselect"))
|
|
} else {
|
|
fmt.Fprintln(&b, T("klondike.hint.selected"))
|
|
}
|
|
fmt.Fprintln(&b, dimStyle.Render(T("klondike.hint.footer")))
|
|
|
|
fmt.Fprintln(&b)
|
|
if m.message != "" {
|
|
if m.isError {
|
|
fmt.Fprintln(&b, errorStyle.Render("! "+m.message))
|
|
} else {
|
|
fmt.Fprintln(&b, infoStyle.Render(m.message))
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|