go_games_collection/checkers_bot.go
2026-07-04 13:51:04 +03:00

217 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}