package main import "errors" // NardyVariant — короткие или длинные нарды. Отличия: начальная // расстановка, направление движения второго игрока относительно // первого, и (главное) возможность взятия шашек — в длинных нардах // её нет вовсе: занятая соперником точка (даже одной шашкой) // заблокирована полностью, тогда как в коротких одинокая шашка // соперника ("блот") сбивается и уходит на бар. type NardyVariant int const ( NardyShort NardyVariant = iota NardyLong ) // NardyPlayer — цвет шашек игрока. type NardyPlayer int const ( NardyWhite NardyPlayer = iota NardyBlack ) func nardyOpponent(p NardyPlayer) NardyPlayer { if p == NardyWhite { return NardyBlack } return NardyWhite } // NardyPhase — фаза текущего хода. type NardyPhase int const ( NardyPhaseRoll NardyPhase = iota // нужно бросить кости NardyPhaseMove // кости брошены, есть неиспользованные значения NardyPhaseOver ) // NardyPoint — состояние одной точки доски в АБСОЛЮТНЫХ координатах // (0-23, общих для обоих игроков — доска физически одна). type NardyPoint struct { Owner NardyPlayer Count int } // NardyMove — одно единичное перемещение одной шашки одним // значением кости. type NardyMove struct { FromBar bool // ход шашкой с бара (только короткие нарды) From int // относительная (своя) точка-источник; не используется при FromBar Die int BearOff bool // это снятие шашки с доски насовсем } // NardyResult — итог завершённой партии. type NardyResult struct { Winner NardyPlayer } // NardyGameState — полное состояние партии в нарды. type NardyGameState struct { Variant NardyVariant // Board — 24 абсолютные точки, общие для обоих игроков. Board [24]NardyPoint BorneOff [2]int // сколько шашек снято с доски (индекс — NardyPlayer) Bar [2]int // сколько шашек на баре (только короткие нарды; в длинных всегда 0) CurrentPlayer NardyPlayer Phase NardyPhase Dice [2]int // выпавшие значения последнего броска DiceLeft []int // оставшиеся неиспользованные значения в этом ходу (4 при дубле) headMovesThisTurn int // сколько шашек уже взято "с головы" в этом ходу Result *NardyResult } var ( ErrNardyWrongPhase = errors.New("недопустимое действие для текущей фазы") ErrNardyIllegalMove = errors.New("такой ход сейчас недопустим") ) // nardyPointToAbsolute переводит "относительную" точку игрока // (0..23, где 23 — голова/дальняя от дома точка, 0..5 — дом) в // абсолютную точку доски, в зависимости от варианта игры. // // Короткие нарды: игроки движутся навстречу друг другу — точки // зеркальны (белый: abs=rel; чёрный: abs=23-rel). // // Длинные нарды: игроки движутся В ОДНОМ направлении — раскладка // чёрного просто сдвинута на половину круга (белый: abs=rel; // чёрный: abs=(rel+12) mod 24), а не отражена. func nardyPointToAbsolute(variant NardyVariant, player NardyPlayer, rel int) int { if player == NardyWhite { return rel } if variant == NardyLong { return (rel + 12) % 24 } return 23 - rel } // nardyStartingSetup — начальная расстановка в ОТНОСИТЕЛЬНЫХ // координатах (одинакова для обоих игроков относительно их // собственной головы). func nardyStartingSetup(variant NardyVariant) map[int]int { if variant == NardyLong { return map[int]int{23: 15} } return map[int]int{23: 2, 12: 5, 7: 3, 5: 5} } // NewNardyGame создаёт новую партию с заданным вариантом правил; // firstPlayer — кто ходит первым (по правилам определяется броском // одного зара каждым игроком — эта логика оставлена вызывающему // коду, здесь просто принимается готовый результат). func NewNardyGame(variant NardyVariant, firstPlayer NardyPlayer) *NardyGameState { g := &NardyGameState{ Variant: variant, CurrentPlayer: firstPlayer, Phase: NardyPhaseRoll, } setup := nardyStartingSetup(variant) for _, player := range [2]NardyPlayer{NardyWhite, NardyBlack} { for rel, count := range setup { abs := nardyPointToAbsolute(variant, player, rel) g.Board[abs] = NardyPoint{Owner: player, Count: count} } } return g } // canLand проверяет, может ли игрок player поставить шашку на свою // относительную точку rel. func (g *NardyGameState) canLand(player NardyPlayer, rel int) bool { abs := nardyPointToAbsolute(g.Variant, player, rel) pt := g.Board[abs] if pt.Count == 0 { return true } if pt.Owner == player { return true } if g.Variant == NardyLong { return false // в длинных нардах взятия нет — даже одна чужая шашка блокирует точку насмерть } return pt.Count == 1 // короткие нарды: можно сбить одинокий блот } // allCheckersHome проверяет, все ли шашки игрока уже в доме // (относительные точки 0-5) и не на баре — необходимое условие для // начала снятия шашек с доски. func (g *NardyGameState) allCheckersHome(player NardyPlayer) bool { if g.Bar[player] > 0 { return false } for rel := 6; rel <= 23; rel++ { abs := nardyPointToAbsolute(g.Variant, player, rel) if g.Board[abs].Owner == player && g.Board[abs].Count > 0 { return false } } return true } // hasCheckerBeyond проверяет, есть ли у игрока шашки на // относительных точках дома строго дальше rel (т.е. на точках, // которые ещё не готовы сниматься точным значением кости) — // используется, чтобы разрешить "снятие с избытком". func (g *NardyGameState) hasCheckerBeyond(player NardyPlayer, rel int) bool { for r := rel + 1; r <= 5; r++ { abs := nardyPointToAbsolute(g.Variant, player, r) if g.Board[abs].Owner == player && g.Board[abs].Count > 0 { return true } } return false } // candidateMovesForDie перечисляет все ходы, физически возможные для // текущего игрока при использовании ОДНОГО значения кости die (без // учёта правила "обязаны использовать максимум костей" — эта // проверка делается на уровне LegalMovesNow). func (g *NardyGameState) candidateMovesForDie(die int) []NardyMove { player := g.CurrentPlayer var out []NardyMove if g.Bar[player] > 0 { entryRel := 24 - die if g.canLand(player, entryRel) { out = append(out, NardyMove{FromBar: true, Die: die}) } return out } allHome := g.allCheckersHome(player) for rel := 23; rel >= 0; rel-- { abs := nardyPointToAbsolute(g.Variant, player, rel) pt := g.Board[abs] if pt.Count == 0 || pt.Owner != player { continue } if rel == 23 && g.headMovesThisTurn >= 1 { continue // с головы — не больше одной шашки за ход } dest := rel - die if dest >= 0 { if g.canLand(player, dest) { out = append(out, NardyMove{From: rel, Die: die}) } continue } if !allHome { continue } if dest == -1 { out = append(out, NardyMove{From: rel, Die: die, BearOff: true}) } else if !g.hasCheckerBeyond(player, rel) { out = append(out, NardyMove{From: rel, Die: die, BearOff: true}) } } return out } // cloneForSearch делает независимую копию состояния, достаточную // для перебора вариантов хода (для реальной мутации самой партии не // используется). // // ВАЖНО: DiceLeft — срез, и поверхностное копирование структуры // (c := *g) делило бы его нижележащий массив с оригиналом. Тогда // планирование бота (применяющее ходы к "клону" через applyMoveRaw // + removeDieOnce) тихо портило бы DiceLeft настоящей партии же до // того, как ходы были бы реально применены — тот же класс ошибки, // что уже однажды ловился в раздаче карт Тонка. func (g *NardyGameState) cloneForSearch() *NardyGameState { c := *g c.DiceLeft = append([]int{}, g.DiceLeft...) return &c } // applyMoveRaw применяет одиночный ход БЕЗ проверки легальности // (легальность уже должна быть проверена вызывающим кодом) и без // смены игрока/фазы — используется и реальным ApplyMove, и // внутренним перебором вариантов. func (g *NardyGameState) applyMoveRaw(mv NardyMove) { player := g.CurrentPlayer if mv.FromBar { entryRel := 24 - mv.Die abs := nardyPointToAbsolute(g.Variant, player, entryRel) g.hitIfBlot(player, abs) g.Board[abs].Owner = player g.Board[abs].Count++ g.Bar[player]-- return } fromAbs := nardyPointToAbsolute(g.Variant, player, mv.From) g.Board[fromAbs].Count-- if mv.From == 23 { g.headMovesThisTurn++ } if mv.BearOff { g.BorneOff[player]++ return } destRel := mv.From - mv.Die destAbs := nardyPointToAbsolute(g.Variant, player, destRel) g.hitIfBlot(player, destAbs) g.Board[destAbs].Owner = player g.Board[destAbs].Count++ } // hitIfBlot сбивает одинокую шашку соперника на абсолютной точке // abs, если она там есть (только короткие нарды — в длинных // canLand уже не пустит на клетку с чужой шашкой вовсе, так что // здесь этот случай просто никогда не наступит для длинных нард). func (g *NardyGameState) hitIfBlot(player NardyPlayer, abs int) { pt := g.Board[abs] if pt.Count == 1 && pt.Owner != player { g.Bar[pt.Owner]++ g.Board[abs] = NardyPoint{} } } func (g *NardyGameState) removeDieOnce(die int) { for i, d := range g.DiceLeft { if d == die { g.DiceLeft = append(g.DiceLeft[:i], g.DiceLeft[i+1:]...) return } } } // nardyMaxDiceUsableFrom считает, сколько костей максимум можно // использовать целиком (0..len(dice)) при данном состоянии доски и // списке оставшихся значений костей — перебором всех // последовательностей ходов. Именно эта функция обеспечивает // правило "обязаны использовать максимально возможное число костей". func (g *NardyGameState) nardyMaxDiceUsableFrom(dice []int) int { best := 0 seenDie := map[int]bool{} for i, d := range dice { if seenDie[d] { continue } seenDie[d] = true for _, mv := range g.candidateMovesForDie(d) { clone := g.cloneForSearch() clone.applyMoveRaw(mv) rest := make([]int, 0, len(dice)-1) removed := false for j, dv := range dice { if j == i && !removed { removed = true continue } rest = append(rest, dv) } sub := 1 + clone.nardyMaxDiceUsableFrom(rest) if sub > best { best = sub } } } return best } // LegalMovesNow возвращает все ходы, легальные ПРЯМО СЕЙЧАС — с // учётом того, что до конца хода нужно использовать максимально // возможное число оставшихся костей (см. nardyMaxDiceUsableFrom), и // что если можно сыграть только ОДНУ кость из двух РАЗНЫХ значений // (не дубль), обязана быть сыграна БОЛЬШАЯ — стандартное правило нард. func (g *NardyGameState) LegalMovesNow() []NardyMove { if g.Phase != NardyPhaseMove || len(g.DiceLeft) == 0 { return nil } needed := g.nardyMaxDiceUsableFrom(g.DiceLeft) if needed == 0 { return nil } if needed == 1 { unique := map[int]bool{} for _, d := range g.DiceLeft { unique[d] = true } if len(unique) == 2 { bigger := 0 for d := range unique { if d > bigger { bigger = d } } if biggerMoves := g.candidateMovesForDie(bigger); len(biggerMoves) > 0 { return biggerMoves } } } var out []NardyMove seenDie := map[int]bool{} for _, d := range g.DiceLeft { if seenDie[d] { continue } seenDie[d] = true for _, mv := range g.candidateMovesForDie(d) { clone := g.cloneForSearch() clone.applyMoveRaw(mv) rest := make([]int, 0, len(g.DiceLeft)-1) removedOne := false for _, dv := range g.DiceLeft { if dv == d && !removedOne { removedOne = true continue } rest = append(rest, dv) } if 1+clone.nardyMaxDiceUsableFrom(rest) >= needed { out = append(out, mv) } } } return out } // Roll бросает кости и переходит в фазу хода. Если после броска не // находится ни одного легального хода — очки сгорают, ход сразу же // переходит следующему игроку. func (g *NardyGameState) Roll(d1, d2 int) { g.Dice = [2]int{d1, d2} if d1 == d2 { g.DiceLeft = []int{d1, d1, d1, d1} } else { g.DiceLeft = []int{d1, d2} } g.headMovesThisTurn = 0 g.Phase = NardyPhaseMove if len(g.LegalMovesNow()) == 0 { g.endTurn() } } // ApplyMove выполняет один из ходов, предложенных LegalMovesNow. func (g *NardyGameState) ApplyMove(mv NardyMove) error { if g.Phase != NardyPhaseMove { return ErrNardyWrongPhase } legal := g.LegalMovesNow() found := false for _, l := range legal { if l == mv { found = true break } } if !found { return ErrNardyIllegalMove } g.applyMoveRaw(mv) g.removeDieOnce(mv.Die) if g.BorneOff[g.CurrentPlayer] == 15 { g.Result = &NardyResult{Winner: g.CurrentPlayer} g.Phase = NardyPhaseOver return nil } if len(g.LegalMovesNow()) == 0 { g.endTurn() } return nil } func (g *NardyGameState) endTurn() { g.DiceLeft = nil g.Dice = [2]int{} g.CurrentPlayer = nardyOpponent(g.CurrentPlayer) g.Phase = NardyPhaseRoll }