159 lines
6.5 KiB
Go
159 lines
6.5 KiB
Go
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)
|
||
}
|
||
}
|