181 lines
4.8 KiB
Go
181 lines
4.8 KiB
Go
package main
|
||
|
||
import "math"
|
||
|
||
// ChessBot — бот на основе минимакса с альфа-бета отсечением.
|
||
// Ветвление в шахматах гораздо шире, чем в шашках, поэтому глубина
|
||
// поиска по умолчанию заметно меньше.
|
||
type ChessBot struct {
|
||
Depth int // глубина поиска в полуходах; 0 -> используется значение по умолчанию
|
||
}
|
||
|
||
// Уровни сложности: разница только в глубине поиска.
|
||
const (
|
||
ChessDifficultyEasy = 2 // "Простой" — быстрый, играет более-менее случайно с точки зрения стратегии
|
||
ChessDifficultyHard = 4 // "Сильный" — заметно сильнее, всё ещё отвечает быстро
|
||
)
|
||
|
||
const defaultChessDepth = 3
|
||
|
||
// chessMoveStep — один ход (from->to), достаточный для одного
|
||
// вызова g.Move.
|
||
type chessMoveStep struct {
|
||
From ChessPos
|
||
To ChessPos
|
||
}
|
||
|
||
// legalChessSteps перечисляет все допустимые ходы для стороны color.
|
||
func legalChessSteps(g *ChessGameState, color ChessColor) []chessMoveStep {
|
||
var out []chessMoveStep
|
||
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 := ChessPos{r, c}
|
||
for _, opt := range g.LegalMovesFrom(pos) {
|
||
out = append(out, chessMoveStep{From: pos, To: opt.To})
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
var chessPieceValue = map[ChessPieceType]int{
|
||
ChessPawn: 100, ChessKnight: 320, ChessBishop: 330, ChessRook: 500, ChessQueen: 900, ChessKing: 0,
|
||
}
|
||
|
||
// chessCenterBonus — небольшой позиционный бонус за контроль центра
|
||
// (тем же духом, что и бонус за продвижение шашек к дамке): не
|
||
// претендует на глубокое понимание позиции, просто чуть предпочитает
|
||
// активные фигуры пассивным.
|
||
func chessCenterBonus(pos ChessPos) int {
|
||
dr := pos.Row - 3
|
||
if dr < 0 {
|
||
dr = -dr
|
||
}
|
||
dc := pos.Col - 3
|
||
if dc < 0 {
|
||
dc = -dc
|
||
}
|
||
dist := dr + dc
|
||
return 6 - dist
|
||
}
|
||
|
||
// evaluateChess оценивает позицию с точки зрения perspective:
|
||
// положительно — хорошо для perspective.
|
||
func evaluateChess(g *ChessGameState, perspective ChessColor) int {
|
||
if g.Result != nil {
|
||
if g.Result.Draw {
|
||
return 0
|
||
}
|
||
if g.Result.Winner == perspective {
|
||
return 1000000
|
||
}
|
||
return -1000000
|
||
}
|
||
score := 0
|
||
for r := 0; r < 8; r++ {
|
||
for c := 0; c < 8; c++ {
|
||
p := g.Board[r][c]
|
||
if p == nil {
|
||
continue
|
||
}
|
||
value := chessPieceValue[p.Type] + chessCenterBonus(ChessPos{r, c})
|
||
if p.Color == perspective {
|
||
score += value
|
||
} else {
|
||
score -= value
|
||
}
|
||
}
|
||
}
|
||
return score
|
||
}
|
||
|
||
func minimaxChess(g *ChessGameState, depth int, alpha, beta int, perspective ChessColor) int {
|
||
if g.Result != nil || depth == 0 {
|
||
return evaluateChess(g, perspective)
|
||
}
|
||
steps := legalChessSteps(g, g.Turn)
|
||
if len(steps) == 0 {
|
||
return evaluateChess(g, perspective)
|
||
}
|
||
|
||
maximizing := g.Turn == perspective
|
||
if maximizing {
|
||
best := math.MinInt32
|
||
for _, step := range steps {
|
||
child := cloneChess(g)
|
||
_ = child.Move(step.From, step.To)
|
||
val := minimaxChess(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 := cloneChess(g)
|
||
_ = child.Move(step.From, step.To)
|
||
val := minimaxChess(child, depth-1, alpha, beta, perspective)
|
||
if val < best {
|
||
best = val
|
||
}
|
||
if best < beta {
|
||
beta = best
|
||
}
|
||
if alpha >= beta {
|
||
break
|
||
}
|
||
}
|
||
return best
|
||
}
|
||
|
||
// DecideMove выбирает лучший ход для текущего хода в g согласно
|
||
// минимаксу.
|
||
func (b ChessBot) DecideMove(g *ChessGameState) (chessMoveStep, bool) {
|
||
steps := legalChessSteps(g, g.Turn)
|
||
if len(steps) == 0 {
|
||
return chessMoveStep{}, false
|
||
}
|
||
depth := b.Depth
|
||
if depth <= 0 {
|
||
depth = defaultChessDepth
|
||
}
|
||
|
||
perspective := g.Turn
|
||
bestVal := math.MinInt32
|
||
var bestStep chessMoveStep
|
||
alpha, beta := math.MinInt32, math.MaxInt32
|
||
for _, step := range steps {
|
||
child := cloneChess(g)
|
||
_ = child.Move(step.From, step.To)
|
||
val := minimaxChess(child, depth-1, alpha, beta, perspective)
|
||
if val > bestVal {
|
||
bestVal = val
|
||
bestStep = step
|
||
}
|
||
if bestVal > alpha {
|
||
alpha = bestVal
|
||
}
|
||
}
|
||
return bestStep, true
|
||
}
|
||
|
||
// PlayFullTurn исполняет один ход бота.
|
||
func (b ChessBot) PlayFullTurn(g *ChessGameState) error {
|
||
step, ok := b.DecideMove(g)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return g.Move(step.From, step.To)
|
||
}
|