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