package main

import "errors"

// NardyVariant — короткие или длинные нарды. Отличия: начальная
// расстановка, направление движения второго игрока относительно
// первого, и (главное) возможность взятия шашек — в длинных нардах
// её нет вовсе: занятая соперником точка (даже одной шашкой)
// заблокирована полностью, тогда как в коротких одинокая шашка
// соперника ("блот") сбивается и уходит на бар.
type NardyVariant int

const (
	NardyShort NardyVariant = iota
	NardyLong
)

// NardyPlayer — цвет шашек игрока.
type NardyPlayer int

const (
	NardyWhite NardyPlayer = iota
	NardyBlack
)

func nardyOpponent(p NardyPlayer) NardyPlayer {
	if p == NardyWhite {
		return NardyBlack
	}
	return NardyWhite
}

// NardyPhase — фаза текущего хода.
type NardyPhase int

const (
	NardyPhaseRoll NardyPhase = iota // нужно бросить кости
	NardyPhaseMove                   // кости брошены, есть неиспользованные значения
	NardyPhaseOver
)

// NardyPoint — состояние одной точки доски в АБСОЛЮТНЫХ координатах
// (0-23, общих для обоих игроков — доска физически одна).
type NardyPoint struct {
	Owner NardyPlayer
	Count int
}

// NardyMove — одно единичное перемещение одной шашки одним
// значением кости.
type NardyMove struct {
	FromBar bool // ход шашкой с бара (только короткие нарды)
	From    int  // относительная (своя) точка-источник; не используется при FromBar
	Die     int
	BearOff bool // это снятие шашки с доски насовсем
}

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

// NardyGameState — полное состояние партии в нарды.
type NardyGameState struct {
	Variant NardyVariant

	// Board — 24 абсолютные точки, общие для обоих игроков.
	Board [24]NardyPoint

	BorneOff [2]int // сколько шашек снято с доски (индекс — NardyPlayer)
	Bar      [2]int // сколько шашек на баре (только короткие нарды; в длинных всегда 0)

	CurrentPlayer NardyPlayer
	Phase         NardyPhase

	Dice     [2]int // выпавшие значения последнего броска
	DiceLeft []int  // оставшиеся неиспользованные значения в этом ходу (4 при дубле)

	headMovesThisTurn int // сколько шашек уже взято "с головы" в этом ходу

	Result *NardyResult
}

var (
	ErrNardyWrongPhase  = errors.New("недопустимое действие для текущей фазы")
	ErrNardyIllegalMove = errors.New("такой ход сейчас недопустим")
)

// nardyPointToAbsolute переводит "относительную" точку игрока
// (0..23, где 23 — голова/дальняя от дома точка, 0..5 — дом) в
// абсолютную точку доски, в зависимости от варианта игры.
//
// Короткие нарды: игроки движутся навстречу друг другу — точки
// зеркальны (белый: abs=rel; чёрный: abs=23-rel).
//
// Длинные нарды: игроки движутся В ОДНОМ направлении — раскладка
// чёрного просто сдвинута на половину круга (белый: abs=rel;
// чёрный: abs=(rel+12) mod 24), а не отражена.
func nardyPointToAbsolute(variant NardyVariant, player NardyPlayer, rel int) int {
	if player == NardyWhite {
		return rel
	}
	if variant == NardyLong {
		return (rel + 12) % 24
	}
	return 23 - rel
}

// nardyStartingSetup — начальная расстановка в ОТНОСИТЕЛЬНЫХ
// координатах (одинакова для обоих игроков относительно их
// собственной головы).
func nardyStartingSetup(variant NardyVariant) map[int]int {
	if variant == NardyLong {
		return map[int]int{23: 15}
	}
	return map[int]int{23: 2, 12: 5, 7: 3, 5: 5}
}

// NewNardyGame создаёт новую партию с заданным вариантом правил;
// firstPlayer — кто ходит первым (по правилам определяется броском
// одного зара каждым игроком — эта логика оставлена вызывающему
// коду, здесь просто принимается готовый результат).
func NewNardyGame(variant NardyVariant, firstPlayer NardyPlayer) *NardyGameState {
	g := &NardyGameState{
		Variant:       variant,
		CurrentPlayer: firstPlayer,
		Phase:         NardyPhaseRoll,
	}
	setup := nardyStartingSetup(variant)
	for _, player := range [2]NardyPlayer{NardyWhite, NardyBlack} {
		for rel, count := range setup {
			abs := nardyPointToAbsolute(variant, player, rel)
			g.Board[abs] = NardyPoint{Owner: player, Count: count}
		}
	}
	return g
}

// canLand проверяет, может ли игрок player поставить шашку на свою
// относительную точку rel.
func (g *NardyGameState) canLand(player NardyPlayer, rel int) bool {
	abs := nardyPointToAbsolute(g.Variant, player, rel)
	pt := g.Board[abs]
	if pt.Count == 0 {
		return true
	}
	if pt.Owner == player {
		return true
	}
	if g.Variant == NardyLong {
		return false // в длинных нардах взятия нет — даже одна чужая шашка блокирует точку насмерть
	}
	return pt.Count == 1 // короткие нарды: можно сбить одинокий блот
}

// allCheckersHome проверяет, все ли шашки игрока уже в доме
// (относительные точки 0-5) и не на баре — необходимое условие для
// начала снятия шашек с доски.
func (g *NardyGameState) allCheckersHome(player NardyPlayer) bool {
	if g.Bar[player] > 0 {
		return false
	}
	for rel := 6; rel <= 23; rel++ {
		abs := nardyPointToAbsolute(g.Variant, player, rel)
		if g.Board[abs].Owner == player && g.Board[abs].Count > 0 {
			return false
		}
	}
	return true
}

// hasCheckerBeyond проверяет, есть ли у игрока шашки на
// относительных точках дома строго дальше rel (т.е. на точках,
// которые ещё не готовы сниматься точным значением кости) —
// используется, чтобы разрешить "снятие с избытком".
func (g *NardyGameState) hasCheckerBeyond(player NardyPlayer, rel int) bool {
	for r := rel + 1; r <= 5; r++ {
		abs := nardyPointToAbsolute(g.Variant, player, r)
		if g.Board[abs].Owner == player && g.Board[abs].Count > 0 {
			return true
		}
	}
	return false
}

// candidateMovesForDie перечисляет все ходы, физически возможные для
// текущего игрока при использовании ОДНОГО значения кости die (без
// учёта правила "обязаны использовать максимум костей" — эта
// проверка делается на уровне LegalMovesNow).
func (g *NardyGameState) candidateMovesForDie(die int) []NardyMove {
	player := g.CurrentPlayer
	var out []NardyMove

	if g.Bar[player] > 0 {
		entryRel := 24 - die
		if g.canLand(player, entryRel) {
			out = append(out, NardyMove{FromBar: true, Die: die})
		}
		return out
	}

	allHome := g.allCheckersHome(player)
	for rel := 23; rel >= 0; rel-- {
		abs := nardyPointToAbsolute(g.Variant, player, rel)
		pt := g.Board[abs]
		if pt.Count == 0 || pt.Owner != player {
			continue
		}
		if rel == 23 && g.headMovesThisTurn >= 1 {
			continue // с головы — не больше одной шашки за ход
		}

		dest := rel - die
		if dest >= 0 {
			if g.canLand(player, dest) {
				out = append(out, NardyMove{From: rel, Die: die})
			}
			continue
		}
		if !allHome {
			continue
		}
		if dest == -1 {
			out = append(out, NardyMove{From: rel, Die: die, BearOff: true})
		} else if !g.hasCheckerBeyond(player, rel) {
			out = append(out, NardyMove{From: rel, Die: die, BearOff: true})
		}
	}
	return out
}

// cloneForSearch делает независимую копию состояния, достаточную
// для перебора вариантов хода (для реальной мутации самой партии не
// используется).
//
// ВАЖНО: DiceLeft — срез, и поверхностное копирование структуры
// (c := *g) делило бы его нижележащий массив с оригиналом. Тогда
// планирование бота (применяющее ходы к "клону" через applyMoveRaw
// + removeDieOnce) тихо портило бы DiceLeft настоящей партии же до
// того, как ходы были бы реально применены — тот же класс ошибки,
// что уже однажды ловился в раздаче карт Тонка.
func (g *NardyGameState) cloneForSearch() *NardyGameState {
	c := *g
	c.DiceLeft = append([]int{}, g.DiceLeft...)
	return &c
}

// applyMoveRaw применяет одиночный ход БЕЗ проверки легальности
// (легальность уже должна быть проверена вызывающим кодом) и без
// смены игрока/фазы — используется и реальным ApplyMove, и
// внутренним перебором вариантов.
func (g *NardyGameState) applyMoveRaw(mv NardyMove) {
	player := g.CurrentPlayer

	if mv.FromBar {
		entryRel := 24 - mv.Die
		abs := nardyPointToAbsolute(g.Variant, player, entryRel)
		g.hitIfBlot(player, abs)
		g.Board[abs].Owner = player
		g.Board[abs].Count++
		g.Bar[player]--
		return
	}

	fromAbs := nardyPointToAbsolute(g.Variant, player, mv.From)
	g.Board[fromAbs].Count--
	if mv.From == 23 {
		g.headMovesThisTurn++
	}

	if mv.BearOff {
		g.BorneOff[player]++
		return
	}

	destRel := mv.From - mv.Die
	destAbs := nardyPointToAbsolute(g.Variant, player, destRel)
	g.hitIfBlot(player, destAbs)
	g.Board[destAbs].Owner = player
	g.Board[destAbs].Count++
}

// hitIfBlot сбивает одинокую шашку соперника на абсолютной точке
// abs, если она там есть (только короткие нарды — в длинных
// canLand уже не пустит на клетку с чужой шашкой вовсе, так что
// здесь этот случай просто никогда не наступит для длинных нард).
func (g *NardyGameState) hitIfBlot(player NardyPlayer, abs int) {
	pt := g.Board[abs]
	if pt.Count == 1 && pt.Owner != player {
		g.Bar[pt.Owner]++
		g.Board[abs] = NardyPoint{}
	}
}

func (g *NardyGameState) removeDieOnce(die int) {
	for i, d := range g.DiceLeft {
		if d == die {
			g.DiceLeft = append(g.DiceLeft[:i], g.DiceLeft[i+1:]...)
			return
		}
	}
}

// nardyMaxDiceUsableFrom считает, сколько костей максимум можно
// использовать целиком (0..len(dice)) при данном состоянии доски и
// списке оставшихся значений костей — перебором всех
// последовательностей ходов. Именно эта функция обеспечивает
// правило "обязаны использовать максимально возможное число костей".
func (g *NardyGameState) nardyMaxDiceUsableFrom(dice []int) int {
	best := 0
	seenDie := map[int]bool{}
	for i, d := range dice {
		if seenDie[d] {
			continue
		}
		seenDie[d] = true
		for _, mv := range g.candidateMovesForDie(d) {
			clone := g.cloneForSearch()
			clone.applyMoveRaw(mv)
			rest := make([]int, 0, len(dice)-1)
			removed := false
			for j, dv := range dice {
				if j == i && !removed {
					removed = true
					continue
				}
				rest = append(rest, dv)
			}
			sub := 1 + clone.nardyMaxDiceUsableFrom(rest)
			if sub > best {
				best = sub
			}
		}
	}
	return best
}

// LegalMovesNow возвращает все ходы, легальные ПРЯМО СЕЙЧАС — с
// учётом того, что до конца хода нужно использовать максимально
// возможное число оставшихся костей (см. nardyMaxDiceUsableFrom), и
// что если можно сыграть только ОДНУ кость из двух РАЗНЫХ значений
// (не дубль), обязана быть сыграна БОЛЬШАЯ — стандартное правило нард.
func (g *NardyGameState) LegalMovesNow() []NardyMove {
	if g.Phase != NardyPhaseMove || len(g.DiceLeft) == 0 {
		return nil
	}
	needed := g.nardyMaxDiceUsableFrom(g.DiceLeft)
	if needed == 0 {
		return nil
	}

	if needed == 1 {
		unique := map[int]bool{}
		for _, d := range g.DiceLeft {
			unique[d] = true
		}
		if len(unique) == 2 {
			bigger := 0
			for d := range unique {
				if d > bigger {
					bigger = d
				}
			}
			if biggerMoves := g.candidateMovesForDie(bigger); len(biggerMoves) > 0 {
				return biggerMoves
			}
		}
	}

	var out []NardyMove
	seenDie := map[int]bool{}
	for _, d := range g.DiceLeft {
		if seenDie[d] {
			continue
		}
		seenDie[d] = true
		for _, mv := range g.candidateMovesForDie(d) {
			clone := g.cloneForSearch()
			clone.applyMoveRaw(mv)
			rest := make([]int, 0, len(g.DiceLeft)-1)
			removedOne := false
			for _, dv := range g.DiceLeft {
				if dv == d && !removedOne {
					removedOne = true
					continue
				}
				rest = append(rest, dv)
			}
			if 1+clone.nardyMaxDiceUsableFrom(rest) >= needed {
				out = append(out, mv)
			}
		}
	}
	return out
}

// Roll бросает кости и переходит в фазу хода. Если после броска не
// находится ни одного легального хода — очки сгорают, ход сразу же
// переходит следующему игроку.
func (g *NardyGameState) Roll(d1, d2 int) {
	g.Dice = [2]int{d1, d2}
	if d1 == d2 {
		g.DiceLeft = []int{d1, d1, d1, d1}
	} else {
		g.DiceLeft = []int{d1, d2}
	}
	g.headMovesThisTurn = 0
	g.Phase = NardyPhaseMove
	if len(g.LegalMovesNow()) == 0 {
		g.endTurn()
	}
}

// ApplyMove выполняет один из ходов, предложенных LegalMovesNow.
func (g *NardyGameState) ApplyMove(mv NardyMove) error {
	if g.Phase != NardyPhaseMove {
		return ErrNardyWrongPhase
	}
	legal := g.LegalMovesNow()
	found := false
	for _, l := range legal {
		if l == mv {
			found = true
			break
		}
	}
	if !found {
		return ErrNardyIllegalMove
	}

	g.applyMoveRaw(mv)
	g.removeDieOnce(mv.Die)

	if g.BorneOff[g.CurrentPlayer] == 15 {
		g.Result = &NardyResult{Winner: g.CurrentPlayer}
		g.Phase = NardyPhaseOver
		return nil
	}

	if len(g.LegalMovesNow()) == 0 {
		g.endTurn()
	}
	return nil
}

func (g *NardyGameState) endTurn() {
	g.DiceLeft = nil
	g.Dice = [2]int{}
	g.CurrentPlayer = nardyOpponent(g.CurrentPlayer)
	g.Phase = NardyPhaseRoll
}
