Files
chess/internal/server/replay/verify.go
T

119 lines
4.2 KiB
Go

package replay
import (
"fmt"
"github.com/lixenwraith/chess/internal/server/chess"
"github.com/lixenwraith/chess/internal/server/core"
"github.com/lixenwraith/chess/internal/server/storage"
)
// Verify checks a stored game against the rules and returns one line per
// problem, empty when the game is consistent:
//
// - the initial position parses, and moves are numbered 1..n with no gap
// (a gap means rows were deleted);
// - each move is made by the side to move, is legal from the stored
// position before it, and produces the stored position after it;
// - the result and its termination belong together, and a termination
// decided by the position (mate, stalemate, dead material, repetition,
// fifty moves) holds in the final position;
// - a mated or stalemated final position has its result recorded.
//
// Draw rules that became automatic later are not required of older games:
// an unfinished game in a drawn position is not a problem.
func Verify(record *storage.GameRecord, moves []storage.MoveRecord) []string {
var problems []string
pos, err := chess.ParseFEN(record.InitialFEN)
if err != nil {
return []string{fmt.Sprintf("initial position: %v", err)}
}
positions := []*chess.Position{pos}
for i, move := range moves {
if move.MoveNumber != i+1 {
return append(problems, fmt.Sprintf("ply %d missing (next stored ply is %d)", i+1, move.MoveNumber))
}
if move.PlayerColor != pos.Turn().String() {
problems = append(problems, fmt.Sprintf("ply %d %s: stored as played by %q, but %q is to move",
move.MoveNumber, move.MoveUCI, move.PlayerColor, pos.Turn().String()))
}
m, err := pos.ParseUCI(move.MoveUCI)
if err != nil {
problems = append(problems, fmt.Sprintf("ply %d %s: %v", move.MoveNumber, move.MoveUCI, err))
} else if want := pos.Play(m).FEN(); !sameFEN(want, move.FENAfterMove) {
problems = append(problems, fmt.Sprintf("ply %d %s: stored %q, rules give %q",
move.MoveNumber, move.MoveUCI, move.FENAfterMove, want))
}
// Continue from the stored position so one fault is reported once.
if pos, err = chess.ParseFEN(move.FENAfterMove); err != nil {
return append(problems, fmt.Sprintf("ply %d stored position: %v", move.MoveNumber, err))
}
positions = append(positions, pos)
}
return append(problems, verifyOutcome(record.Result, record.Termination, positions)...)
}
func verifyOutcome(result, termination string, positions []*chess.Position) []string {
final := positions[len(positions)-1]
hasMoves := final.HasLegalMoves()
mated := !hasMoves && final.InCheck()
stalemated := !hasMoves && !final.InCheck()
if result == "" {
switch {
case termination != "":
return []string{fmt.Sprintf("termination %q without a result", termination)}
case mated:
return []string{"final position is checkmate but no result is stored"}
case stalemated:
return []string{"final position is stalemate but no result is stored"}
}
return nil
}
state, ok := stateOf(result)
if !ok || !core.Termination(termination).ValidFor(state) {
return []string{fmt.Sprintf("result %q cannot come from termination %q", result, termination)}
}
var holds bool
switch core.Termination(termination) {
case core.TermCheckmate:
// The side to move is the one mated.
holds = mated && (final.Turn() == chess.White) == (result == "black_wins")
case core.TermStalemate:
holds = stalemated
case core.TermInsufficientMaterial:
holds = final.InsufficientMaterial()
case core.TermThreefoldRepetition:
holds = chess.Repetitions(positions) >= 3
case core.TermFiftyMoveRule:
holds = final.Halfmove() >= 100
case core.TermResignation, core.TermAgreement:
holds = true // decided by the players, not the position
}
if !holds {
return []string{fmt.Sprintf("stored %s by %s does not hold in the final position", result, termination)}
}
return nil
}
func stateOf(result string) (core.State, bool) {
switch result {
case "white_wins":
return core.StateWhiteWins, true
case "black_wins":
return core.StateBlackWins, true
case "stalemate":
return core.StateStalemate, true
case "draw":
return core.StateDraw, true
}
return 0, false
}
func sameFEN(a, b string) bool {
na, errA := chess.NormalizeFEN(a)
nb, errB := chess.NormalizeFEN(b)
return errA == nil && errB == nil && na == nb
}