291 lines
11 KiB
Go
291 lines
11 KiB
Go
package main
|
||
|
||
import "errors"
|
||
|
||
// ПРИМЕЧАНИЕ О ПРАВИЛАХ: реализован классический "Клондайк"
|
||
// (Косынка) с добором по одной карте за раз и неограниченным числом
|
||
// пересдач колоды из отбоя. Перемещать между колоннами можно только
|
||
// целиком собранную по правилам последовательность (по убыванию,
|
||
// чередуя цвет мастей) — нельзя "разрезать" такую цепочку и взять
|
||
// только её часть. Возврат карт с фундамента обратно в игру не
|
||
// реализован — это распространённое упрощение цифровых версий.
|
||
|
||
// KlondikeCard — карта в колонне: помимо самой карты нужно знать,
|
||
// открыта она лицом вверх или ещё лежит рубашкой вверх.
|
||
type KlondikeCard struct {
|
||
Card Card
|
||
FaceUp bool
|
||
}
|
||
|
||
var (
|
||
ErrKlondikeEmptySource = errors.New("в исходной стопке нет карт")
|
||
ErrKlondikeNotMovable = errors.New("эта карта (или последовательность под ней) не может быть перемещена")
|
||
ErrKlondikeInvalidTableau = errors.New("эту карту нельзя положить в эту колонну")
|
||
ErrKlondikeInvalidFoundation = errors.New("эту карту нельзя положить на фундамент")
|
||
ErrKlondikeMultiToFoundation = errors.New("на фундамент можно положить только одну карту")
|
||
ErrKlondikeNoUndo = errors.New("нечего отменять")
|
||
)
|
||
|
||
// KlondikeGameState — полное состояние партии пасьянса.
|
||
type KlondikeGameState struct {
|
||
Tableau [7][]KlondikeCard
|
||
Foundations [4]int // индекс — Suit; значение — старший ранг на фундаменте (0 = пусто)
|
||
Stock []Card // рубашкой вверх; Stock[len-1] — верхняя (берётся первой)
|
||
Waste []Card // отбой лицом вверх; Waste[len-1] — верхняя (видимая)
|
||
Moves int
|
||
Won bool
|
||
|
||
history []klondikeSnapshot
|
||
}
|
||
|
||
// NewKlondikeGame раздаёт новую партию: колонны по 1..7 карт (верхняя
|
||
// открыта), остаток — колода добора.
|
||
func NewKlondikeGame() *KlondikeGameState {
|
||
deck := NewDeck()
|
||
deck.Shuffle()
|
||
|
||
g := &KlondikeGameState{}
|
||
for col := 0; col < 7; col++ {
|
||
for row := 0; row <= col; row++ {
|
||
card, _ := deck.Draw()
|
||
g.Tableau[col] = append(g.Tableau[col], KlondikeCard{Card: card, FaceUp: row == col})
|
||
}
|
||
}
|
||
g.Stock = deck.cards
|
||
return g
|
||
}
|
||
|
||
func isRedSuit(s Suit) bool {
|
||
return s == Hearts || s == Diamonds
|
||
}
|
||
|
||
// movableRun возвращает длину наибольшей корректно собранной (по
|
||
// убыванию, с чередованием цвета) последовательности лицевых карт в
|
||
// конце (внизу) колонны col — именно столько карт можно перемещать
|
||
// вместе одним ходом. Если конец колонны не образует такой
|
||
// последовательности, движется только самая нижняя (видимая) карта.
|
||
func (g *KlondikeGameState) movableRun(col int) int {
|
||
cards := g.Tableau[col]
|
||
n := 0
|
||
for i := len(cards) - 1; i >= 0; i-- {
|
||
if !cards[i].FaceUp {
|
||
break
|
||
}
|
||
if n == 0 {
|
||
n = 1
|
||
continue
|
||
}
|
||
below := cards[i]
|
||
above := cards[i+1]
|
||
if int(below.Card.Rank) == int(above.Card.Rank)+1 && isRedSuit(below.Card.Suit) != isRedSuit(above.Card.Suit) {
|
||
n++
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
return n
|
||
}
|
||
|
||
// klondikeSnapshot — полная копия состояния для отмены хода.
|
||
type klondikeSnapshot struct {
|
||
tableau [7][]KlondikeCard
|
||
foundations [4]int
|
||
stock []Card
|
||
waste []Card
|
||
moves int
|
||
}
|
||
|
||
func (g *KlondikeGameState) snapshot() klondikeSnapshot {
|
||
var s klondikeSnapshot
|
||
for i := range g.Tableau {
|
||
s.tableau[i] = append([]KlondikeCard{}, g.Tableau[i]...)
|
||
}
|
||
s.foundations = g.Foundations
|
||
s.stock = append([]Card{}, g.Stock...)
|
||
s.waste = append([]Card{}, g.Waste...)
|
||
s.moves = g.Moves
|
||
return s
|
||
}
|
||
|
||
func (g *KlondikeGameState) pushHistory() {
|
||
g.history = append(g.history, g.snapshot())
|
||
if len(g.history) > 200 {
|
||
g.history = g.history[1:]
|
||
}
|
||
}
|
||
|
||
// Undo отменяет последний совершённый ход.
|
||
func (g *KlondikeGameState) Undo() error {
|
||
if len(g.history) == 0 {
|
||
return ErrKlondikeNoUndo
|
||
}
|
||
last := g.history[len(g.history)-1]
|
||
g.history = g.history[:len(g.history)-1]
|
||
g.Tableau = last.tableau
|
||
g.Foundations = last.foundations
|
||
g.Stock = last.stock
|
||
g.Waste = last.waste
|
||
g.Moves = last.moves
|
||
g.Won = false
|
||
return nil
|
||
}
|
||
|
||
// DrawStock переносит верхнюю карту колоды добора в отбой лицом
|
||
// вверх; если колода добора пуста — перекладывает весь отбой
|
||
// обратно в колоду (в обратном порядке) для новой пересдачи.
|
||
func (g *KlondikeGameState) DrawStock() error {
|
||
if len(g.Stock) == 0 && len(g.Waste) == 0 {
|
||
return ErrKlondikeEmptySource
|
||
}
|
||
g.pushHistory()
|
||
if len(g.Stock) == 0 {
|
||
for i, j := 0, len(g.Waste)-1; i < j; i, j = i+1, j-1 {
|
||
g.Waste[i], g.Waste[j] = g.Waste[j], g.Waste[i]
|
||
}
|
||
g.Stock = g.Waste
|
||
g.Waste = nil
|
||
return nil
|
||
}
|
||
card := g.Stock[len(g.Stock)-1]
|
||
g.Stock = g.Stock[:len(g.Stock)-1]
|
||
g.Waste = append(g.Waste, card)
|
||
g.Moves++
|
||
return nil
|
||
}
|
||
|
||
// canPlaceOnTableau проверяет, можно ли положить card на верхнюю
|
||
// (видимую) карту колонны col (или в пустую колонну — только
|
||
// король).
|
||
func (g *KlondikeGameState) canPlaceOnTableau(col int, card Card) bool {
|
||
cards := g.Tableau[col]
|
||
if len(cards) == 0 {
|
||
return card.Rank == King
|
||
}
|
||
top := cards[len(cards)-1]
|
||
if !top.FaceUp {
|
||
return false
|
||
}
|
||
return int(card.Rank)+1 == int(top.Card.Rank) && isRedSuit(card.Suit) != isRedSuit(top.Card.Suit)
|
||
}
|
||
|
||
// canPlaceOnFoundation проверяет, можно ли положить card на
|
||
// фундамент её масти прямо сейчас.
|
||
func (g *KlondikeGameState) canPlaceOnFoundation(card Card) bool {
|
||
return int(card.Rank) == g.Foundations[card.Suit]+1
|
||
}
|
||
|
||
func (g *KlondikeGameState) flipTopIfNeeded(col int) {
|
||
cards := g.Tableau[col]
|
||
if len(cards) > 0 && !cards[len(cards)-1].FaceUp {
|
||
g.Tableau[col][len(cards)-1].FaceUp = true
|
||
}
|
||
}
|
||
|
||
func (g *KlondikeGameState) checkWin() {
|
||
for _, r := range g.Foundations {
|
||
if r != int(King) {
|
||
return
|
||
}
|
||
}
|
||
g.Won = true
|
||
}
|
||
|
||
// MoveWasteToTableau перекладывает верхнюю карту отбоя в колонну col.
|
||
func (g *KlondikeGameState) MoveWasteToTableau(col int) error {
|
||
if len(g.Waste) == 0 {
|
||
return ErrKlondikeEmptySource
|
||
}
|
||
card := g.Waste[len(g.Waste)-1]
|
||
if !g.canPlaceOnTableau(col, card) {
|
||
return ErrKlondikeInvalidTableau
|
||
}
|
||
g.pushHistory()
|
||
g.Waste = g.Waste[:len(g.Waste)-1]
|
||
g.Tableau[col] = append(g.Tableau[col], KlondikeCard{Card: card, FaceUp: true})
|
||
g.Moves++
|
||
return nil
|
||
}
|
||
|
||
// MoveWasteToFoundation перекладывает верхнюю карту отбоя на
|
||
// фундамент её масти.
|
||
func (g *KlondikeGameState) MoveWasteToFoundation() error {
|
||
if len(g.Waste) == 0 {
|
||
return ErrKlondikeEmptySource
|
||
}
|
||
card := g.Waste[len(g.Waste)-1]
|
||
if !g.canPlaceOnFoundation(card) {
|
||
return ErrKlondikeInvalidFoundation
|
||
}
|
||
g.pushHistory()
|
||
g.Waste = g.Waste[:len(g.Waste)-1]
|
||
g.Foundations[card.Suit] = int(card.Rank)
|
||
g.Moves++
|
||
g.checkWin()
|
||
return nil
|
||
}
|
||
|
||
// MoveTableauToFoundation перекладывает верхнюю (видимую) карту
|
||
// колонны from на фундамент её масти.
|
||
func (g *KlondikeGameState) MoveTableauToFoundation(from int) error {
|
||
cards := g.Tableau[from]
|
||
if len(cards) == 0 {
|
||
return ErrKlondikeEmptySource
|
||
}
|
||
top := cards[len(cards)-1]
|
||
if !top.FaceUp {
|
||
return ErrKlondikeNotMovable
|
||
}
|
||
if !g.canPlaceOnFoundation(top.Card) {
|
||
return ErrKlondikeInvalidFoundation
|
||
}
|
||
g.pushHistory()
|
||
g.Tableau[from] = cards[:len(cards)-1]
|
||
g.Foundations[top.Card.Suit] = int(top.Card.Rank)
|
||
g.flipTopIfNeeded(from)
|
||
g.Moves++
|
||
g.checkWin()
|
||
return nil
|
||
}
|
||
|
||
// MoveTableauToTableau перемещает наибольшую корректную
|
||
// последовательность в конце колонны from в конец колонны to.
|
||
// MoveTableauToTableau переносит ВСЮ корректно собранную
|
||
// последовательность в конце колонны from в колонну to (см.
|
||
// movableRun). Чтобы перенести только ЧАСТЬ последовательности,
|
||
// используйте MoveTableauToTableauN с явным количеством карт.
|
||
func (g *KlondikeGameState) MoveTableauToTableau(from, to int) error {
|
||
if g.movableRun(from) == 0 {
|
||
return ErrKlondikeEmptySource
|
||
}
|
||
return g.MoveTableauToTableauN(from, to, g.movableRun(from))
|
||
}
|
||
|
||
// MoveTableauToTableauN переносит ровно count карт с конца
|
||
// (снизу, лицевой стороной вверх) колонны from в колонну to. count
|
||
// может быть меньше полной длины корректной последовательности
|
||
// (movableRun) — любой "хвост" корректной последовательности сам по
|
||
// себе тоже корректная последовательность, так что достаточно
|
||
// проверить, что count в допустимых пределах и что верхняя (первая)
|
||
// из переносимых карт подходит по правилам на колонну to.
|
||
func (g *KlondikeGameState) MoveTableauToTableauN(from, to, count int) error {
|
||
if from == to {
|
||
return ErrKlondikeInvalidTableau
|
||
}
|
||
maxRun := g.movableRun(from)
|
||
if count < 1 || count > maxRun {
|
||
return ErrKlondikeInvalidTableau
|
||
}
|
||
cards := g.Tableau[from]
|
||
moving := cards[len(cards)-count:]
|
||
if !g.canPlaceOnTableau(to, moving[0].Card) {
|
||
return ErrKlondikeInvalidTableau
|
||
}
|
||
|
||
g.pushHistory()
|
||
movingCopy := append([]KlondikeCard{}, moving...)
|
||
g.Tableau[from] = cards[:len(cards)-count]
|
||
g.Tableau[to] = append(g.Tableau[to], movingCopy...)
|
||
g.flipTopIfNeeded(from)
|
||
g.Moves++
|
||
return nil
|
||
}
|