package main import "testing" func gp(r, c int) GoPos { return GoPos{Row: r, Col: c} } func TestNewGoGameEmpty(t *testing.T) { g := NewGoGame(9) if g.Size != 9 { t.Fatalf("ожидался размер доски 9, получено %d", g.Size) } if g.Turn != GoBlack { t.Errorf("первый ход должен быть за чёрными") } for r := 0; r < 9; r++ { for c := 0; c < 9; c++ { if g.Board[r][c] != GoEmpty { t.Fatalf("новая доска должна быть полностью пустой") } } } } func TestPlaySwitchesTurn(t *testing.T) { g := NewGoGame(9) if err := g.Play(gp(4, 4)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Board[4][4] != GoBlack { t.Errorf("камень должен был встать в (4,4) как чёрный") } if g.Turn != GoWhite { t.Errorf("ход должен был перейти к белым") } } func TestCannotPlayOnOccupiedPoint(t *testing.T) { g := NewGoGame(9) g.Play(gp(4, 4)) if err := g.Play(gp(4, 4)); err != ErrGoOccupied { t.Errorf("ожидалась ошибка занятой точки, получено: %v", err) } } func TestCannotPlayOutOfBounds(t *testing.T) { g := NewGoGame(9) if err := g.Play(gp(-1, 0)); err != ErrGoOutOfBounds { t.Errorf("ожидалась ошибка выхода за границы, получено: %v", err) } if err := g.Play(gp(9, 0)); err != ErrGoOutOfBounds { t.Errorf("ожидалась ошибка выхода за границы, получено: %v", err) } } func TestSingleStoneCapture(t *testing.T) { g := NewGoGame(9) g.Board[3][4] = GoBlack g.Board[5][4] = GoBlack g.Board[4][3] = GoBlack g.Board[4][4] = GoWhite g.Turn = GoBlack if err := g.Play(gp(4, 5)); err != nil { t.Fatalf("неожиданная ошибка взятия: %v", err) } if g.Board[4][4] != GoEmpty { t.Errorf("белый камень должен был быть взят") } if g.CapturedByBlack != 1 { t.Errorf("ожидалось 1 взятие у чёрных, получено %d", g.CapturedByBlack) } } func TestGroupCaptureRemovesWholeGroup(t *testing.T) { g := NewGoGame(9) g.Board[4][4] = GoWhite g.Board[4][5] = GoWhite g.Board[3][4] = GoBlack g.Board[3][5] = GoBlack g.Board[5][4] = GoBlack g.Board[5][5] = GoBlack g.Board[4][3] = GoBlack g.Turn = GoBlack if err := g.Play(gp(4, 6)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Board[4][4] != GoEmpty || g.Board[4][5] != GoEmpty { t.Errorf("вся группа должна была быть взята целиком") } if g.CapturedByBlack != 2 { t.Errorf("ожидалось 2 взятия, получено %d", g.CapturedByBlack) } } func TestSuicideMoveIsIllegal(t *testing.T) { g := NewGoGame(9) g.Board[3][4] = GoWhite g.Board[5][4] = GoWhite g.Board[4][3] = GoWhite g.Board[4][5] = GoWhite g.Turn = GoBlack if err := g.Play(gp(4, 4)); err != ErrGoSuicide { t.Errorf("ожидалась ошибка самоубийственного хода, получено: %v", err) } if g.Board[4][4] != GoEmpty { t.Errorf("доска не должна была измениться после отклонённого хода") } } func TestSuicideAllowedIfItCaptures(t *testing.T) { g := NewGoGame(9) // белый камень (4,4) окружён чёрными с трёх сторон, единственная // свобода — (4,5); чёрные играют туда — это взятие, а не // самоубийство их собственного камня g.Board[3][4] = GoBlack g.Board[5][4] = GoBlack g.Board[4][3] = GoBlack g.Board[4][4] = GoWhite g.Turn = GoBlack if err := g.Play(gp(4, 5)); err != nil { t.Fatalf("ход должен был пройти как взятие, а не самоубийство: %v", err) } if g.Board[4][4] != GoEmpty { t.Errorf("белый камень должен был быть взят") } if g.Board[4][5] != GoBlack { t.Errorf("чёрный камень должен был остаться на месте после взятия") } } func TestKoRuleForbidsImmediateRecapture(t *testing.T) { g := NewGoGame(9) // классическая форма ко (координаты row,col): // (0,1)=W (0,2)=B // (1,0)=W (1,1)=. (1,2)=W (1,3)=B <- белые только что взяли // одинокого чёрного в (1,1), // сыграв в (1,2) // (2,1)=W (2,2)=B // белый камень (1,2) сам стоит всего с одной свободой — (1,1) — // и чёрные не могут немедленно отыграть его обратно g.Board[0][1] = GoWhite g.Board[0][2] = GoBlack g.Board[1][0] = GoWhite g.Board[1][2] = GoWhite g.Board[1][3] = GoBlack g.Board[2][1] = GoWhite g.Board[2][2] = GoBlack // позиция ДО хода белых: на месте (1,2) было пусто, а на (1,1) // стоял тот самый одинокий чёрный камень, которого взяли before := cloneGoBoard(g.Board) before[1][1] = GoBlack before[1][2] = GoEmpty g.prevBoardBeforeOpponentMove = before g.Turn = GoBlack if err := g.Play(gp(1, 1)); err != ErrGoKoViolation { t.Errorf("ожидалась ошибка нарушения ко, получено: %v", err) } } func TestKoAllowedAfterPlayingElsewhere(t *testing.T) { g := NewGoGame(9) g.Board[0][1] = GoWhite g.Board[0][2] = GoBlack g.Board[1][0] = GoWhite g.Board[1][2] = GoWhite g.Board[1][3] = GoBlack g.Board[2][1] = GoWhite g.Board[2][2] = GoBlack before := cloneGoBoard(g.Board) before[1][1] = GoBlack before[1][2] = GoEmpty g.prevBoardBeforeOpponentMove = before g.Turn = GoBlack // чёрные играют "ходом ко" в другое место if err := g.Play(gp(7, 7)); err != nil { t.Fatalf("неожиданная ошибка хода в сторону: %v", err) } // теперь белые тоже играют куда-то ещё if err := g.Play(gp(0, 7)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } // а теперь чёрные вновь могут взять в (1,1), т.к. позиция уже не та if err := g.Play(gp(1, 1)); err != nil { t.Fatalf("после ходов в сторону взятие должно быть разрешено: %v", err) } } func TestPassTwiceEndsGame(t *testing.T) { g := NewGoGame(9) if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Result != nil { t.Fatalf("после одного паса партия не должна завершаться") } if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Result == nil { t.Fatalf("после двух пасов подряд партия должна была завершиться") } } func TestPassResetByPlay(t *testing.T) { g := NewGoGame(9) g.Pass() if err := g.Play(gp(4, 4)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Passes != 0 { t.Errorf("счётчик пасов должен был сброситься после реального хода") } } func TestScoringEmptyBoardKomiDecides(t *testing.T) { g := NewGoGame(9) g.Pass() g.Pass() if g.Result == nil { t.Fatalf("партия должна была завершиться") } if g.Result.BlackScore != 0 { t.Errorf("на пустой доске у чёрных должно быть 0 очков, получено %v", g.Result.BlackScore) } if g.Result.WhiteScore != GoKomi { t.Errorf("у белых должно быть ровно коми (%v), получено %v", GoKomi, g.Result.WhiteScore) } if g.Result.Winner != GoWhite { t.Errorf("на пустой доске белые должны побеждать за счёт коми") } } func TestScoringCountsStonesAndTerritory(t *testing.T) { g := NewGoGame(9) // угловая точка (0,0) на доске имеет всего два соседа — (0,1) и // (1,0); если оба чёрные, то сама точка (0,0), оставшись пустой, // полностью замкнута исключительно чёрным цветом g.Board[0][1] = GoBlack g.Board[1][0] = GoBlack g.Board[8][8] = GoWhite g.Pass() g.Pass() if g.Result == nil { t.Fatalf("партия должна была завершиться") } // у чёрных: 2 камня + 1 замкнутая пустая точка (0,0) = 3 if g.Result.BlackScore != 3 { t.Errorf("ожидалось 3 очка у чёрных, получено %v", g.Result.BlackScore) } } func TestNeutralTerritoryNotScoredToEither(t *testing.T) { g := NewGoGame(9) g.Board[4][4] = GoBlack g.Board[4][5] = GoWhite g.Pass() g.Pass() if g.Result == nil { t.Fatalf("партия должна была завершиться") } if g.Result.BlackScore != 1 { t.Errorf("ожидалось 1 очко у чёрных (только свой камень), получено %v", g.Result.BlackScore) } if g.Result.WhiteScore != 1+GoKomi { t.Errorf("ожидалось 1+коми у белых, получено %v", g.Result.WhiteScore) } } func TestIsLegalDoesNotMutateState(t *testing.T) { g := NewGoGame(9) g.Board[3][4] = GoWhite g.Board[5][4] = GoWhite g.Board[4][3] = GoWhite g.Board[4][5] = GoWhite g.Turn = GoBlack if g.IsLegal(gp(4, 4)) { t.Errorf("самоубийственный ход не должен считаться легальным") } if g.Board[4][4] != GoEmpty { t.Errorf("IsLegal не должен был изменить реальную доску") } if g.Turn != GoBlack { t.Errorf("IsLegal не должен был менять очередь хода") } } func TestPlayAfterGameOverReturnsError(t *testing.T) { g := NewGoGame(9) g.Pass() g.Pass() if err := g.Play(gp(0, 0)); err != ErrGoGameOver { t.Errorf("ожидалась ErrGoGameOver, получено: %v", err) } if err := g.Pass(); err != ErrGoGameOver { t.Errorf("ожидалась ErrGoGameOver при пасе после конца игры, получено: %v", err) } } func TestBoardSizesSupported(t *testing.T) { for _, size := range []int{9, 13, 19} { g := NewGoGame(size) if g.Size != size || len(g.Board) != size || len(g.Board[0]) != size { t.Errorf("некорректная доска для размера %d", size) } } }