package main import ( "math/rand" "testing" ) func playNardyFullGame(t *testing.T, g *NardyGameState, botWhite, botBlack NardyBot, rnd *rand.Rand, maxTurns int) { t.Helper() for turn := 0; turn < maxTurns && g.Phase != NardyPhaseOver; turn++ { var bot NardyBot if g.CurrentPlayer == NardyWhite { bot = botWhite } else { bot = botBlack } if err := bot.PlayFullTurn(g, rnd); err != nil { t.Fatalf("ход %d: неожиданная ошибка бота: %v", turn, err) } } if g.Phase != NardyPhaseOver { t.Fatalf("партия не завершилась за %d ходов — похоже на зависание. BorneOff=%v Bar=%v", maxTurns, g.BorneOff, g.Bar) } } func TestNardyPipCountBasic(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[0] = NardyPoint{Owner: NardyWhite, Count: 1} // rel=0 -> 1 очко g.Board[5] = NardyPoint{Owner: NardyWhite, Count: 2} // rel=5 -> 6 очков каждая = 12 if got := nardyPipCount(g, NardyWhite); got != 13 { t.Errorf("ожидался pip count 13 (1+6+6), получено %d", got) } } func TestNardyPipCountIncludesBar(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Bar[NardyWhite] = 1 if got := nardyPipCount(g, NardyWhite); got != 25 { t.Errorf("шашка на баре должна считаться как 25 очков, получено %d", got) } } func TestMediumBotPrefersHitWhenEquallyExposedOtherwise(t *testing.T) { rnd := rand.New(rand.NewSource(1)) g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} // единственная шашка белых способна пойти либо на открытую // пустую точку (тоже остаётся одиноким блотом), либо на точку с // блотом чёрных (тоже остаётся блотом, но со сбитой шашкой // соперника) — оба варианта одинаково уязвимы сами по себе, // значит удар должен явно перевешивать по выгоде g.Board[10] = NardyPoint{Owner: NardyWhite, Count: 1} g.Board[5] = NardyPoint{Owner: NardyBlack, Count: 1} // блот чёрных, досягаем костью 5 g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{5} bot := NardyBot{Difficulty: NardyDifficultyMedium} seq := bot.DecideSequence(g, rnd) if len(seq) != 1 || seq[0].From != 10 || seq[0].Die != 5 { t.Errorf("при прочих равных средний уровень должен был предпочесть удар, получено %+v", seq) } } func TestNardyEvaluateRewardsHittingAllElseEqual(t *testing.T) { setup := func() *NardyGameState { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} return g } // сценарий А: белая шашка одна на точке 5 (блот) — просто дошла туда без удара gNoHit := setup() gNoHit.Board[5] = NardyPoint{Owner: NardyWhite, Count: 1} scoreNoHit := nardyEvaluate(gNoHit, NardyWhite) // сценарий Б: та же позиция, но на баре у чёрных сидит сбитая // шашка (как будто именно она стояла на точке 5 и была сбита) — // изолированно проверяем эффект наличия чужой шашки на баре gHit := setup() gHit.Board[5] = NardyPoint{Owner: NardyWhite, Count: 1} gHit.Bar[NardyBlack] = 1 scoreHit := nardyEvaluate(gHit, NardyWhite) if scoreHit <= scoreNoHit { t.Errorf("наличие шашки соперника на баре должно было увеличивать оценку позиции для белых: без удара=%v, с шашкой соперника на баре=%v", scoreNoHit, scoreHit) } } func TestHardBotSequenceUsesMaximalDice(t *testing.T) { rnd := rand.New(rand.NewSource(1)) g := NewNardyGame(NardyShort, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyHard} g.Roll(6, 4) needed := g.nardyMaxDiceUsableFrom(g.DiceLeft) seq := bot.DecideSequence(g, rnd) if len(seq) != needed { t.Errorf("последовательность жёсткого уровня должна была использовать максимум костей (%d), получено %d ходов: %+v", needed, len(seq), seq) } } func TestEasyBotSequenceOnlyLegalMoves(t *testing.T) { rnd := rand.New(rand.NewSource(1)) g := NewNardyGame(NardyLong, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyEasy} g.Roll(3, 5) seq := bot.DecideSequence(g, rnd) state := g.cloneForSearch() for _, mv := range seq { legal := state.LegalMovesNow() found := false for _, l := range legal { if l == mv { found = true } } if !found { t.Fatalf("ход %+v не входит в число легальных на этом шаге: %+v", mv, legal) } state.applyMoveRaw(mv) state.removeDieOnce(mv.Die) } } func TestPlayFullTurnRollsWhenNeeded(t *testing.T) { rnd := rand.New(rand.NewSource(1)) g := NewNardyGame(NardyShort, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyEasy} if g.Phase != NardyPhaseRoll { t.Fatalf("новая партия должна начинаться в фазе броска") } if err := bot.PlayFullTurn(g, rnd); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.CurrentPlayer != NardyBlack { t.Errorf("после полного хода бота очередь должна была перейти к чёрным") } } func TestNardyBotFullGamesShortEasyVsEasy(t *testing.T) { rnd := rand.New(rand.NewSource(1)) for i := 0; i < 5; i++ { g := NewNardyGame(NardyShort, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyEasy} playNardyFullGame(t, g, bot, bot, rnd, 2000) } } func TestNardyBotFullGamesShortMediumVsMedium(t *testing.T) { rnd := rand.New(rand.NewSource(1)) for i := 0; i < 5; i++ { g := NewNardyGame(NardyShort, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyMedium} playNardyFullGame(t, g, bot, bot, rnd, 2000) } } func TestNardyBotFullGamesShortHardVsHard(t *testing.T) { rnd := rand.New(rand.NewSource(1)) for i := 0; i < 3; i++ { g := NewNardyGame(NardyShort, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyHard} playNardyFullGame(t, g, bot, bot, rnd, 2000) } } func TestNardyBotFullGamesLongEasyVsEasy(t *testing.T) { rnd := rand.New(rand.NewSource(1)) for i := 0; i < 5; i++ { g := NewNardyGame(NardyLong, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyEasy} playNardyFullGame(t, g, bot, bot, rnd, 2000) } } func TestNardyBotFullGamesLongMediumVsMedium(t *testing.T) { rnd := rand.New(rand.NewSource(1)) for i := 0; i < 5; i++ { g := NewNardyGame(NardyLong, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyMedium} playNardyFullGame(t, g, bot, bot, rnd, 2000) } } func TestNardyBotFullGamesLongHardVsHard(t *testing.T) { rnd := rand.New(rand.NewSource(1)) for i := 0; i < 3; i++ { g := NewNardyGame(NardyLong, NardyWhite) bot := NardyBot{Difficulty: NardyDifficultyHard} playNardyFullGame(t, g, bot, bot, rnd, 2000) } } func TestNardyBotFullGamesMixedDifficulties(t *testing.T) { rnd := rand.New(rand.NewSource(1)) combos := []struct { white, black NardyDifficulty }{ {NardyDifficultyEasy, NardyDifficultyMedium}, {NardyDifficultyMedium, NardyDifficultyHard}, {NardyDifficultyEasy, NardyDifficultyHard}, } for _, variant := range []NardyVariant{NardyShort, NardyLong} { for _, combo := range combos { g := NewNardyGame(variant, NardyWhite) playNardyFullGame(t, g, NardyBot{Difficulty: combo.white}, NardyBot{Difficulty: combo.black}, rnd, 2000) } } }