go_games_collection/senet_game.go

327 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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