go_games_collection/senet_bot.go

142 lines
4.4 KiB
Go
Raw 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"
// SenetDifficulty — уровень сложности бота.
type SenetDifficulty int
const (
SenetDifficultyEasy SenetDifficulty = iota
SenetDifficultyMedium
SenetDifficultyHard
)
// SenetBot — бот-соперник для Сенета.
type SenetBot struct {
Difficulty SenetDifficulty
}
// senetStickResultByMarked — перевод числа "отмеченных вверх" граней
// четырёх метательных палочек в игровое значение: 0 -> 5 очков,
// 1 -> 1, 2 -> 2, 3 -> 3, 4 -> 4 (историческое соглашение).
var senetStickResultByMarked = [5]int{5, 1, 2, 3, 4}
// senetStickWeights — вероятностные веса каждого числа отмеченных
// граней (на 16, те же биномиальные "четыре монеты", что и в Уре).
var senetStickWeights = [5]int{1, 4, 6, 4, 1}
// senetRollSticks бросает четыре монеты и переводит число
// "отмеченных вверх" в игровое значение.
func senetRollSticks(rnd *rand.Rand) int {
marked := 0
for i := 0; i < 4; i++ {
if rnd.Intn(2) == 1 {
marked++
}
}
return senetStickResultByMarked[marked]
}
// senetEvaluate оценивает позицию с точки зрения player.
func senetEvaluate(g *SenetGameState, player SenetPlayer) float64 {
opp := senetOpponent(player)
score := float64(g.BorneOff[player]) * 40
score -= float64(g.BorneOff[opp]) * 40
for sq := 1; sq <= SenetBoardSize; sq++ {
owner, ok := senetCellOwner(g.Board[sq])
if !ok {
continue
}
if owner == player {
score += float64(sq) * 0.5
if senetSafeSquares[sq] {
score += 5
}
if g.senetRunLength(sq) == 1 && !senetSafeSquares[sq] {
score -= 4 // одинокая шашка вне безопасной клетки — уязвима к обмену
}
} else {
score -= float64(sq) * 0.45
}
}
return score
}
// DecideMove выбирает ход бота (клетку-источник) согласно уровню
// сложности. Предполагается, что кости уже брошены и есть хотя бы
// один легальный ход.
func (b SenetBot) DecideMove(g *SenetGameState, rnd *rand.Rand) int {
moves := g.LegalMoves()
if len(moves) == 0 {
return -1
}
if b.Difficulty == SenetDifficultyEasy {
return moves[rnd.Intn(len(moves))].From
}
player := g.CurrentPlayer
bestScore := 0.0
var best []int
for i, mv := range moves {
clone := *g
clone.Move(mv.From)
score := senetEvaluate(&clone, player)
if b.Difficulty == SenetDifficultyHard {
score -= b.opponentThreatPenalty(&clone, player)
}
if i == 0 || score > bestScore {
bestScore = score
best = []int{mv.From}
} else if score == bestScore {
best = append(best, mv.From)
}
}
return best[rnd.Intn(len(best))]
}
// opponentThreatPenalty оценивает (усреднённо по возможным броскам
// соперника) риск того, что после нашего хода соперник сможет
// сразу же взять одну из наших незащищённых шашек.
func (b SenetBot) opponentThreatPenalty(g *SenetGameState, player SenetPlayer) float64 {
opp := senetOpponent(player)
penalty := 0.0
for marked := 0; marked <= 4; marked++ {
weight := float64(senetStickWeights[marked]) / 16
roll := senetStickResultByMarked[marked]
clone := *g
clone.CurrentPlayer = opp
clone.Rolled = true
clone.DiceValue = roll
for _, mv := range clone.legalMovesForDirection(1) {
dest := mv.From + roll
if dest >= 1 && dest <= SenetBoardSize {
if owner, ok := senetCellOwner(clone.Board[dest]); ok && owner == player {
penalty += weight * 4
}
}
}
}
return penalty
}
// PlayFullTurn бросает кости и доигрывает весь ход бота — включая
// повторные броски, которые дают значения 1, 4 и 5.
func (b SenetBot) PlayFullTurn(g *SenetGameState, rnd *rand.Rand) error {
me := g.CurrentPlayer
for g.Result == nil && g.CurrentPlayer == me {
if !g.Rolled {
if err := g.Roll(senetRollSticks(rnd)); err != nil {
return err
}
continue
}
from := b.DecideMove(g, rnd)
if from < 0 {
return nil
}
if err := g.Move(from); err != nil {
return err
}
}
return nil
}