203 lines
7.1 KiB
Go
203 lines
7.1 KiB
Go
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
|
||
}
|