244 lines
9.9 KiB
Go
244 lines
9.9 KiB
Go
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 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)
|
||
}
|
||
}
|
||
}
|
||
}
|