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 }