327 lines
11 KiB
Go
327 lines
11 KiB
Go
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)
|
||
}
|
||
}
|