go_games_collection/thousand_game_test.go
2026-07-04 13:51:04 +03:00

399 lines
15 KiB
Go
Raw 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 "testing"
func TestThousandDealsCorrectly(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
for i, p := range g.Players {
if len(p.Hand) != 7 {
t.Errorf("игрок %d: ожидалось 7 карт, получено %d", i, len(p.Hand))
}
}
if len(g.Kitty) != 3 {
t.Fatalf("ожидалось 3 карты в прикупе, получено %d", len(g.Kitty))
}
if g.CurrentBidderIdx != (g.DealerIdx+1)%3 {
t.Errorf("первым в торгах должен ходить игрок слева от сдающего")
}
}
func TestFirstBidderMustBidAtLeast100(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
if err := g.Pass(); err != ErrThousandMustBid {
t.Errorf("первый ходящий не может пасовать, ожидалась ErrThousandMustBid, получено: %v", err)
}
if err := g.Bid(95); err != ErrThousandBidTooLow {
t.Errorf("ставка меньше 100 должна быть отклонена, получено: %v", err)
}
if err := g.Bid(100); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
}
func TestBidMustBeMultipleOf5AndHigherThanCurrent(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
if err := g.Bid(100); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.Bid(102); err != ErrThousandBidTooLow {
t.Errorf("ставка не кратная 5 должна быть отклонена, получено: %v", err)
}
if err := g.Bid(100); err != ErrThousandBidTooLow {
t.Errorf("ставка не выше текущей должна быть отклонена, получено: %v", err)
}
if err := g.Bid(105); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
}
func TestBidCappedAt120WithoutMarriage(t *testing.T) {
noMarriageHand := []Card{
c(Nine, Clubs), c(Jack, Clubs), c(Nine, Hearts),
c(Jack, Hearts), c(Nine, Diamonds), c(Jack, Diamonds), c(Nine, Spades),
}
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.Players[g.CurrentBidderIdx].Hand = append([]Card{}, noMarriageHand...)
if err := g.Bid(120); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
// проверяем превышение кэпа отдельной игрой — чтобы ставка 125
// проверялась для ТОГО ЖЕ игрока с той же рукой, а не для
// следующего (ход уже передан после успешной ставки выше)
g2 := NewThousandGame([]string{"A", "B", "C"}, -1)
g2.Players[g2.CurrentBidderIdx].Hand = append([]Card{}, noMarriageHand...)
if err := g2.Bid(125); err != ErrThousandBidTooHigh {
t.Errorf("без марьяжа ставка выше 120 должна быть отклонена, получено: %v", err)
}
}
func TestBidCapRaisedByMarriage(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.Players[g.CurrentBidderIdx].Hand = []Card{
c(King, Hearts), c(Queen, Hearts), c(Nine, Clubs),
c(Nine, Diamonds), c(Nine, Spades), c(Jack, Clubs), c(Jack, Diamonds),
}
if err := g.Bid(220); err != nil {
t.Fatalf("ставка 120+100 должна быть разрешена при червовом марьяже: %v", err)
}
g2 := NewThousandGame([]string{"A", "B", "C"}, -1)
g2.Players[g2.CurrentBidderIdx].Hand = []Card{
c(King, Hearts), c(Queen, Hearts), c(Nine, Clubs),
c(Nine, Diamonds), c(Nine, Spades), c(Jack, Clubs), c(Jack, Diamonds),
}
if err := g2.Bid(225); err != ErrThousandBidTooHigh {
t.Errorf("ставка выше 220 должна быть отклонена, получено: %v", err)
}
}
func TestBiddingEndsWhenOneBidderRemains(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
first := g.CurrentBidderIdx
if err := g.Bid(100); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.Pass(); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.Pass(); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.Phase != ThousandPhaseDiscard {
t.Fatalf("торги должны были завершиться после 2 пасов, фаза: %v", g.Phase)
}
if g.DeclarerIdx != first {
t.Errorf("объявившим торг должен стать %d, получено %d", first, g.DeclarerIdx)
}
if g.FinalBid != 100 {
t.Errorf("ожидалась итоговая ставка 100, получено %d", g.FinalBid)
}
if len(g.Players[first].Hand) != 10 {
t.Errorf("объявивший торг должен был получить прикуп (10 карт), получено %d", len(g.Players[first].Hand))
}
}
func TestDiscardGivesCardsToOpponentsAndStartsTrick(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
if err := g.Bid(100); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.Pass(); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.Pass(); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
declarer := g.DeclarerIdx
hand := append([]Card{}, g.Players[declarer].Hand...)
if err := g.DiscardCard(hand[0]); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.Phase != ThousandPhaseDiscard {
t.Fatalf("после первого сброса раздача ещё не должна была начаться")
}
if err := g.DiscardCard(hand[1]); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.Phase != ThousandPhaseTrick {
t.Fatalf("после второго сброса должна была начаться игра взятками")
}
if len(g.Players[declarer].Hand) != 8 {
t.Errorf("у объявившего торг должно остаться 8 карт, получено %d", len(g.Players[declarer].Hand))
}
for i, p := range g.Players {
if i == declarer {
continue
}
if len(p.Hand) != 8 {
t.Errorf("у игрока %d должно стать 8 карт после получения снoса, получено %d", i, len(p.Hand))
}
}
if g.LeaderIdx != declarer {
t.Errorf("объявивший торг должен ходить первым")
}
}
// setupThousandTrickPlay — тестовый хелпер: пропускает торги и снос,
// вручную задаёт руки игроков и переводит игру сразу в фазу взятки.
func setupThousandTrickPlay(g *ThousandGameState, declarerIdx int, hands map[int][]Card) {
g.DeclarerIdx = declarerIdx
g.FinalBid = 100
for idx, hand := range hands {
g.Players[idx].Hand = hand
g.Players[idx].HandPoints = 0
}
g.LeaderIdx = declarerIdx
g.CurrentTrick = nil
g.TricksPlayed = 0
g.TrumpSet = false
g.Phase = ThousandPhaseTrick
}
func TestPlayCardFollowSuitRequired(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(Nine, Clubs)},
1: {c(Nine, Hearts), c(Jack, Clubs)},
2: {c(Nine, Spades)},
})
if err := g.PlayCard(c(Nine, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Nine, Hearts)); err != ErrThousandInvalidFollow {
t.Errorf("ожидалась ошибка обязательной масти, получено: %v", err)
}
if err := g.PlayCard(c(Jack, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка при верном ходе в масть: %v", err)
}
}
func TestPlayCardMustTrumpWhenNoLedSuit(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(Nine, Clubs)},
1: {c(Jack, Hearts), c(Nine, Spades)},
2: {c(Nine, Diamonds)},
})
g.TrumpSet = true
g.TrumpSuit = Hearts
if err := g.PlayCard(c(Nine, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Nine, Spades)); err != ErrThousandInvalidFollow {
t.Errorf("при отсутствии масти, но наличии козыря, обязан ходить козырем, получено: %v", err)
}
if err := g.PlayCard(c(Jack, Hearts)); err != nil {
t.Fatalf("неожиданная ошибка при ходе козырем: %v", err)
}
}
func TestTrickWinnerHighestOfLedSuit(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(Jack, Clubs)},
1: {c(King, Clubs)},
2: {c(Nine, Clubs)},
})
if err := g.PlayCard(c(Jack, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(King, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Nine, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.LeaderIdx != 1 {
t.Errorf("ожидался победитель взятки — игрок 1 (король треф), получено %d", g.LeaderIdx)
}
if g.Players[1].HandPoints != 6 {
t.Errorf("ожидалось 6 очков за взятку, получено %d", g.Players[1].HandPoints)
}
}
func TestTenBeatsKingButLosesToAce(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(King, Clubs)},
1: {c(Ten, Clubs)},
2: {c(Ace, Clubs)},
})
if err := g.PlayCard(c(King, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Ace, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.LeaderIdx != 2 {
t.Errorf("туз должен побить и десятку, и короля; ожидался победитель 2, получено %d", g.LeaderIdx)
}
}
func TestTrumpBeatsNonTrumpEvenIfLower(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(Ace, Clubs)},
1: {c(Nine, Hearts), c(Nine, Spades)},
2: {c(Queen, Diamonds), c(Nine, Diamonds)},
})
g.TrumpSet = true
g.TrumpSuit = Hearts
if err := g.PlayCard(c(Ace, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Nine, Hearts)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Nine, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.LeaderIdx != 1 {
t.Errorf("младший козырь должен побить туза не в масть, ожидался победитель 1, получено %d", g.LeaderIdx)
}
}
func TestMarriageDeclarationSetsTrumpAndScores(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(Nine, Clubs), c(King, Diamonds), c(Queen, Diamonds)},
1: {c(Jack, Clubs)},
2: {c(Ten, Clubs)},
})
if err := g.PlayCard(c(Nine, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Jack, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.TricksPlayed != 1 {
t.Fatalf("ожидалась 1 сыгранная взятка, получено %d", g.TricksPlayed)
}
winner := g.LeaderIdx
if winner == 0 {
if err := g.PlayCard(c(King, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка объявления марьяжа: %v", err)
}
if !g.TrumpSet || g.TrumpSuit != Diamonds {
t.Fatalf("бубновый марьяж должен был назначить козырь бубны")
}
if g.Players[0].HandPoints < 80 {
t.Errorf("ожидались очки марьяжа (80) в счёте игрока 0, получено %d", g.Players[0].HandPoints)
}
}
}
func TestMarriageCannotBeDeclaredOnFirstTrick(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(King, Diamonds), c(Queen, Diamonds), c(Nine, Clubs)},
1: {c(Nine, Hearts)},
2: {c(Nine, Spades)},
})
if err := g.PlayCard(c(King, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.TrumpSet {
t.Errorf("марьяж не должен объявляться на самой первой взятке раздачи")
}
}
func TestHandScoringDeclarerMakesBid(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 110
g.Players[1].HandPoints = 5
g.Players[2].HandPoints = 5
g.resolveHandEnd()
if !g.Result.Made {
t.Fatalf("объявивший торг должен был выполнить ставку")
}
if g.Result.ScoreDeltas[0] != 100 {
t.Errorf("выполнившему ставку засчитывается сама ставка (100), получено %d", g.Result.ScoreDeltas[0])
}
if g.Players[0].Score != 100 {
t.Errorf("ожидался счёт 100, получено %d", g.Players[0].Score)
}
}
func TestHandScoringDeclarerFailsBid(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 60
g.Players[1].HandPoints = 40
g.Players[2].HandPoints = 20
g.resolveHandEnd()
if g.Result.Made {
t.Fatalf("не должен был выполнить ставку")
}
if g.Result.ScoreDeltas[0] != -100 {
t.Errorf("невыполнившему ставку списывается ставка (-100), получено %d", g.Result.ScoreDeltas[0])
}
if g.Result.ScoreDeltas[1] != 40 || g.Result.ScoreDeltas[2] != 20 {
t.Errorf("соперники должны получить ровно свои очки: %v", g.Result.ScoreDeltas)
}
}
func TestMatchOverAt1000(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.Players[0].Score = 920
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 100
g.Players[1].HandPoints = 0
g.Players[2].HandPoints = 0
g.resolveHandEnd()
if !g.Result.MatchOver {
t.Fatalf("партия должна была завершиться при достижении 1000 очков")
}
if g.Result.MatchWinner != 0 {
t.Errorf("ожидался победитель 0, получено %d", g.Result.MatchWinner)
}
}
func TestNextHandRotatesDealer(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
oldDealer := g.DealerIdx
g.NextHand()
if g.DealerIdx != (oldDealer+1)%3 {
t.Errorf("сдающий должен был смениться по кругу")
}
if g.Phase != ThousandPhaseBidding {
t.Errorf("новая раздача должна была начаться с торгов")
}
}