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 }