go_games_collection/thousand_game_test.go

773 lines
29 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 "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 TestAceMarriageDeclaredAfterTrickAdds200Points(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(Nine, Clubs), c(Ace, Diamonds)},
1: {c(Jack, Clubs), c(Nine, Hearts)},
2: {c(Ten, Clubs), c(Nine, Spades)},
})
// добавляем оставшиеся три туза на руку игрока 0 — итого все четыре
g.Players[0].Hand = append(g.Players[0].Hand, c(Ace, Clubs), c(Ace, Hearts), c(Ace, Spades))
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.LeaderIdx != 2 {
t.Fatalf("тест предполагает, что первую взятку берёт игрок 2 (десятка старше валета и девятки)")
}
// взятку взял игрок 2, значит у игрока 0 тузовый марьяж объявить
// пока нельзя (взятку должен был взять он сам) — переигрываем со
// взяткой, которую забирает сам игрок 0
g2 := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g2, 0, map[int][]Card{
0: {c(Ten, Clubs), c(Ace, Diamonds), c(Ace, Clubs), c(Ace, Hearts), c(Ace, Spades)},
1: {c(Nine, Hearts)},
2: {c(Nine, Spades)},
})
if err := g2.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g2.PlayCard(c(Nine, Hearts)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g2.PlayCard(c(Nine, Spades)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g2.LeaderIdx != 0 {
t.Fatalf("тест предполагает, что взятку забирает сам игрок 0 (десятка старше девяток)")
}
if err := g2.PlayCard(c(Ace, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка объявления тузового марьяжа: %v", err)
}
if g2.Players[0].HandMarriagePoints != ThousandAceMarriageValue {
t.Errorf("ожидалось %d очков тузового марьяжа, получено %d", ThousandAceMarriageValue, g2.Players[0].HandMarriagePoints)
}
}
func TestAceMarriageCannotBeDeclaredOnFirstTrick(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(Ace, Diamonds), c(Ace, Clubs), c(Ace, Hearts), c(Ace, Spades)},
1: {c(Nine, Hearts)},
2: {c(Nine, Spades)},
})
if err := g.PlayCard(c(Ace, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.Players[0].HandMarriagePoints != 0 {
t.Errorf("тузовый марьяж не должен объявляться на самой первой взятке раздачи")
}
}
func TestAceMarriageDoesNotChangeTrump(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
setupThousandTrickPlay(g, 0, map[int][]Card{
0: {c(Ten, Clubs), c(Ace, Diamonds), c(Ace, Clubs), c(Ace, Hearts), c(Ace, Spades)},
1: {c(Nine, Hearts)},
2: {c(Nine, Spades)},
})
g.PlayCard(c(Ten, Clubs))
g.PlayCard(c(Nine, Hearts))
g.PlayCard(c(Nine, Spades))
g.PlayCard(c(Ace, Diamonds))
if g.TrumpSet {
t.Errorf("тузовый марьяж не должен назначать козырь (в отличие от обычного марьяжа)")
}
}
func TestMaxBidIncludesAceMarriage(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.Players[g.CurrentBidderIdx].Hand = []Card{
c(Ace, Clubs), c(Ace, Diamonds), c(Ace, Hearts), c(Ace, Spades),
c(Nine, Clubs), c(Nine, Diamonds), c(Nine, Hearts),
}
want := 120 + ThousandAceMarriageValue
if got := g.maxBidFor(g.CurrentBidderIdx); got != want {
t.Errorf("ожидался потолок ставки %d (120+тузовый), получено %d", want, got)
}
}
// --- Золотой кон ------------------------------------------------------
func TestGoldenRoundDoublesNonDeclarerTrickPoints(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = 1 // первая раздача матча — золотой кон
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 100
g.Players[1].HandPoints = 30
g.resolveHandEnd()
if g.Result.ScoreDeltas[1] != 60 {
t.Errorf("очки соперника должны были удвоиться в золотом коне: ожидалось 60, получено %d", g.Result.ScoreDeltas[1])
}
}
func TestGoldenRoundDoesNotDoubleMarriagePoints(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = 1
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 100
g.Players[1].HandPoints = 0
g.Players[1].HandMarriagePoints = 40
g.resolveHandEnd()
if g.Result.ScoreDeltas[1] != 40 {
t.Errorf("марьяжные очки не должны удваиваться золотым коном: ожидалось 40, получено %d", g.Result.ScoreDeltas[1])
}
}
func TestGoldenRoundDoesNotAffectDeclarer(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = 1
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 100
g.resolveHandEnd()
if g.Result.ScoreDeltas[0] != 100 {
t.Errorf("золотой кон не удваивает собственную заявку объявившего, ожидалось 100, получено %d", g.Result.ScoreDeltas[0])
}
}
func TestGoldenRoundEndsAfterThreeHands(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 100
g.Players[1].HandPoints = 30
g.resolveHandEnd()
if g.Result.ScoreDeltas[1] != 30 {
t.Errorf("после золотого кона очки не должны удваиваться, ожидалось 30, получено %d", g.Result.ScoreDeltas[1])
}
}
// --- Игра втёмную -------------------------------------------------------
func TestDeclareBlindSetsDeclarerAndFixedBid(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
firstBidder := g.CurrentBidderIdx
if err := g.DeclareBlind(); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.DeclarerIdx != firstBidder {
t.Errorf("объявивший втёмную должен был стать declarer'ом")
}
if g.FinalBid != 120 {
t.Errorf("ожидалась фиксированная заявка 120, получено %d", g.FinalBid)
}
if !g.PlayedBlind {
t.Errorf("флаг PlayedBlind должен был выставиться")
}
if g.Phase != ThousandPhaseDiscard {
t.Errorf("после объявления втёмную должна начаться фаза сноса прикупа, получено %v", g.Phase)
}
}
func TestDeclareBlindOnlyAvailableBeforeAnyBid(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
if err := g.Bid(100); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := g.DeclareBlind(); err != ErrThousandBlindTooLate {
t.Errorf("ожидалась ошибка опоздания с объявлением втёмную, получено: %v", err)
}
}
func TestDeclareBlindDoublesDeclarerReward(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1 // вне золотого кона, чтобы не смешивать эффекты
g.DeclareBlind()
g.Phase = ThousandPhaseTrick
g.Players[g.DeclarerIdx].HandPoints = 120
g.resolveHandEnd()
if g.Result.ScoreDeltas[g.Result.DeclarerIdx] != 240 {
t.Errorf("успешная игра втёмную должна давать удвоенную награду (240), получено %d", g.Result.ScoreDeltas[g.Result.DeclarerIdx])
}
}
func TestDeclareBlindDoublesDeclarerPenalty(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.DeclareBlind()
g.Phase = ThousandPhaseTrick
g.Players[g.DeclarerIdx].HandPoints = 50 // меньше 120 — провал
g.resolveHandEnd()
if g.Result.ScoreDeltas[g.Result.DeclarerIdx] != -240 {
t.Errorf("провал игры втёмную должен давать удвоенный штраф (-240), получено %d", g.Result.ScoreDeltas[g.Result.DeclarerIdx])
}
}
func TestDeclareBlindDoublesOpponentTrickPoints(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
declarer := g.CurrentBidderIdx
g.DeclareBlind()
g.Phase = ThousandPhaseTrick
g.Players[declarer].HandPoints = 120
opponent := (declarer + 1) % 3
g.Players[opponent].HandPoints = 30
g.resolveHandEnd()
if g.Result.ScoreDeltas[opponent] != 60 {
t.Errorf("очки соперников должны удваиваться при игре втёмную: ожидалось 60, получено %d", g.Result.ScoreDeltas[opponent])
}
}
// --- Бочка --------------------------------------------------------------
func TestPlayerEntersBarrelAt880(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.Players[1].Score = 800
g.DeclarerIdx = 1
g.FinalBid = 100
g.Players[1].HandPoints = 100
g.resolveHandEnd()
if g.Players[1].Score != 900 {
t.Fatalf("ожидался счёт 900, получено %d", g.Players[1].Score)
}
if !g.Players[1].OnBarrel {
t.Errorf("игрок с 900 очками (>= 880) должен был сесть на бочку")
}
}
func TestOnlyOnePlayerOnBarrelAtOnce(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.Players[0].Score = 900
g.Players[0].OnBarrel = true
g.Players[1].Score = 800
g.DeclarerIdx = 1
g.FinalBid = 100
g.Players[1].HandPoints = 100
g.resolveHandEnd()
if g.Players[0].OnBarrel {
t.Errorf("прежний сидящий на бочке должен был слететь")
}
if g.Players[0].Score != 900-ThousandBarrelPenalty {
t.Errorf("слетевший с бочки должен был получить штраф %d, счёт: %d", ThousandBarrelPenalty, g.Players[0].Score)
}
if !g.Players[1].OnBarrel {
t.Errorf("новый игрок, достигший бочки, должен был на неё сесть")
}
}
func TestBarrelSitterGetsNoPointsAsNonDeclarer(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.Players[0].Score = 900
g.Players[0].OnBarrel = true
g.DeclarerIdx = 1
g.FinalBid = 100
g.Players[0].HandPoints = 50 // не объявлял в этой раздаче — очки не должны пойти на счёт
g.Players[1].HandPoints = 100
g.resolveHandEnd()
if g.Players[0].Score != 900 {
t.Errorf("сидящий на бочке, но не объявлявший, не должен получать очки за чужую раздачу, было 900, стало %d", g.Players[0].Score)
}
}
func TestBarrelSitterSuccessWinsMatch(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.Players[0].Score = 880
g.Players[0].OnBarrel = true
g.DeclarerIdx = 0
g.FinalBid = 120
g.Players[0].HandPoints = 120
g.resolveHandEnd()
if !g.Result.MatchOver || g.Result.MatchWinner != 0 {
t.Errorf("выполнивший заявку на бочке и пересёкший 1000 должен был выиграть матч")
}
}
func TestBarrelSitterDropsAfterThreeFailedAttempts(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.Players[0].Score = 880
g.Players[0].OnBarrel = true
g.DeclarerIdx = 0
g.FinalBid = 120
for attempt := 1; attempt <= ThousandBarrelMaxAttempts; attempt++ {
g.Players[0].HandPoints = 50 // провал
g.Phase = ThousandPhaseTrick
g.resolveHandEnd()
if attempt < ThousandBarrelMaxAttempts {
if g.Players[0].Score != 880 {
t.Fatalf("попытка %d: счёт не должен меняться при провале до исчерпания попыток, получено %d", attempt, g.Players[0].Score)
}
if !g.Players[0].OnBarrel {
t.Fatalf("попытка %d: игрок должен оставаться на бочке", attempt)
}
}
}
if g.Players[0].OnBarrel {
t.Errorf("после %d неудачных попыток игрок должен был слететь с бочки", ThousandBarrelMaxAttempts)
}
if g.Players[0].Score != 880-ThousandBarrelPenalty {
t.Errorf("ожидался штраф %d при слёте, счёт: %d", ThousandBarrelPenalty, g.Players[0].Score)
}
}
// --- Самосвал -------------------------------------------------------------
func TestSamosvalResetsAtExactly555(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.Players[0].Score = 455
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 100
g.resolveHandEnd()
if g.Players[0].Score != 0 {
t.Errorf("счёт ровно 555 должен был обнулиться, получено %d", g.Players[0].Score)
}
}
func TestSamosvalDoesNotTriggerNearButNotExact555(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.Players[0].Score = 450
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 100
g.resolveHandEnd()
if g.Players[0].Score != 550 {
t.Errorf("550 — не точное попадание в 555, обнуления быть не должно, получено %d", g.Players[0].Score)
}
}
func TestSamosvalResetsAtNegative555(t *testing.T) {
g := NewThousandGame([]string{"A", "B", "C"}, -1)
g.HandNumber = ThousandGoldenHands + 1
g.Players[0].Score = -455
g.DeclarerIdx = 0
g.FinalBid = 100
g.Players[0].HandPoints = 0 // провал заявки -> -100
g.resolveHandEnd()
if g.Players[0].Score != 0 {
t.Errorf("счёт ровно -555 должен был обнулиться, получено %d", g.Players[0].Score)
}
}
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.HandNumber = ThousandGoldenHands + 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("новая раздача должна была начаться с торгов")
}
}