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. func (g *KlondikeGameState) MoveTableauToTableau(from, to int) error { if from == to { return ErrKlondikeInvalidTableau } run := g.movableRun(from) if run == 0 { return ErrKlondikeEmptySource } cards := g.Tableau[from] moving := cards[len(cards)-run:] if !g.canPlaceOnTableau(to, moving[0].Card) { return ErrKlondikeInvalidTableau } g.pushHistory() movingCopy := append([]KlondikeCard{}, moving...) g.Tableau[from] = cards[:len(cards)-run] g.Tableau[to] = append(g.Tableau[to], movingCopy...) g.flipTopIfNeeded(from) g.Moves++ return nil }