773 lines
29 KiB
Go
773 lines
29 KiB
Go
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("новая раздача должна была начаться с торгов")
|
||
}
|
||
}
|