package main

import "errors"

// Сенет — древнеегипетская игра, известна с ок. 3500 до н.э.
// Подлинные правила не сохранились ни в одном источнике; здесь
// реализована консолидированная реконструкция (по Кендаллу и
// Жекье), наиболее часто используемая в современных цифровых
// версиях. Другие историки (Р. К. Белл и другие) предлагают
// заметно иные правила — это ОДНА из конкурирующих реконструкций,
// а не восстановленные подлинные правила игры.

// SenetPlayer — сторона игрока.
type SenetPlayer int

const (
	SenetPlayerA SenetPlayer = iota // начинает на нечётных клетках 1,3,5,7,9
	SenetPlayerB                    // начинает на чётных клетках 2,4,6,8,10
)

func senetOpponent(p SenetPlayer) SenetPlayer {
	if p == SenetPlayerA {
		return SenetPlayerB
	}
	return SenetPlayerA
}

// SenetPieceCount — число шашек у каждого игрока.
const SenetPieceCount = 5

// SenetBoardSize — число клеток доски (3 ряда по 10, змейкой).
const SenetBoardSize = 30

// Особые клетки в конце пути:
const (
	SenetHouseRebirth = 15 // безопасная клетка, "дом возрождения"
	SenetHouseBeauty  = 26 // безопасная клетка, "дом красоты"
	SenetHouseWater   = 27 // ловушка — отправляет обратно на 15
	SenetHouseThree   = 28 // безопасная; снять шашку можно только броском 3
	SenetHouseTwo     = 29 // безопасная; снять шашку можно только броском 2
)

var senetSafeSquares = map[int]bool{
	SenetHouseRebirth: true,
	SenetHouseBeauty:  true,
	SenetHouseThree:   true,
	SenetHouseTwo:     true,
}

// SenetGameState — состояние партии.
type SenetGameState struct {
	// Board[sq] для sq=1..30: 0 — пусто, иначе SenetPlayerA+1 или
	// SenetPlayerB+1 (чтобы 0 однозначно означал "пусто").
	Board [SenetBoardSize + 1]int

	BorneOff [2]int

	CurrentPlayer SenetPlayer
	DiceValue     int // 0 пока не брошено; иначе 1..5
	Rolled        bool

	Result *SenetResult
}

// SenetResult — итог завершённой партии.
type SenetResult struct {
	Winner SenetPlayer
}

var (
	ErrSenetMustRollFirst = errors.New("сначала нужно бросить кости")
	ErrSenetAlreadyRolled = errors.New("кости в этом ходу уже брошены")
	ErrSenetIllegalMove   = errors.New("этот ход сейчас недопустим")
	ErrSenetGameOver      = errors.New("партия уже завершена")
)

func senetCellOwner(v int) (SenetPlayer, bool) {
	if v == 0 {
		return 0, false
	}
	return SenetPlayer(v - 1), true
}

func senetCellValue(p SenetPlayer) int { return int(p) + 1 }

// NewSenetGame создаёт новую партию с классической начальной
// расстановкой: шашки обоих игроков чередуются на первых десяти
// клетках.
func NewSenetGame(firstPlayer SenetPlayer) *SenetGameState {
	g := &SenetGameState{CurrentPlayer: firstPlayer}
	for sq := 1; sq <= 10; sq++ {
		if sq%2 == 1 {
			g.Board[sq] = senetCellValue(SenetPlayerA)
		} else {
			g.Board[sq] = senetCellValue(SenetPlayerB)
		}
	}
	return g
}

// senetCornerBreak проверяет, разрывается ли последовательность
// подряд идущих клеток между a и a+1 (заграждения не поворачивают
// за угол доски — переходы 10→11 и 20→21 всегда прерывают цепочку).
func senetCornerBreak(a int) bool { return a == 10 || a == 20 }

// senetRunLength считает, сколько подряд идущих клеток (не
// пересекая углы доски) занято тем же игроком, что и клетка sq —
// используется и для защиты (2+), и для заграждения (3+).
func (g *SenetGameState) senetRunLength(sq int) int {
	owner, ok := senetCellOwner(g.Board[sq])
	if !ok {
		return 0
	}
	count := 1
	for s := sq - 1; s >= 1 && !senetCornerBreak(s) && g.Board[s] == senetCellValue(owner); s-- {
		count++
	}
	for s := sq + 1; s <= SenetBoardSize && !senetCornerBreak(s-1) && g.Board[s] == senetCellValue(owner); s++ {
		count++
	}
	return count
}

// senetPathBlocked проверяет, пересекает ли перемещение с клетки
// from на клетку to (шаг step: +1 вперёд или -1 назад) какую-либо
// клетку с заграждением (3+ подряд) соперника — заграждение нельзя
// не только занять, но и пройти насквозь.
func (g *SenetGameState) senetPathBlocked(from, to, step int) bool {
	for s := from + step; ; s += step {
		if s == to {
			return false
		}
		if s < 1 || s > SenetBoardSize {
			return false
		}
		if owner, ok := senetCellOwner(g.Board[s]); ok && owner != g.CurrentPlayer {
			if g.senetRunLength(s) >= 3 {
				return true
			}
		}
	}
}

// SenetMove — ход одной шашкой (её текущая клетка).
type SenetMove struct {
	From     int
	Backward bool // ход "назад" (см. правило отсутствия хода вперёд)
}

// canBearOffExact проверяет, можно ли СНЯТЬ шашку с клетки from
// именно этим значением кости (только клетки 28/29/30 — и только
// точным броском: 3, 2 и 1 соответственно).
func canBearOffExact(from, die int) bool {
	switch from {
	case SenetHouseThree:
		return die == 3
	case SenetBoardSize - 1: // 29
		return die == 2
	case SenetBoardSize: // 30
		return die == 1
	}
	return false
}

// canLandOn проверяет, может ли текущий игрок встать на клетку to
// (клетка уже гарантированно в пределах доски 1..30).
func (g *SenetGameState) canLandOn(to int) bool {
	owner, occupied := senetCellOwner(g.Board[to])
	if !occupied {
		return true
	}
	if owner == g.CurrentPlayer {
		return false // своя шашка уже там
	}
	if senetSafeSquares[to] {
		return false // безопасная клетка защищает соперника всегда
	}
	return g.senetRunLength(to) == 1 // одинокую шашку соперника можно "взять" обменом
}

// legalMovesForDirection перечисляет легальные ходы для текущего
// значения кости, используя шаг step (+1 вперёд, -1 назад).
func (g *SenetGameState) legalMovesForDirection(step int) []SenetMove {
	var out []SenetMove
	for sq := 1; sq <= SenetBoardSize; sq++ {
		owner, ok := senetCellOwner(g.Board[sq])
		if !ok || owner != g.CurrentPlayer {
			continue
		}
		dest := sq + step*g.DiceValue
		if step > 0 && dest > SenetBoardSize {
			if canBearOffExact(sq, g.DiceValue) {
				out = append(out, SenetMove{From: sq, Backward: false})
			}
			continue
		}
		if dest < 1 || dest > SenetBoardSize {
			continue
		}
		if g.senetPathBlocked(sq, dest, step) {
			continue
		}
		if !g.canLandOn(dest) {
			continue
		}
		out = append(out, SenetMove{From: sq, Backward: step < 0})
	}
	return out
}

// LegalMoves перечисляет ходы, легальные прямо сейчас: сперва
// пробуются ходы вперёд; если таких нет вовсе — по тем же правилам
// пробуются ходы назад (обязательное правило "нет хода вперёд —
// ходи назад"); если нет и таких — возвращается пустой список (ход
// сгорает).
func (g *SenetGameState) LegalMoves() []SenetMove {
	if g.Result != nil || !g.Rolled {
		return nil
	}
	forward := g.legalMovesForDirection(1)
	if len(forward) > 0 {
		return forward
	}
	return g.legalMovesForDirection(-1)
}

// Roll бросает кости (см. senetRollSticks для распределения
// значений исторических метательных палочек). Если ходов нет вовсе
// — ход сразу переходит сопернику.
func (g *SenetGameState) Roll(value int) error {
	if g.Result != nil {
		return ErrSenetGameOver
	}
	if g.Rolled {
		return ErrSenetAlreadyRolled
	}
	g.DiceValue = value
	g.Rolled = true
	if len(g.LegalMoves()) == 0 {
		g.endTurn(false)
	}
	return nil
}

// Move передвигает шашку с клетки from (как предложено LegalMoves).
func (g *SenetGameState) Move(from int) error {
	if g.Result != nil {
		return ErrSenetGameOver
	}
	if !g.Rolled {
		return ErrSenetMustRollFirst
	}
	legal := g.LegalMoves()
	var mv *SenetMove
	for i := range legal {
		if legal[i].From == from {
			mv = &legal[i]
			break
		}
	}
	if mv == nil {
		return ErrSenetIllegalMove
	}

	player := g.CurrentPlayer
	step := 1
	if mv.Backward {
		step = -1
	}
	dest := from + step*g.DiceValue

	if step > 0 && dest > SenetBoardSize {
		g.Board[from] = 0
		g.BorneOff[player]++
		if g.BorneOff[player] == SenetPieceCount {
			g.Result = &SenetResult{Winner: player}
			return nil
		}
		g.endTurn(g.diceGrantsExtraTurn())
		return nil
	}

	if owner, ok := senetCellOwner(g.Board[dest]); ok && owner != player {
		// обмен местами — шашка соперника переезжает туда, откуда мы ушли
		g.Board[from] = g.Board[dest]
	} else {
		g.Board[from] = 0
	}
	g.Board[dest] = senetCellValue(player)

	if dest == SenetHouseWater {
		g.bounceFromWater(dest)
	}

	g.endTurn(g.diceGrantsExtraTurn())
	return nil
}

// bounceFromWater отправляет шашку, только что попавшую в "дом
// воды" (клетка 27), обратно на клетку 15 — или на ближайшую
// свободную клетку перед ней, если 15 уже занята.
func (g *SenetGameState) bounceFromWater(from int) {
	owner := g.CurrentPlayer
	g.Board[from] = 0
	target := SenetHouseRebirth
	for target >= 1 && g.Board[target] != 0 {
		target--
	}
	if target < 1 {
		target = SenetHouseRebirth
	}
	g.Board[target] = senetCellValue(owner)
}

// diceGrantsExtraTurn — по историческому соглашению, броски 1, 4 и 5
// (но не 2 и 3) дают дополнительный ход.
func (g *SenetGameState) diceGrantsExtraTurn() bool {
	return g.DiceValue == 1 || g.DiceValue == 4 || g.DiceValue == 5
}

func (g *SenetGameState) endTurn(extraTurn bool) {
	g.Rolled = false
	g.DiceValue = 0
	if !extraTurn {
		g.CurrentPlayer = senetOpponent(g.CurrentPlayer)
	}
}
