package main import "errors" // Игра Ур (Royal Game of Ur) — одна из старейших настольных игр, // найдена при раскопках Ур (Месопотамия, ок. 2600 до н.э.). // Подлинные правила утеряны; здесь реализована реконструкция // Ирвинга Финкеля (British Museum, 2007) — самая распространённая // сегодня, основанная на клинописной табличке II века до н.э. // Другие историки (Мастерс, Мюррей) предлагают иные пути по доске — // это ОДНА из конкурирующих реконструкций, а не единственно верная. // UrPlayer — сторона игрока. type UrPlayer int const ( UrPlayerA UrPlayer = iota UrPlayerB ) func urOpponent(p UrPlayer) UrPlayer { if p == UrPlayerA { return UrPlayerB } return UrPlayerA } // UrPieceCount — число шашек у каждого игрока (историческая // комплектация по Финкелю). const UrPieceCount = 7 // UrPathLength — длина пути одной шашки: 14 клеток по доске плюс // 15-й (последний) ход снимает её с доски. const UrPathLength = 14 // Путь каждого игрока (индексы 1..14, 0 = ещё не вошла на доску, // 15 = снята с доски): // 1-4 — частный "стартовый" ряд игрока (свой у каждого) // 5-12 — общий средний ряд (восемь клеток, включая "мостик") — // единственное место, где происходят встречи и взятия // 13-14 — частный "выходной" ряд игрока (свой у каждого) // // Общие клетки нумеруются 0..7 отдельным индексом sharedIdx = // pathIndex-5, одинаковым для обоих игроков — именно так // проверяется, кто на какой физической клетке стоит. // urRosettePathIndices — на каких позициях пути расположены клетки // с розеткой (дают дополнительный бросок; общая розетка в середине // ряда, кроме того, защищает от взятия — единственная "безопасная" // клетка вне частных рядов). var urRosettePathIndices = map[int]bool{4: true, 8: true, 14: true} // UrGameState — состояние партии. type UrGameState struct { // Path[player][i] для i от 0 до UrPieceCount-1 — позиция i-й // шашки игрока на ЕЁ ПУТИ: 0 = в руке, 1-14 = на доске, 15 = снята. Path [2][UrPieceCount]int CurrentPlayer UrPlayer DiceValue int // последнее выпавшее значение (0-4), -1 пока не брошено в этом ходу Rolled bool // брошены ли кости в этом ходу Result *UrResult } // UrResult — итог завершённой партии. type UrResult struct { Winner UrPlayer } var ( ErrUrMustRollFirst = errors.New("сначала нужно бросить кости") ErrUrAlreadyRolled = errors.New("кости в этом ходу уже брошены") ErrUrIllegalMove = errors.New("этот ход сейчас недопустим") ErrUrGameOver = errors.New("партия уже завершена") ) // NewUrGame создаёт новую партию; firstPlayer — кто ходит первым. func NewUrGame(firstPlayer UrPlayer) *UrGameState { return &UrGameState{CurrentPlayer: firstPlayer, DiceValue: -1} } // urSharedIndex переводит позицию пути (5..12) в индекс общей // клетки (0..7), одинаковый для обоих игроков. func urSharedIndex(pathIndex int) int { return pathIndex - 5 } // occupantOnShared возвращает (игрок, индекс_шашки, есть_ли), если // на общей клетке sharedIdx кто-то стоит. func (g *UrGameState) occupantOnShared(sharedIdx int) (UrPlayer, int, bool) { for _, p := range [2]UrPlayer{UrPlayerA, UrPlayerB} { for i, pos := range g.Path[p] { if pos >= 5 && pos <= 12 && urSharedIndex(pos) == sharedIdx { return p, i, true } } } return 0, 0, false } // occupantOnPrivate возвращает индекс шашки игрока player, если // какая-то из его же шашек стоит на его собственной клетке пути // pathIndex (1-4 или 13-14 — эти клетки не пересекаются с другим // игроком физически). func (g *UrGameState) occupantOnPrivate(player UrPlayer, pathIndex int) (int, bool) { for i, pos := range g.Path[player] { if pos == pathIndex { return i, true } } return 0, false } // UrMove — ход одной шашкой (её индекс в Path[player]). type UrMove struct { PieceIdx int } // LegalMoves перечисляет индексы шашек, которыми можно сходить // прямо сейчас выпавшим значением костей. func (g *UrGameState) LegalMoves() []UrMove { if g.Result != nil || !g.Rolled || g.DiceValue == 0 { return nil } player := g.CurrentPlayer var out []UrMove for i, pos := range g.Path[player] { if pos == UrPathLength+1 { continue // уже снята с доски } dest := pos + g.DiceValue if dest > UrPathLength+1 { continue // перелёт — нельзя, нужен точный бросок для выхода } if g.canOccupy(player, dest) { out = append(out, UrMove{PieceIdx: i}) } } return out } // canOccupy проверяет, может ли шашка игрока player встать на // позицию пути dest (1..14 — на доске, 15 — снятие с доски). func (g *UrGameState) canOccupy(player UrPlayer, dest int) bool { if dest == UrPathLength+1 { return true // снятие с доски всегда допустимо при точном броске } if dest >= 1 && dest <= 4 || dest >= 13 && dest <= 14 { // частная клетка — конфликт возможен только со своей же шашкой _, occupied := g.occupantOnPrivate(player, dest) return !occupied } // общая клетка (5..12) sharedIdx := urSharedIndex(dest) occ, _, occupied := g.occupantOnShared(sharedIdx) if !occupied { return true } if occ == player { return false // своя шашка уже там } // чужая шашка — можно "взять", если клетка не защищена розеткой return !urRosettePathIndices[dest] } // Roll бросает кости (сумма четырёх бинарных значений — 0..4, // биномиальное распределение, как и в оригинале с четырьмя // тетраэдрическими костями). Если после броска нет ни одного // легального хода — ход сразу переходит сопернику. func (g *UrGameState) Roll(value int) error { if g.Result != nil { return ErrUrGameOver } if g.Rolled { return ErrUrAlreadyRolled } g.DiceValue = value g.Rolled = true if value == 0 || len(g.LegalMoves()) == 0 { g.endTurn(false) } return nil } // Move передвигает шашку pieceIdx на выпавшее число костей. // Розетка даёт дополнительный бросок тем же игроком; взятие // отправляет шашку соперника обратно в руку. func (g *UrGameState) Move(pieceIdx int) error { if g.Result != nil { return ErrUrGameOver } if !g.Rolled { return ErrUrMustRollFirst } legal := g.LegalMoves() found := false for _, mv := range legal { if mv.PieceIdx == pieceIdx { found = true break } } if !found { return ErrUrIllegalMove } player := g.CurrentPlayer pos := g.Path[player][pieceIdx] dest := pos + g.DiceValue if dest >= 5 && dest <= 12 { if occ, idx, occupied := g.occupantOnShared(urSharedIndex(dest)); occupied && occ != player { g.Path[occ][idx] = 0 // взятие — шашка соперника уходит в руку } } g.Path[player][pieceIdx] = dest if g.borneOffCount(player) == UrPieceCount { g.Result = &UrResult{Winner: player} return nil } rosette := urRosettePathIndices[dest] && dest != UrPathLength+1 g.endTurn(rosette) return nil } func (g *UrGameState) borneOffCount(player UrPlayer) int { n := 0 for _, pos := range g.Path[player] { if pos == UrPathLength+1 { n++ } } return n } // endTurn завершает текущий ход. Если extraTurn (розетка) — тот же // игрок бросает снова; иначе очередь переходит сопернику. func (g *UrGameState) endTurn(extraTurn bool) { g.Rolled = false g.DiceValue = -1 if !extraTurn { g.CurrentPlayer = urOpponent(g.CurrentPlayer) } }