go_games_collection/ur_game.go

245 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}