From 20cf75899931dff1afa0c263233d4f9142f31c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20=C3=87engel?= <71849938+omer-cengel@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:39:06 +0300 Subject: [PATCH] feat: Implement individual ignore options for automatic draws (#80) --- game.go | 55 +++++++++++++++++++++++++++++++--------- game_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 12 deletions(-) diff --git a/game.go b/game.go index 49cee8d..9d47a11 100644 --- a/game.go +++ b/game.go @@ -86,14 +86,16 @@ type TagPairs map[string]string // A Game represents a single chess game. type Game struct { - pos *Position // Current position - outcome Outcome // Game result - tagPairs TagPairs // PGN tag pairs - rootMove *Move // Root of move tree - currentMove *Move // Current position in tree - comments [][]string // Game comments - method Method // How the game ended - ignoreAutomaticDraws bool // Flag for automatic draw handling + pos *Position // Current position + outcome Outcome // Game result + tagPairs TagPairs // PGN tag pairs + rootMove *Move // Root of move tree + currentMove *Move // Current position in tree + comments [][]string // Game comments + method Method // How the game ended + ignoreFivefoldRepetitionDraw bool // Flag for automatic FivefoldRepetition draw handling + ignoreSeventyFiveMoveRuleDraw bool // Flag for automatic SeventyFiveMoveRule draw handling + ignoreInsufficientMaterialDraw bool // Flag for automatic InsufficientMaterial draw handling } // PGN takes a reader and returns a function that updates @@ -688,19 +690,19 @@ func (g *Game) evaluatePositionStatus() { } // five fold rep creates automatic draw - if !g.ignoreAutomaticDraws && g.numOfRepetitions() >= 5 { + if !g.ignoreFivefoldRepetitionDraw && g.numOfRepetitions() >= 5 { g.outcome = Draw g.method = FivefoldRepetition } // 75 move rule creates automatic draw - if !g.ignoreAutomaticDraws && g.pos.halfMoveClock >= 150 && g.method != Checkmate { + if !g.ignoreSeventyFiveMoveRuleDraw && g.pos.halfMoveClock >= 150 && g.method != Checkmate { g.outcome = Draw g.method = SeventyFiveMoveRule } // insufficient material creates automatic draw - if !g.ignoreAutomaticDraws && !g.pos.board.hasSufficientMaterial() { + if !g.ignoreInsufficientMaterialDraw && !g.pos.board.hasSufficientMaterial() { g.outcome = Draw g.method = InsufficientMaterial } @@ -718,7 +720,9 @@ func (g *Game) copy(game *Game) { g.outcome = game.outcome g.method = game.method g.comments = game.Comments() - g.ignoreAutomaticDraws = game.ignoreAutomaticDraws + g.ignoreFivefoldRepetitionDraw = game.ignoreFivefoldRepetitionDraw + g.ignoreSeventyFiveMoveRuleDraw = game.ignoreSeventyFiveMoveRuleDraw + g.ignoreInsufficientMaterialDraw = game.ignoreInsufficientMaterialDraw } // Clone returns a deep copy of the game. @@ -1043,3 +1047,30 @@ func ValidateSAN(s string) error { _, err := algebraicNotationParts(s) return err } + +// IgnoreFivefoldRepetitionDraw returns a Game option that disables automatic draws +// caused by the fivefold repetition rule. When applied, the game will not +// automatically end in a draw if the same position occurs five times. +func IgnoreFivefoldRepetitionDraw() func(*Game) { + return func(g *Game) { + g.ignoreFivefoldRepetitionDraw = true + } +} + +// IgnoreSeventyFiveMoveRuleDraw returns a Game option that disables automatic draws +// triggered by the seventy-five move rule. When applied, the game will not +// automatically end in a draw if one hundred fifty half-moves pass without a pawn move or capture. +func IgnoreSeventyFiveMoveRuleDraw() func(*Game) { + return func(g *Game) { + g.ignoreSeventyFiveMoveRuleDraw = true + } +} + +// IgnoreInsufficientMaterialDraw returns a Game option that disables automatic draws +// caused by insufficient material. When applied, the game will not automatically +// end in a draw even if checkmate is impossible with the remaining pieces. +func IgnoreInsufficientMaterialDraw() func(*Game) { + return func(g *Game) { + g.ignoreInsufficientMaterialDraw = true + } +} diff --git a/game_test.go b/game_test.go index 6e53e72..f9a7cd9 100644 --- a/game_test.go +++ b/game_test.go @@ -147,6 +147,24 @@ func TestFiveFoldRepetition(t *testing.T) { } } +func TestFiveFoldRepetitionIgnored(t *testing.T) { + g := NewGame(IgnoreFivefoldRepetitionDraw()) + moves := []string{ + "Nf3", "Nf6", "Ng1", "Ng8", + "Nf3", "Nf6", "Ng1", "Ng8", + "Nf3", "Nf6", "Ng1", "Ng8", + "Nf3", "Nf6", "Ng1", "Ng8", + } + for _, m := range moves { + if err := g.PushMove(m, nil); err != nil { + t.Fatal(err) + } + } + if g.Outcome() == Draw && g.Method() == FivefoldRepetition { + t.Fatal("automatically draw after five repetitions should be ignored") + } +} + func TestFiftyMoveRule(t *testing.T) { fen, _ := FEN("2r3k1/1q1nbppp/r3p3/3pP3/pPpP4/P1Q2N2/2RN1PPP/2R4K b - b3 100 60") g := NewGame(fen) @@ -174,6 +192,17 @@ func TestSeventyFiveMoveRule(t *testing.T) { } } +func TestSeventyFiveMoveRuleIgnored(t *testing.T) { + fen, _ := FEN("2r3k1/1q1nbppp/r3p3/3pP3/pPpP4/P1Q2N2/2RN1PPP/2R4K b - b3 149 80") + g := NewGame(fen, IgnoreSeventyFiveMoveRuleDraw()) + if err := g.PushMove("Kf8", nil); err != nil { + t.Fatal(err) + } + if g.Outcome() == Draw && g.Method() == SeventyFiveMoveRule { + t.Fatal("automatically draw after seventy five moves w/ no pawn move or capture should be ignored") + } +} + func TestInsufficientMaterial(t *testing.T) { fens := []string{ "8/2k5/8/8/8/3K4/8/8 w - - 1 1", @@ -195,6 +224,27 @@ func TestInsufficientMaterial(t *testing.T) { } } +func TestInsufficientMaterialIgnored(t *testing.T) { + fens := []string{ + "8/2k5/8/8/8/3K4/8/8 w - - 1 1", + "8/2k5/8/8/8/3K1N2/8/8 w - - 1 1", + "8/2k5/8/8/8/3K1B2/8/8 w - - 1 1", + "8/2k5/2b5/8/8/3K1B2/8/8 w - - 1 1", + "4b3/2k5/2b5/8/8/3K1B2/8/8 w - - 1 1", + } + for _, f := range fens { + fen, err := FEN(f) + if err != nil { + t.Fatal(err) + } + g := NewGame(IgnoreInsufficientMaterialDraw(), fen) + if g.Outcome() == Draw && g.Method() == InsufficientMaterial { + log.Println(g.Position().Board().Draw()) + t.Fatalf("%s automatically draw by insufficient material should be ignored", f) + } + } +} + func TestSufficientMaterial(t *testing.T) { fens := []string{ "8/2k5/8/8/8/3K1B2/4N3/8 w - - 1 1", @@ -2190,3 +2240,24 @@ func TestPushNotationMoveVsUnsafePushNotationMovePerformance(t *testing.T) { t.Logf("Warning: UnsafePushNotationMove wasn't faster than PushNotationMove - this might be expected for simple positions") } } + +func TestIgnoreFivefoldRepetitionDraw(t *testing.T) { + g := NewGame(IgnoreFivefoldRepetitionDraw()) + if !g.ignoreFivefoldRepetitionDraw { + t.Fatal("ignoreFivefoldRepetitionDraw should be true after being ignored") + } +} + +func TestIgnoreSeventyFiveMoveRuleDraw(t *testing.T) { + g := NewGame(IgnoreSeventyFiveMoveRuleDraw()) + if !g.ignoreSeventyFiveMoveRuleDraw { + t.Fatal("ignoreSeventyFiveMoveRuleDraw should be true after being ignored") + } +} + +func TestIgnoreInsufficientMaterialDraw(t *testing.T) { + g := NewGame(IgnoreInsufficientMaterialDraw()) + if !g.ignoreInsufficientMaterialDraw { + t.Fatal("ignoreInsufficientMaterialDraw should be true after being ignored") + } +}