v0.13.0 web and terminal client enhance, replay api, linux server deploy, doc and dependency update
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
This commit is contained in:
1 parent
52ac0fe31a
commit
f1ac4cbbe7
71 files changed
+7677
-537
No files matched your search
@@ -0,0 +1,118 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"chess/internal/server/chess"
|
||||
"chess/internal/server/core"
|
||||
"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
|
||||
}
|
||||
Reference in new issue
Block a user