go_games_collection/tictactoe_bot_test.go

159 lines
6.5 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 (
"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)
}
}