130 lines
4.1 KiB
Go
130 lines
4.1 KiB
Go
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
|
||
}
|