go_games_collection/klondike_game.go

291 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}