go_games_collection/nardy_bot.go

203 lines
7.1 KiB
Go
Raw Permalink 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/rand"
// NardyDifficulty — уровень сложности бота.
type NardyDifficulty int
const (
// NardyDifficultyEasy — случайный выбор среди легальных ходов.
NardyDifficultyEasy NardyDifficulty = iota
// NardyDifficultyMedium — жадная эвристика: на каждом отдельном
// подшаге хода оценивает результирующую позицию и берёт лучший
// вариант, не заглядывая дальше одного подшага вперёд.
NardyDifficultyMedium
// NardyDifficultyHard — перебирает ВСЕ полные последовательности
// ходов на весь текущий ход (с учётом обязательного использования
// максимума костей) и оценивает итоговую позицию целиком,
// выбирая лучшую последовательность. Не идеальная игра (в
// отличие от крестиков-ноликов нарды не решены полностью из-за
// случайности костей), но заметно сильнее эвристики среднего
// уровня за счёт взгляда на весь ход целиком, а не по подшагам.
NardyDifficultyHard
)
// NardyBot — бот-соперник для нард.
type NardyBot struct {
Difficulty NardyDifficulty
}
// nardyPipCount — суммарное "расстояние" (в очках хода), которое
// игроку ещё нужно преодолеть, чтобы вывести все шашки с доски.
// Шашка на относительной точке rel требует ровно rel+1 очков, чтобы
// точно выйти; шашка на баре считается как самая дальняя (25).
func nardyPipCount(g *NardyGameState, player NardyPlayer) int {
total := g.Bar[player] * 25
for rel := 0; rel <= 23; rel++ {
abs := nardyPointToAbsolute(g.Variant, player, rel)
pt := g.Board[abs]
if pt.Owner == player {
total += pt.Count * (rel + 1)
}
}
return total
}
// nardyEvaluate оценивает позицию с точки зрения player — чем выше
// значение, тем позиция выгоднее для player. Учитывает: разницу в
// снятых шашках (важнее всего), разницу в суммарном пути (pip
// count), собственные блоты (только короткие нарды — уязвимость
// быть сбитым), собственные закрытые пункты (блокируют соперника) и
// шашки на баре с обеих сторон.
func nardyEvaluate(g *NardyGameState, player NardyPlayer) float64 {
opp := nardyOpponent(player)
score := 0.0
score += float64(g.BorneOff[player]-g.BorneOff[opp]) * 50
score += float64(nardyPipCount(g, opp) - nardyPipCount(g, player))
for rel := 0; rel <= 23; rel++ {
abs := nardyPointToAbsolute(g.Variant, player, rel)
pt := g.Board[abs]
if pt.Owner != player {
continue
}
if g.Variant == NardyShort && pt.Count == 1 {
score -= 10 // блот — риск быть сбитым (актуально только в коротких нардах)
}
if pt.Count >= 2 {
score += 2 // закрытый пункт — мешает продвижению соперника
}
}
score -= float64(g.Bar[player]) * 15
score += float64(g.Bar[opp]) * 15
return score
}
// enumerateMaximalSequences перечисляет ВСЕ последовательности
// ходов, использующие максимально возможное число оставшихся костей
// текущего хода (см. NardyGameState.LegalMovesNow).
func enumerateMaximalSequences(g *NardyGameState) [][]NardyMove {
start := g.cloneForSearch()
needed := start.nardyMaxDiceUsableFrom(start.DiceLeft)
var out [][]NardyMove
var walk func(state *NardyGameState, path []NardyMove)
walk = func(state *NardyGameState, path []NardyMove) {
if len(path) == needed {
out = append(out, append([]NardyMove{}, path...))
return
}
for _, mv := range state.LegalMovesNow() {
clone := state.cloneForSearch()
clone.applyMoveRaw(mv)
clone.removeDieOnce(mv.Die)
walk(clone, append(path, mv))
}
}
walk(start, nil)
if len(out) == 0 {
out = [][]NardyMove{{}}
}
return out
}
// DecideSequence решает весь текущий ход целиком (список одиночных
// перемещений, готовых к последовательному применению через
// ApplyMove) согласно уровню сложности бота.
func (b NardyBot) DecideSequence(g *NardyGameState, rnd *rand.Rand) []NardyMove {
switch b.Difficulty {
case NardyDifficultyEasy:
return b.decideEasySequence(g, rnd)
case NardyDifficultyMedium:
return b.decideMediumSequence(g, rnd)
default:
return b.decideHardSequence(g, rnd)
}
}
func (b NardyBot) decideEasySequence(g *NardyGameState, rnd *rand.Rand) []NardyMove {
var seq []NardyMove
state := g.cloneForSearch()
for {
moves := state.LegalMovesNow()
if len(moves) == 0 {
break
}
mv := moves[rnd.Intn(len(moves))]
state.applyMoveRaw(mv)
state.removeDieOnce(mv.Die)
seq = append(seq, mv)
}
return seq
}
func (b NardyBot) decideMediumSequence(g *NardyGameState, rnd *rand.Rand) []NardyMove {
var seq []NardyMove
state := g.cloneForSearch()
player := g.CurrentPlayer
for {
moves := state.LegalMovesNow()
if len(moves) == 0 {
break
}
bestScore := 0.0
var best []NardyMove
for i, mv := range moves {
clone := state.cloneForSearch()
clone.applyMoveRaw(mv)
score := nardyEvaluate(clone, player)
if i == 0 || score > bestScore {
bestScore = score
best = []NardyMove{mv}
} else if score == bestScore {
best = append(best, mv)
}
}
chosen := best[rnd.Intn(len(best))]
state.applyMoveRaw(chosen)
state.removeDieOnce(chosen.Die)
seq = append(seq, chosen)
}
return seq
}
func (b NardyBot) decideHardSequence(g *NardyGameState, rnd *rand.Rand) []NardyMove {
sequences := enumerateMaximalSequences(g)
player := g.CurrentPlayer
bestScore := 0.0
var best [][]NardyMove
for i, seq := range sequences {
clone := g.cloneForSearch()
for _, mv := range seq {
clone.applyMoveRaw(mv)
}
score := nardyEvaluate(clone, player)
if i == 0 || score > bestScore {
bestScore = score
best = [][]NardyMove{seq}
} else if score == bestScore {
best = append(best, seq)
}
}
return best[rnd.Intn(len(best))]
}
// PlayFullTurn бросает кости (если нужно) и доигрывает весь ход
// бота целиком согласно его уровню сложности.
func (b NardyBot) PlayFullTurn(g *NardyGameState, rnd *rand.Rand) error {
if g.Phase == NardyPhaseRoll {
g.Roll(rnd.Intn(6)+1, rnd.Intn(6)+1)
}
if g.Phase != NardyPhaseMove {
return nil
}
for _, mv := range b.DecideSequence(g, rnd) {
if err := g.ApplyMove(mv); err != nil {
return err
}
}
return nil
}