192 lines
7 KiB
Go
192 lines
7 KiB
Go
package main
|
||
|
||
import "math/rand"
|
||
|
||
// ПРИМЕЧАНИЕ О СИЛЕ ИГРЫ: сильный бот для Го в реальности требует
|
||
// Monte-Carlo поиска по дереву или нейросетевой оценки позиции
|
||
// (как в AlphaGo) — минимакс тут почти бесполезен из-за огромного
|
||
// ветвления (до нескольких сотен ходов на 19x19). Все три уровня
|
||
// ниже — эвристические, БЕЗ поиска вперёд по дереву партии, и
|
||
// играют существенно слабее сильного человека даже на "Сильном"
|
||
// уровне. Это осознанное ограничение, а не недоработка — честная
|
||
// сильная игра в Го выходит далеко за рамки того, что можно
|
||
// реализовать без полноценного MCTS/нейросети.
|
||
|
||
// GoDifficulty — уровень сложности бота.
|
||
type GoDifficulty int
|
||
|
||
const (
|
||
GoDifficultyEasy GoDifficulty = iota
|
||
GoDifficultyMedium
|
||
GoDifficultyHard
|
||
)
|
||
|
||
// GoBot — бот на основе эвристик без поиска вперёд.
|
||
type GoBot struct {
|
||
Difficulty GoDifficulty
|
||
}
|
||
|
||
// goPassScoreThreshold — если у бота нет ни одного хода лучше этого
|
||
// эвристического порога, он пасует, а не портит позицию заведомо
|
||
// плохим ходом (актуально в основном под конец партии).
|
||
const goPassScoreThreshold = -5.0
|
||
|
||
// goEasyPassChance — простой бот не считает выгоду ходов, но всё же
|
||
// изредка пасует сам, иначе полностью случайная игра почти никогда
|
||
// не заканчивается естественным образом (лишние ходы почти всегда
|
||
// находятся, если не пасовать вообще никогда).
|
||
const goEasyPassChance = 0.04
|
||
|
||
// DecideMove выбирает ход бота. Возвращает (pos, true) для хода в
|
||
// точку pos, либо (_, false), если бот решил пасовать.
|
||
func (b GoBot) DecideMove(g *GoGameState, rnd *rand.Rand) (GoPos, bool) {
|
||
legal := goLegalMoves(g)
|
||
if len(legal) == 0 {
|
||
return GoPos{}, false
|
||
}
|
||
|
||
switch b.Difficulty {
|
||
case GoDifficultyEasy:
|
||
if rnd.Float64() < goEasyPassChance {
|
||
return GoPos{}, false
|
||
}
|
||
return legal[rnd.Intn(len(legal))], true
|
||
default:
|
||
best := legal[0]
|
||
bestScore := goEvaluateMove(g, best, b.Difficulty)
|
||
for _, pos := range legal[1:] {
|
||
score := goEvaluateMove(g, pos, b.Difficulty)
|
||
if score > bestScore {
|
||
bestScore = score
|
||
best = pos
|
||
}
|
||
}
|
||
// при плотно заполненной доске бот больше не тратит ходы на
|
||
// бессмысленное заполнение нейтральных точек ("дамэ"), если
|
||
// лучший доступный ход не связан с реальным взятием (взятие
|
||
// всегда даёт +10 к оценке за камень, так что порог 8
|
||
// надёжно отличает "взял что-то" от "просто занял свободную
|
||
// точку") — иначе партии ботов друг с другом никогда не
|
||
// заканчивались бы сами по себе
|
||
if goBoardOccupancy(g) > 0.5 && bestScore < 8 {
|
||
return GoPos{}, false
|
||
}
|
||
if bestScore < goPassScoreThreshold {
|
||
return GoPos{}, false
|
||
}
|
||
return best, true
|
||
}
|
||
}
|
||
|
||
// goBoardOccupancy возвращает долю занятых точек доски (0..1).
|
||
func goBoardOccupancy(g *GoGameState) float64 {
|
||
occupied := 0
|
||
for _, row := range g.Board {
|
||
for _, c := range row {
|
||
if c != GoEmpty {
|
||
occupied++
|
||
}
|
||
}
|
||
}
|
||
return float64(occupied) / float64(g.Size*g.Size)
|
||
}
|
||
|
||
// PlayFullTurn исполняет один ход бота (игру в точку либо пас).
|
||
// Возвращает passed=true, если бот решил пасовать — вызывающий код
|
||
// (TUI) должен показать это явно, а не молчать, будто ничего не
|
||
// произошло.
|
||
func (b GoBot) PlayFullTurn(g *GoGameState, rnd *rand.Rand) (passed bool, err error) {
|
||
pos, ok := b.DecideMove(g, rnd)
|
||
if !ok {
|
||
return true, g.Pass()
|
||
}
|
||
return false, g.Play(pos)
|
||
}
|
||
|
||
// goLegalMoves перечисляет все легальные точки для текущего игрока.
|
||
func goLegalMoves(g *GoGameState) []GoPos {
|
||
var out []GoPos
|
||
for r := 0; r < g.Size; r++ {
|
||
for c := 0; c < g.Size; c++ {
|
||
pos := GoPos{Row: r, Col: c}
|
||
if g.IsLegal(pos) {
|
||
out = append(out, pos)
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// goEvaluateMove — эвристическая оценка одного кандидатского хода
|
||
// (без поиска вперёд): чем выше, тем привлекательнее ход для
|
||
// текущего игрока.
|
||
func goEvaluateMove(g *GoGameState, pos GoPos, level GoDifficulty) float64 {
|
||
color := g.Turn
|
||
sim := &GoGameState{
|
||
Size: g.Size,
|
||
Board: cloneGoBoard(g.Board),
|
||
Turn: g.Turn,
|
||
prevBoardBeforeOpponentMove: g.prevBoardBeforeOpponentMove,
|
||
}
|
||
capturedBefore := boardStoneCount(sim.Board, color.Opponent())
|
||
if err := sim.Play(pos); err != nil {
|
||
return -1000
|
||
}
|
||
capturedAfter := boardStoneCount(sim.Board, color.Opponent())
|
||
captures := capturedBefore - capturedAfter
|
||
|
||
score := float64(captures) * 10
|
||
|
||
_, liberties := sim.groupAndLiberties(pos)
|
||
score += float64(liberties)
|
||
|
||
if level == GoDifficultyHard {
|
||
score += goDistanceToCenterBonus(pos, g.Size)
|
||
score += goFriendlyNeighborBonus(g, pos)
|
||
}
|
||
|
||
return score
|
||
}
|
||
|
||
func boardStoneCount(board [][]GoColor, color GoColor) int {
|
||
count := 0
|
||
for _, row := range board {
|
||
for _, c := range row {
|
||
if c == color {
|
||
count++
|
||
}
|
||
}
|
||
}
|
||
return count
|
||
}
|
||
|
||
// goDistanceToCenterBonus — небольшой позиционный бонус за игру
|
||
// ближе к центру доски в дебюте (грубая, но стандартная для
|
||
// простых Го-ботов эвристика).
|
||
func goDistanceToCenterBonus(pos GoPos, size int) float64 {
|
||
center := float64(size-1) / 2
|
||
dr := float64(pos.Row) - center
|
||
dc := float64(pos.Col) - center
|
||
dist := dr*dr + dc*dc
|
||
maxDist := center * center * 2
|
||
if maxDist == 0 {
|
||
return 0
|
||
}
|
||
return 3 * (1 - dist/maxDist)
|
||
}
|
||
|
||
// goFriendlyNeighborBonus поощряет игру рядом со своими же камнями
|
||
// (грубая замена полноценной оценке влияния/территории).
|
||
func goFriendlyNeighborBonus(g *GoGameState, pos GoPos) float64 {
|
||
color := g.Turn
|
||
bonus := 0.0
|
||
for _, n := range goNeighbors(pos) {
|
||
if !g.inBounds(n) {
|
||
continue
|
||
}
|
||
if g.Board[n.Row][n.Col] == color {
|
||
bonus += 1.5
|
||
}
|
||
}
|
||
return bonus
|
||
}
|