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