119 lines
4.2 KiB
Go
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
|
|
}
|