package main import "math" // CheckersBot — бот на основе минимакса с альфа-бета отсечением. // Оценка позиции простая: количество шашек (дамка весит дороже // простой) плюс небольшой бонус за продвижение к последней // горизонтали — этого достаточно для разумной, но не идеальной игры. type CheckersBot struct { Depth int // глубина поиска в полуходах; 0 -> используется значение по умолчанию } // Уровни сложности: разница только в глубине поиска минимакса — // более глубокий просчёт означает более сильную (но и более // медленную) игру. На доступном железе даже "сильный" уровень // отвечает за доли секунды. const ( CheckersDifficultyEasy = 3 // "Простой" — быстрый, ощутимо слабее CheckersDifficultyHard = 8 // "Сильный" — заметно сильнее, чуть медленнее ) const defaultCheckersDepth = 5 // checkersMoveStep — один шаг хода (from->to), достаточный для // вызова g.Move один раз; серии взятий бот проходит по одному шагу, // как и человек через TUI. type checkersMoveStep struct { From CheckersPos To CheckersPos } // legalStepsForColor перечисляет все допустимые ходы (from->to) для // стороны color при текущем состоянии g (учитывая обязательное // взятие и середину серии, если применимо). func legalStepsForColor(g *CheckersGameState, color CheckersColor) []checkersMoveStep { var out []checkersMoveStep if g.ChainPos != nil { for _, opt := range g.LegalMovesFrom(*g.ChainPos) { out = append(out, checkersMoveStep{From: *g.ChainPos, To: opt.To}) } return out } for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { p := g.Board[r][c] if p == nil || p.Color != color { continue } pos := CheckersPos{Row: r, Col: c} for _, opt := range g.LegalMovesFrom(pos) { out = append(out, checkersMoveStep{From: pos, To: opt.To}) } } } return out } // cloneCheckers создаёт независимую глубокую копию состояния — нужна // минимаксу для просчёта ходов без изменения реальной партии. func cloneCheckers(g *CheckersGameState) *CheckersGameState { clone := &CheckersGameState{ Turn: g.Turn, MovesSinceCapture: g.MovesSinceCapture, } for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { if p := g.Board[r][c]; p != nil { cp := *p clone.Board[r][c] = &cp } } } if g.ChainPos != nil { pos := *g.ChainPos clone.ChainPos = &pos } if g.Result != nil { res := *g.Result clone.Result = &res } return clone } // evaluateCheckers оценивает позицию с точки зрения perspective: // положительно — хорошо для perspective, отрицательно — для // соперника. func evaluateCheckers(g *CheckersGameState, perspective CheckersColor) int { if g.Result != nil { if g.Result.Draw { return 0 } if g.Result.Winner == perspective { return 100000 } return -100000 } score := 0 for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { p := g.Board[r][c] if p == nil { continue } value := 10 if p.King { value = 18 } else { if p.Color == CheckersWhite { value += (7 - r) / 3 } else { value += r / 3 } } if p.Color == perspective { score += value } else { score -= value } } } return score } // minimax реализует альфа-бета отсечение. toMove — сторона, чей ход // сейчас просчитывается; perspective — сторона, для которой ведётся // итоговая оценка (бот). func minimax(g *CheckersGameState, depth int, alpha, beta int, perspective CheckersColor) int { if g.Result != nil || depth == 0 { return evaluateCheckers(g, perspective) } steps := legalStepsForColor(g, g.Turn) if len(steps) == 0 { return evaluateCheckers(g, perspective) } maximizing := g.Turn == perspective if maximizing { best := math.MinInt32 for _, step := range steps { child := cloneCheckers(g) _ = child.Move(step.From, step.To) val := minimax(child, depth-1, alpha, beta, perspective) if val > best { best = val } if best > alpha { alpha = best } if alpha >= beta { break } } return best } best := math.MaxInt32 for _, step := range steps { child := cloneCheckers(g) _ = child.Move(step.From, step.To) val := minimax(child, depth-1, alpha, beta, perspective) if val < best { best = val } if best < beta { beta = best } if alpha >= beta { break } } return best } // DecideMove выбирает лучший ход для текущего хода в g согласно // минимаксу. Возвращает (step, true), либо (_, false), если ходов // нет (не должно происходить, если PlayFullTurn вызывается только // когда игра не окончена). func (b CheckersBot) DecideMove(g *CheckersGameState) (checkersMoveStep, bool) { steps := legalStepsForColor(g, g.Turn) if len(steps) == 0 { return checkersMoveStep{}, false } depth := b.Depth if depth <= 0 { depth = defaultCheckersDepth } perspective := g.Turn bestVal := math.MinInt32 var bestStep checkersMoveStep alpha, beta := math.MinInt32, math.MaxInt32 for _, step := range steps { child := cloneCheckers(g) _ = child.Move(step.From, step.To) val := minimax(child, depth-1, alpha, beta, perspective) if val > bestVal { bestVal = val bestStep = step } if bestVal > alpha { alpha = bestVal } } return bestStep, true } // PlayFullTurn исполняет один шаг хода бота (одно применение Move) — // при серии взятий вызывающий код должен продолжать вызывать // PlayFullTurn, пока не сменится очередь хода (g.Turn). func (b CheckersBot) PlayFullTurn(g *CheckersGameState) error { step, ok := b.DecideMove(g) if !ok { return nil } return g.Move(step.From, step.To) }