go_games_collection/game_test.go

361 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 TestNewGameDealsCorrectly(t *testing.T) {
g := NewGame([]string{"A", "B", "C"}, -1)
if len(g.Players) != 3 {
t.Fatalf("ожидалось 3 игрока, получено %d", len(g.Players))
}
for _, p := range g.Players {
if len(p.Hand) != 5 {
t.Errorf("у игрока %s ожидалось 5 карт, получено %d", p.Name, len(p.Hand))
}
}
if _, ok := g.PeekDiscard(); !ok {
t.Error("ожидалась вскрытая карта в сбросе после раздачи")
}
// 3*5 карт игрокам + 1 в сброс = 16, в колоде должно остаться 36
if g.Deck.Len() != 52-15-1 {
t.Errorf("неверное количество карт в колоде: %d", g.Deck.Len())
}
}
func TestDrawDiscardCycle(t *testing.T) {
g := NewGame([]string{"A", "B"}, -1)
handBefore := len(g.current().Hand)
if _, err := g.DrawFromDeck(); err != nil {
t.Fatalf("неожиданная ошибка при взятии карты: %v", err)
}
if len(g.current().Hand) != handBefore+1 {
t.Fatalf("рука должна была вырасти на 1 карту")
}
if g.Phase != PhaseAction {
t.Fatalf("после взятия карты ожидалась фаза PhaseAction, получена %v", g.Phase)
}
card := g.current().Hand[0]
if err := g.Discard(card); err != nil {
t.Fatalf("неожиданная ошибка при сбросе: %v", err)
}
if g.CurrentPlayer != 1 {
t.Fatalf("ход должен был перейти ко второму игроку, текущий: %d", g.CurrentPlayer)
}
if g.Phase != PhaseDraw {
t.Fatalf("после сброса ожидалась фаза PhaseDraw, получена %v", g.Phase)
}
}
func TestWrongPhaseRejected(t *testing.T) {
g := NewGame([]string{"A", "B"}, -1)
// ещё не брали карту — сброс должен быть запрещён
if err := g.Discard(g.current().Hand[0]); err != ErrWrongPhase {
t.Errorf("ожидалась ошибка ErrWrongPhase, получено: %v", err)
}
// повторное взятие карты в фазе PhaseDraw -> PhaseAction, затем
// повторный DrawFromDeck должен быть отклонён
if _, err := g.DrawFromDeck(); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if _, err := g.DrawFromDeck(); err != ErrWrongPhase {
t.Errorf("повторное взятие карты в фазе PhaseAction должно быть запрещено, получено: %v", err)
}
}
func TestEndByEmptyHandTakesPot(t *testing.T) {
g := NewGameWithStakes([]string{"A", "B", "C"}, -1, 100, 10, 2)
// банк уже собран в NewGameWithStakes: 3 игрока * 10 = 30
if g.Pot != 30 {
t.Fatalf("ожидался банк 30 после сбора анте, получено %d", g.Pot)
}
for _, p := range g.Players {
if p.Balance != 90 {
t.Fatalf("ожидался капитал 90 после анте, получено %d у %s", p.Balance, p.Name)
}
}
g.Players[0].Hand = []Card{c(Five, Hearts)}
g.CurrentPlayer = 0
g.Phase = PhaseAction
if err := g.Discard(c(Five, Hearts)); err != nil {
t.Fatalf("неожиданная ошибка при сбросе: %v", err)
}
if g.Phase != PhaseGameOver {
t.Fatalf("раунд должен был завершиться победой игрока 0")
}
if g.Result.Winner != 0 {
t.Fatalf("ожидался победитель 0, получен %d", g.Result.Winner)
}
if g.Result.Pot != 30 {
t.Fatalf("ожидался банк раунда 30, получено %d", g.Result.Pot)
}
// победитель: было 90, получил банк 30 -> 120; итоговое изменение
// относительно капитала ДО анте (100): +20
if g.Players[0].Balance != 120 {
t.Errorf("ожидался капитал победителя 120, получено %d", g.Players[0].Balance)
}
if g.Result.BalanceDeltas[0] != 20 {
t.Errorf("ожидалось изменение капитала +20 у победителя, получено %d", g.Result.BalanceDeltas[0])
}
for i := 1; i < 3; i++ {
if g.Players[i].Balance != 90 {
t.Errorf("проигравший %d должен остаться с капиталом 90, получено %d", i, g.Players[i].Balance)
}
if g.Result.BalanceDeltas[i] != -10 {
t.Errorf("проигравший %d должен потерять анте (-10), получено %d", i, g.Result.BalanceDeltas[i])
}
}
}
func TestDeadRoundRefundsAnte(t *testing.T) {
g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
g.endDeadRound()
if !g.Result.DeadRound {
t.Fatalf("ожидался DeadRound=true")
}
for i, p := range g.Players {
if p.Balance != 100 {
t.Errorf("капитал игрока %d должен был вернуться к 100, получено %d", i, p.Balance)
}
if g.Result.BalanceDeltas[i] != 0 {
t.Errorf("изменение капитала при мёртвом раунде должно быть 0, получено %d", g.Result.BalanceDeltas[i])
}
}
}
func TestDropSucceedsWhenStrictlyLowest(t *testing.T) {
g := NewGameWithStakes([]string{"A", "B", "C"}, -1, 100, 10, 2)
// банк = 30 (3*10), капитал каждого сейчас 90
g.Players[0].Hand = []Card{c(Two, Clubs)} // вес 2 — строго ниже всех
g.Players[1].Hand = []Card{c(King, Hearts), c(Nine, Diamonds)} // вес 14
g.Players[2].Hand = []Card{c(Queen, Spades)} // вес 10
g.CurrentPlayer = 0
g.Phase = PhaseDraw
res, err := g.Drop()
if err != nil {
t.Fatalf("неожиданная ошибка дропа: %v", err)
}
if !res.DropCall || res.DropCaught {
t.Fatalf("ожидался успешный дроп: %+v", res)
}
if res.Winner != 0 {
t.Errorf("ожидался победитель 0, получено %d", res.Winner)
}
// объявивший забирает банк целиком (без множителя): 90+30=120,
// дельта +20 относительно стартовых 100
if g.Players[0].Balance != 120 {
t.Errorf("ожидался капитал 120 у объявившего, получено %d", g.Players[0].Balance)
}
if g.Players[1].Balance != 90 || g.Players[2].Balance != 90 {
t.Errorf("остальные должны были просто остаться при своём анте (90), получено %d и %d",
g.Players[1].Balance, g.Players[2].Balance)
}
if res.BalanceDeltas[0] != 20 || res.BalanceDeltas[1] != -10 || res.BalanceDeltas[2] != -10 {
t.Errorf("неверный расчёт изменения капитала при успешном дропе: %v", res.BalanceDeltas)
}
}
func TestDropCaughtByLowerOpponent(t *testing.T) {
g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
// банк = 20, капитал каждого сейчас 90
g.Players[0].Hand = []Card{c(King, Hearts)} // вес 4 — думает, что мало
g.Players[1].Hand = []Card{c(Ace, Clubs)} // вес 1 — реально ниже
g.CurrentPlayer = 0
g.Phase = PhaseDraw
res, err := g.Drop()
if err != nil {
t.Fatalf("неожиданная ошибка дропа: %v", err)
}
if !res.DropCall || !res.DropCaught {
t.Fatalf("ожидался неудачный дроп (попался): %+v", res)
}
if res.Winner != 1 {
t.Errorf("ожидался победитель 1 (реально ниже), получено %d", res.Winner)
}
// игрок 1 получает банк (20) плюс штраф с объявившего (анте=10):
// 90+20+10=120, дельта +20
if g.Players[1].Balance != 120 {
t.Errorf("ожидался капитал 120 у поймавшего, получено %d", g.Players[1].Balance)
}
// объявивший теряет анте (уже потрачено) и вдобавок платит штраф
// в размере анте: 90-10=80, дельта -20 относительно стартовых 100
if g.Players[0].Balance != 80 {
t.Errorf("ожидался капитал 80 у объявившего (попался), получено %d", g.Players[0].Balance)
}
if res.BalanceDeltas[0] != -20 || res.BalanceDeltas[1] != 20 {
t.Errorf("неверный расчёт изменения капитала при неудачном дропе: %v", res.BalanceDeltas)
}
}
func TestDropTieWithAnotherPlayerCountsAsCaught(t *testing.T) {
g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
g.Players[0].Hand = []Card{c(Ace, Clubs)} // вес 1
g.Players[1].Hand = []Card{c(Ace, Hearts)} // тоже вес 1 — ничья не в пользу объявившего
g.CurrentPlayer = 0
g.Phase = PhaseDraw
res, err := g.Drop()
if err != nil {
t.Fatalf("неожиданная ошибка дропа: %v", err)
}
if !res.DropCaught {
t.Errorf("при равенстве весов дроп должен считаться неудачным (нужно строго меньше всех)")
}
if res.Winner != 1 {
t.Errorf("ожидался победитель 1, получено %d", res.Winner)
}
}
func TestDropOnlyAllowedBeforeDrawing(t *testing.T) {
g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
g.CurrentPlayer = 0
g.Phase = PhaseAction // уже взяли карту — дропать поздно
if _, err := g.Drop(); err != ErrWrongPhase {
t.Errorf("ожидалась ошибка ErrWrongPhase, получено: %v", err)
}
}
func TestDropZeroSumAcrossManyPlayers(t *testing.T) {
g := NewGameWithStakes([]string{"A", "B", "C", "D"}, -1, 100, 7, 2)
g.Players[0].Hand = []Card{c(Five, Clubs)}
g.Players[1].Hand = []Card{c(Three, Hearts)}
g.Players[2].Hand = []Card{c(Three, Spades)} // ничья с игроком 1 за самую низкую
g.Players[3].Hand = []Card{c(King, Diamonds)}
g.CurrentPlayer = 0
g.Phase = PhaseDraw
res, err := g.Drop()
if err != nil {
t.Fatalf("неожиданная ошибка дропа: %v", err)
}
sum := 0
for _, d := range res.BalanceDeltas {
sum += d
}
if sum != 0 {
t.Errorf("сумма изменений капитала при дропе должна быть 0, получено %d (%v)", sum, res.BalanceDeltas)
}
}
func TestDeclareTonk(t *testing.T) {
g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
// банк = 20 (2 игрока * 10), капитал каждого сейчас 90
g.Players[0].Hand = []Card{c(Ace, Hearts), c(Two, Clubs)} // вес 3 <= порога
g.CurrentPlayer = 0
g.Phase = PhaseAction
res, err := g.DeclareTonk()
if err != nil {
t.Fatalf("неожиданная ошибка объявления Тонка: %v", err)
}
if !res.TonkCall || res.Winner != 0 {
t.Fatalf("ожидался Тонк от игрока 0: %+v", res)
}
// банк ×2 = 40; победитель получает 40 (90+40=130, дельта +30
// относительно стартовых 100); единственный проигравший
// доплачивает разницу (extra=20) сверх уже отданного анте:
// 90-20=70, дельта -30 относительно стартовых 100
if g.Players[0].Balance != 130 {
t.Errorf("ожидался капитал победителя 130, получено %d", g.Players[0].Balance)
}
if g.Players[1].Balance != 70 {
t.Errorf("ожидался капитал проигравшего 70, получено %d", g.Players[1].Balance)
}
if res.BalanceDeltas[0] != 30 || res.BalanceDeltas[1] != -30 {
t.Errorf("неверный расчёт изменения капитала при Тонке: %v", res.BalanceDeltas)
}
}
func TestDeclareTonkRejectedAboveThreshold(t *testing.T) {
g := NewGame([]string{"A", "B"}, -1)
g.Players[0].Hand = []Card{c(King, Hearts), c(Nine, Clubs)} // вес 19
g.CurrentPlayer = 0
g.Phase = PhaseAction
if _, err := g.DeclareTonk(); err != ErrTonkNotAllowed {
t.Errorf("ожидалась ошибка ErrTonkNotAllowed, получено: %v", err)
}
}
func TestLayNewMeldAndAddToMeld(t *testing.T) {
g := NewGame([]string{"A", "B"}, -1)
g.Players[0].Hand = []Card{c(Seven, Clubs), c(Seven, Hearts), c(Seven, Spades), c(Two, Diamonds)}
g.CurrentPlayer = 0
g.Phase = PhaseAction
set := []Card{c(Seven, Clubs), c(Seven, Hearts), c(Seven, Spades)}
if err := g.LayNewMeld(set); err != nil {
t.Fatalf("неожиданная ошибка выкладки сета: %v", err)
}
if len(g.Table) != 1 {
t.Fatalf("ожидалась 1 комбинация на столе, получено %d", len(g.Table))
}
if len(g.current().Hand) != 1 {
t.Fatalf("после выкладки в руке должна остаться 1 карта, получено %d", len(g.current().Hand))
}
// второй игрок подкладывает четвёртую семёрку
g.Players[1].Hand = []Card{c(Seven, Diamonds)}
g.CurrentPlayer = 1
g.Phase = PhaseAction
if err := g.AddToMeld(0, c(Seven, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка подкладки: %v", err)
}
if len(g.Table[0].Cards) != 4 {
t.Errorf("ожидалось 4 карты в комбинации после подкладки, получено %d", len(g.Table[0].Cards))
}
}
// TestFullBotGames прогоняет много полных партий силами ботов всех
// трёх уровней в разных сочетаниях и проверяет, что игра всегда
// корректно завершается (без паники и без зависания) и что очки
// нулевой суммы (кроме мёртвых раундов).
func TestFullBotGames(t *testing.T) {
configs := [][]BotStrategy{
{SimpleBot{}, SimpleBot{}, SimpleBot{}, SimpleBot{}},
{MediumBot{}, MediumBot{}, MediumBot{}, MediumBot{}},
{TonkBot{}, TonkBot{}, TonkBot{}, TonkBot{}},
{SimpleBot{}, MediumBot{}, TonkBot{}, SimpleBot{}},
}
for cfgIdx, bots := range configs {
for game := 0; game < 20; game++ {
names := make([]string, len(bots))
for i := range names {
names[i] = "Bot"
}
g := NewGame(names, -1)
const maxTurns = 1000
turn := 0
for ; turn < maxTurns && g.Phase != PhaseGameOver; turn++ {
bot := bots[g.CurrentPlayer]
if err := bot.PlayTurn(g); err != nil {
t.Fatalf("конфиг %d, игра %d: ошибка на ходу бота %d: %v",
cfgIdx, game, g.CurrentPlayer, err)
}
}
if turn >= maxTurns {
t.Fatalf("конфиг %d, игра %d: партия не завершилась за %d ходов (возможное зависание)",
cfgIdx, game, maxTurns)
}
if g.Result == nil {
t.Fatalf("конфиг %d, игра %d: партия завершилась без результата", cfgIdx, game)
}
sum := 0
for _, d := range g.Result.BalanceDeltas {
sum += d
}
if sum != 0 {
t.Errorf("конфиг %d, игра %d: сумма изменений капитала должна быть 0, получено %d (%v)",
cfgIdx, game, sum, g.Result.BalanceDeltas)
}
}
}
}