package main import "errors" // TicTacToeMark — что стоит в клетке: пусто, крестик или нолик. type TicTacToeMark int const ( TicTacToeEmpty TicTacToeMark = iota TicTacToeX TicTacToeO ) // TicTacToePhase — фаза партии. type TicTacToePhase int const ( TicTacToePhasePlay TicTacToePhase = iota TicTacToePhaseOver ) // TicTacToeResult — итог завершённой партии. type TicTacToeResult struct { Winner TicTacToeMark // TicTacToeEmpty при ничьей Line [3]int // индексы выигрышной линии — для подсветки; не имеет значения при ничьей Draw bool } // TicTacToeGameState — состояние партии в крестики-нолики (доска // 3×3, классические правила: три подряд по горизонтали, вертикали // или диагонали). type TicTacToeGameState struct { Board [9]TicTacToeMark CurrentTurn TicTacToeMark // чей сейчас ход HumanMark TicTacToeMark // каким знаком играет человек Phase TicTacToePhase Result *TicTacToeResult MovesPlayed int } var ( ErrTicTacToeGameOver = errors.New("партия уже завершена") ErrTicTacToeInvalidPosition = errors.New("недопустимый номер клетки") ErrTicTacToeCellOccupied = errors.New("клетка уже занята") ) // NewTicTacToeGame создаёт новую партию. humanMark задаёт, каким // знаком играет человек — крестики всегда ходят первыми (классическое // правило), так что если человек играет ноликами, первым автоматически // должен сходить бот. func NewTicTacToeGame(humanMark TicTacToeMark) *TicTacToeGameState { if humanMark != TicTacToeX && humanMark != TicTacToeO { humanMark = TicTacToeX } return &TicTacToeGameState{ HumanMark: humanMark, CurrentTurn: TicTacToeX, Phase: TicTacToePhasePlay, } } // ticTacToeWinLines — все восемь способов выиграть: 3 строки, // 3 столбца, 2 диагонали (индексы клеток по порядку 0..8 слева // направо, сверху вниз). var ticTacToeWinLines = [8][3]int{ {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, {0, 4, 8}, {2, 4, 6}, } // ticTacToeCheckWin проверяет, есть ли на доске выигрышная линия. func ticTacToeCheckWin(board [9]TicTacToeMark) (winner TicTacToeMark, line [3]int, ok bool) { for _, l := range ticTacToeWinLines { a, b, c := board[l[0]], board[l[1]], board[l[2]] if a != TicTacToeEmpty && a == b && b == c { return a, l, true } } return TicTacToeEmpty, [3]int{}, false } // ticTacToeOpponent возвращает противоположный знак. func ticTacToeOpponent(mark TicTacToeMark) TicTacToeMark { if mark == TicTacToeX { return TicTacToeO } return TicTacToeX } // ticTacToeEmptyCells — индексы всех свободных клеток. func ticTacToeEmptyCells(board [9]TicTacToeMark) []int { var out []int for i, m := range board { if m == TicTacToeEmpty { out = append(out, i) } } return out } // PlaceMark — текущий игрок ставит свой знак в клетку pos (0..8, // слева направо, сверху вниз). func (g *TicTacToeGameState) PlaceMark(pos int) error { if g.Phase != TicTacToePhasePlay { return ErrTicTacToeGameOver } if pos < 0 || pos >= 9 { return ErrTicTacToeInvalidPosition } if g.Board[pos] != TicTacToeEmpty { return ErrTicTacToeCellOccupied } g.Board[pos] = g.CurrentTurn g.MovesPlayed++ if winner, line, ok := ticTacToeCheckWin(g.Board); ok { g.Result = &TicTacToeResult{Winner: winner, Line: line} g.Phase = TicTacToePhaseOver return nil } if g.MovesPlayed == 9 { g.Result = &TicTacToeResult{Draw: true} g.Phase = TicTacToePhaseOver return nil } g.CurrentTurn = ticTacToeOpponent(g.CurrentTurn) return nil }