142 lines
4.4 KiB
Go
142 lines
4.4 KiB
Go
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
|
||
}
|