217 lines
6.6 KiB
Go
217 lines
6.6 KiB
Go
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)
|
||
}
|