package main import "testing" func TestShortNardyStartingSetup(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) // белые: 2 на 23(abs), 5 на 12(abs), 3 на 7(abs), 5 на 5(abs) checkPoint := func(abs int, owner NardyPlayer, count int) { t.Helper() pt := g.Board[abs] if pt.Count != count || (count > 0 && pt.Owner != owner) { t.Errorf("точка %d: ожидалось %v x%d, получено %v x%d", abs, owner, count, pt.Owner, pt.Count) } } checkPoint(23, NardyWhite, 2) checkPoint(12, NardyWhite, 5) checkPoint(7, NardyWhite, 3) checkPoint(5, NardyWhite, 5) // чёрные — зеркально: rel 23 -> abs 23-23=0, rel12->abs11, rel7->abs16, rel5->abs18 checkPoint(0, NardyBlack, 2) checkPoint(11, NardyBlack, 5) checkPoint(16, NardyBlack, 3) checkPoint(18, NardyBlack, 5) total := 0 for _, pt := range g.Board { total += pt.Count } if total != 30 { t.Errorf("на доске должно быть 30 шашек суммарно, получено %d", total) } } func TestLongNardyStartingSetup(t *testing.T) { g := NewNardyGame(NardyLong, NardyWhite) if g.Board[23].Count != 15 || g.Board[23].Owner != NardyWhite { t.Errorf("все 15 белых шашек должны стоять на абсолютной точке 23, получено %+v", g.Board[23]) } // чёрные: rel23 -> abs (23+12)%24=11 if g.Board[11].Count != 15 || g.Board[11].Owner != NardyBlack { t.Errorf("все 15 чёрных шашек должны стоять на абсолютной точке 11, получено %+v", g.Board[11]) } } func TestCanLandShortNardyBlocksOnTwoOrMore(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[10] = NardyPoint{Owner: NardyBlack, Count: 2} if g.canLand(NardyWhite, 10) { t.Errorf("точка с двумя и более шашками соперника должна быть заблокирована") } } func TestCanLandShortNardyAllowsHittingBlot(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[10] = NardyPoint{Owner: NardyBlack, Count: 1} if !g.canLand(NardyWhite, 10) { t.Errorf("одинокую шашку соперника (блот) должно быть можно сбить") } } func TestCanLandLongNardyBlocksEvenSingleChecker(t *testing.T) { g := NewNardyGame(NardyLong, NardyWhite) g.Board = [24]NardyPoint{} g.Board[10] = NardyPoint{Owner: NardyBlack, Count: 1} if g.canLand(NardyWhite, 10) { t.Errorf("в длинных нардах даже одна шашка соперника должна блокировать точку полностью") } } func TestHittingSendsBlotToBar(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[15] = NardyPoint{Owner: NardyWhite, Count: 1} g.Board[10] = NardyPoint{Owner: NardyBlack, Count: 1} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{5} if err := g.ApplyMove(NardyMove{From: 15, Die: 5}); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Bar[NardyBlack] != 1 { t.Errorf("сбитая шашка чёрных должна была уйти на бар, Bar[Black]=%d", g.Bar[NardyBlack]) } if g.Board[10].Owner != NardyWhite || g.Board[10].Count != 1 { t.Errorf("на точке 10 теперь должна стоять белая шашка, получено %+v", g.Board[10]) } } func TestMustEnterFromBarBeforeOtherMoves(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[15] = NardyPoint{Owner: NardyWhite, Count: 1} g.Bar[NardyWhite] = 1 g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{5} moves := g.LegalMovesNow() for _, mv := range moves { if !mv.FromBar { t.Errorf("пока на баре есть шашка, разрешён только ход с бара, получен обычный ход %+v", mv) } } if len(moves) == 0 { t.Fatalf("ожидался хотя бы один легальный ход (вход с бара)") } } func TestBarEntryBlockedByOpponentPoint(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Bar[NardyWhite] = 1 entryAbs := 24 - 3 g.Board[entryAbs] = NardyPoint{Owner: NardyBlack, Count: 2} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{3} if err := g.ApplyMove(NardyMove{FromBar: true, Die: 3}); err != ErrNardyIllegalMove { t.Errorf("вход с бара на заблокированную точку должен быть отклонён, получено: %v", err) } } func TestBearOffRequiresAllCheckersHome(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[3] = NardyPoint{Owner: NardyWhite, Count: 1} g.Board[15] = NardyPoint{Owner: NardyWhite, Count: 1} // шашка ещё вне дома g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{4} moves := g.LegalMovesNow() for _, mv := range moves { if mv.BearOff { t.Errorf("снятие с доски не должно быть доступно, пока не все шашки дома") } } } func TestBearOffExactMatch(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[3] = NardyPoint{Owner: NardyWhite, Count: 1} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{4} if err := g.ApplyMove(NardyMove{From: 3, Die: 4, BearOff: true}); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.BorneOff[NardyWhite] != 1 { t.Errorf("ожидалось снятие одной шашки, BorneOff=%d", g.BorneOff[NardyWhite]) } } func TestBearOffExcessRejectedWithCheckerBeyond(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[2] = NardyPoint{Owner: NardyWhite, Count: 1} g.Board[4] = NardyPoint{Owner: NardyWhite, Count: 1} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{6} moves := g.LegalMovesNow() for _, mv := range moves { if mv.BearOff && mv.From == 2 { t.Errorf("избыточное снятие с точки 2 костью 6 не должно быть разрешено, пока есть шашка на точке 4 (дальше)") } } } func TestBearOffExcessAllowedWithoutCheckerBeyond(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[2] = NardyPoint{Owner: NardyWhite, Count: 1} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{6} if err := g.ApplyMove(NardyMove{From: 2, Die: 6, BearOff: true}); err != nil { t.Fatalf("избыточное снятие должно быть разрешено, если нет шашек дальше: %v", err) } if g.BorneOff[NardyWhite] != 1 { t.Errorf("ожидалось снятие шашки, получено BorneOff=%d", g.BorneOff[NardyWhite]) } } func TestOnlyOneCheckerFromHeadPerTurn(t *testing.T) { g := NewNardyGame(NardyLong, NardyWhite) g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{3, 4} moves := g.LegalMovesNow() if len(moves) == 0 { t.Fatalf("ожидался хотя бы один легальный ход") } if err := g.ApplyMove(moves[0]); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } for _, mv := range g.LegalMovesNow() { if mv.From == 23 { t.Errorf("вторая шашка с головы в одном ходу быть взята не должна") } } } func TestDoublesGiveFourDiceValues(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Roll(4, 4) if len(g.DiceLeft) != 4 { t.Fatalf("дубль должен давать 4 значения кости, получено %d: %v", len(g.DiceLeft), g.DiceLeft) } for _, d := range g.DiceLeft { if d != 4 { t.Errorf("все четыре значения должны быть равны 4, получено %v", g.DiceLeft) } } } func TestMustUseMaximumPossibleDice(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} // шашка на rel=10: ходом 5 попадает на rel=5 (открыто), ходом 3 — // на rel=7 (заблокировано двумя чёрными). Если сыграть 5 первой, // потом с rel=5 можно сыграть 3 -> rel=2 (открыто) = используются // ОБЕ кости. Если сыграть 3 первой — ход блокирован сразу. g.Board[10] = NardyPoint{Owner: NardyWhite, Count: 1} g.Board[7] = NardyPoint{Owner: NardyBlack, Count: 2} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{5, 3} moves := g.LegalMovesNow() if len(moves) != 1 || moves[0].Die != 5 || moves[0].From != 10 { t.Fatalf("единственный легальный первый ход должен быть 10->5 (используя большую кость 5), получено %+v", moves) } } func TestMustPlayLargerDieWhenOnlyOnePlayable(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[10] = NardyPoint{Owner: NardyWhite, Count: 1} g.Board[8] = NardyPoint{Owner: NardyBlack, Count: 2} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{5, 2} moves := g.LegalMovesNow() if len(moves) != 1 || moves[0].Die != 5 { t.Fatalf("должна была быть разрешена только большая кость (5), получено %+v", moves) } } func TestNoLegalMoveAtAll(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[10] = NardyPoint{Owner: NardyWhite, Count: 1} // единственная шашка g.Board[4] = NardyPoint{Owner: NardyBlack, Count: 2} // блокирует единственное направление (10-6=4) при дубле 6-6 g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{6, 6, 6, 6} moves := g.LegalMovesNow() if len(moves) != 0 { t.Errorf("ожидалось отсутствие легальных ходов, получено %+v", moves) } } func TestNoLegalMoveAfterRollEndsTurnImmediately(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[10] = NardyPoint{Owner: NardyWhite, Count: 1} g.Board[4] = NardyPoint{Owner: NardyBlack, Count: 2} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseRoll g.Roll(6, 6) if g.Phase != NardyPhaseRoll || g.CurrentPlayer != NardyBlack { t.Errorf("при отсутствии ходов очки должны были сгореть, а ход перейти к чёрным сразу; получено Phase=%v CurrentPlayer=%v", g.Phase, g.CurrentPlayer) } } func TestWinWhenAllCheckersBorneOff(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[0] = NardyPoint{Owner: NardyWhite, Count: 1} g.BorneOff[NardyWhite] = 14 g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{1} if err := g.ApplyMove(NardyMove{From: 0, Die: 1, BearOff: true}); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase != NardyPhaseOver || g.Result == nil || g.Result.Winner != NardyWhite { t.Fatalf("ожидалась победа белых, получено Phase=%v Result=%+v", g.Phase, g.Result) } } func TestTurnSwitchesPlayerWhenDiceExhausted(t *testing.T) { g := NewNardyGame(NardyShort, NardyWhite) g.Board = [24]NardyPoint{} g.Board[15] = NardyPoint{Owner: NardyWhite, Count: 1} g.CurrentPlayer = NardyWhite g.Phase = NardyPhaseMove g.DiceLeft = []int{5} if err := g.ApplyMove(NardyMove{From: 15, Die: 5}); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.CurrentPlayer != NardyBlack || g.Phase != NardyPhaseRoll { t.Errorf("после использования всех костей ход должен был перейти к чёрным (фаза броска), получено CurrentPlayer=%v Phase=%v", g.CurrentPlayer, g.Phase) } }