package main import ( "math/rand" "testing" ) func TestEasyBotAlwaysMovesToEmptyCell(t *testing.T) { rnd := rand.New(rand.NewSource(1)) bot := TicTacToeBot{Difficulty: TicTacToeDifficultyEasy} g := NewTicTacToeGame(TicTacToeO) // человек — нолики, бот ходит крестиками первым for i := 0; i < 9 && g.Phase == TicTacToePhasePlay; i++ { pos := bot.DecideMove(g, rnd) if pos < 0 || pos > 8 || g.Board[pos] != TicTacToeEmpty { t.Fatalf("бот выбрал недопустимую клетку %d при доске %v", pos, g.Board) } g.PlaceMark(pos) } } func TestMediumBotTakesImmediateWin(t *testing.T) { rnd := rand.New(rand.NewSource(1)) bot := TicTacToeBot{Difficulty: TicTacToeDifficultyMedium} g := NewTicTacToeGame(TicTacToeO) // X уже имеет две подряд (0,1) — ход X, победный ход очевиден: клетка 2 g.Board[0] = TicTacToeX g.Board[1] = TicTacToeX g.Board[3] = TicTacToeO g.Board[4] = TicTacToeO g.CurrentTurn = TicTacToeX g.MovesPlayed = 4 pos := bot.DecideMove(g, rnd) if pos != 2 { t.Errorf("средний уровень должен был взять очевидную победу в клетке 2, получено %d", pos) } } func TestMediumBotBlocksImmediateLoss(t *testing.T) { rnd := rand.New(rand.NewSource(1)) bot := TicTacToeBot{Difficulty: TicTacToeDifficultyMedium} g := NewTicTacToeGame(TicTacToeX) // O имеет две подряд (3,4) и вот-вот выиграет в клетке 5; ходит O (бот) g.Board[3] = TicTacToeO g.Board[4] = TicTacToeO g.Board[0] = TicTacToeX g.Board[1] = TicTacToeX g.CurrentTurn = TicTacToeO g.MovesPlayed = 4 pos := bot.DecideMove(g, rnd) if pos != 5 { t.Errorf("средний уровень должен был заблокировать угрозу в клетке 5, получено %d", pos) } } // playTicTacToeToEnd доигрывает партию до конца, где botX ходит за // крестики, а botO — за нолики. func playTicTacToeToEnd(t *testing.T, g *TicTacToeGameState, rnd *rand.Rand, botX, botO TicTacToeBot) { t.Helper() for i := 0; i < 9 && g.Phase == TicTacToePhasePlay; i++ { var pos int if g.CurrentTurn == TicTacToeX { pos = botX.DecideMove(g, rnd) } else { pos = botO.DecideMove(g, rnd) } if pos < 0 { t.Fatalf("бот вернул недопустимый ход -1 при незавершённой игре, доска: %v", g.Board) } if err := g.PlaceMark(pos); err != nil { t.Fatalf("неожиданная ошибка хода: %v (доска: %v, позиция: %d)", err, g.Board, pos) } } if g.Phase != TicTacToePhaseOver { t.Fatalf("партия не завершилась за 9 ходов — это невозможно для крестиков-ноликов, доска: %v", g.Board) } } func TestHardBotNeverLosesAgainstAnyOpponent(t *testing.T) { hard := TicTacToeBot{Difficulty: TicTacToeDifficultyHard} // Средний и непобедимый уровни не используют rnd вообще (их // решение полностью детерминировано доской) — при одинаковой // стартовой раскладке разные seed дали бы совершенно одинаковую // партию, так что достаточно одного прогона на каждую раскладку. deterministic := []TicTacToeBot{ {Difficulty: TicTacToeDifficultyMedium}, {Difficulty: TicTacToeDifficultyHard}, } for _, opponent := range deterministic { rnd := rand.New(rand.NewSource(1)) g := NewTicTacToeGame(TicTacToeO) playTicTacToeToEnd(t, g, rnd, hard, opponent) if g.Result.Winner == TicTacToeO { t.Fatalf("непобедимый уровень (крестики) проиграл сопернику %v", opponent.Difficulty) } g2 := NewTicTacToeGame(TicTacToeX) playTicTacToeToEnd(t, g2, rnd, opponent, hard) if g2.Result.Winner == TicTacToeX { t.Fatalf("непобедимый уровень (нолики) проиграл сопернику %v", opponent.Difficulty) } } // Лёгкий уровень — единственный, где ход действительно случаен, // так что здесь имеет смысл перебрать много разных seed. easy := TicTacToeBot{Difficulty: TicTacToeDifficultyEasy} for seed := int64(0); seed < 500; seed++ { rnd := rand.New(rand.NewSource(seed)) g := NewTicTacToeGame(TicTacToeO) playTicTacToeToEnd(t, g, rnd, hard, easy) if g.Result.Winner == TicTacToeO { t.Fatalf("непобедимый уровень (крестики) проиграл случайному сопернику на seed %d", seed) } g2 := NewTicTacToeGame(TicTacToeX) playTicTacToeToEnd(t, g2, rnd, easy, hard) if g2.Result.Winner == TicTacToeX { t.Fatalf("непобедимый уровень (нолики) проиграл случайному сопернику на seed %d", seed) } } } func TestHardVsHardAlwaysDraws(t *testing.T) { hard := TicTacToeBot{Difficulty: TicTacToeDifficultyHard} rnd := rand.New(rand.NewSource(1)) g := NewTicTacToeGame(TicTacToeO) // оба хода в этом тесте делают боты, HumanMark тут не участвует playTicTacToeToEnd(t, g, rnd, hard, hard) if !g.Result.Draw { t.Fatalf("два непобедимых бота (полностью детерминированных) должны были сыграть вничью, получено Result=%+v", g.Result) } } func TestPlayFullTurnOnlyActsOnBotsTurn(t *testing.T) { rnd := rand.New(rand.NewSource(1)) bot := TicTacToeBot{Difficulty: TicTacToeDifficultyHard} g := NewTicTacToeGame(TicTacToeX) // человек играет крестиками — сейчас его ход if err := bot.PlayFullTurn(g, rnd); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.MovesPlayed != 0 { t.Errorf("бот не должен был ходить, пока сейчас ход человека") } } func TestPlayFullTurnActsWhenItsBotsTurn(t *testing.T) { rnd := rand.New(rand.NewSource(1)) bot := TicTacToeBot{Difficulty: TicTacToeDifficultyHard} g := NewTicTacToeGame(TicTacToeO) // человек — нолики, бот (крестики) ходит первым if err := bot.PlayFullTurn(g, rnd); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.MovesPlayed != 1 { t.Errorf("бот должен был сделать один ход, MovesPlayed=%d", g.MovesPlayed) } }