245 lines
9.2 KiB
Go
245 lines
9.2 KiB
Go
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)
|
||
}
|
||
}
|