Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,17 @@ type engine struct{}
// Each move is validated to ensure it doesn't leave the king in check
func (engine) CalcMoves(pos *Position, first bool) []Move {
// generate possible moves
moves := standardMoves(pos, first)
moves := standardMoves(pos, first, false)
// return moves including castles
return append(moves, castleMoves(pos)...)
}

// UnsafeMoves returns all pseudo-legal moves that are illegal because they
// leave the moving side's king in check.
func (engine) UnsafeMoves(pos *Position) []Move {
return standardMoves(pos, false, true)
}

// Status returns the current game status (Checkmate, Stalemate, or NoMethod)
// based on the position.
//
Expand Down Expand Up @@ -89,7 +95,7 @@ var movePool = sync.Pool{
//
// The function uses a sync.Pool of move arrays to reduce allocations. Each
// move is validated to ensure it doesn't leave the king in check.
func standardMoves(pos *Position, first bool) []Move {
func standardMoves(pos *Position, first bool, unsafeOnly bool) []Move {
moves, _ := movePool.Get().(*[maxPossibleMoves]Move)
defer movePool.Put(moves)
count := 0
Expand Down Expand Up @@ -132,7 +138,7 @@ func standardMoves(pos *Position, first bool) []Move {
for _, pt := range promoPieceTypes {
m.promo = pt
addTags(&m, pos)
if !m.HasTag(inCheck) {
if m.HasTag(inCheck) == unsafeOnly {
// Copy the valid move to the array
moves[count] = m
count++
Expand All @@ -147,7 +153,7 @@ func standardMoves(pos *Position, first bool) []Move {
} else {
m.promo = 0
addTags(&m, pos)
if !m.HasTag(inCheck) {
if m.HasTag(inCheck) == unsafeOnly {
moves[count] = m
count++
if first {
Expand Down
46 changes: 43 additions & 3 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func BenchmarkStandardMoves(b *testing.B) {
b.ReportAllocs()

for i := 0; i < b.N; i++ {
moves := standardMoves(bm.pos, bm.wantFirst)
moves := standardMoves(bm.pos, bm.wantFirst, false)
// Prevent compiler optimization
if len(moves) == 0 {
b.Fatal("unexpected zero moves")
Expand All @@ -61,7 +61,7 @@ func BenchmarkStandardMoves_PawnPromotions(b *testing.B) {
b.ReportAllocs()

for i := 0; i < b.N; i++ {
moves := standardMoves(pos, false)
moves := standardMoves(pos, false, false)
if len(moves) == 0 {
b.Fatal("unexpected zero moves")
}
Expand All @@ -85,7 +85,7 @@ func BenchmarkStandardMoves_BoardDensity(b *testing.B) {
b.Run(p.name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
moves := standardMoves(pos, false)
moves := standardMoves(pos, false, false)
if len(moves) == 0 && p.name != "Empty" {
b.Fatal("unexpected zero moves")
}
Expand Down Expand Up @@ -174,6 +174,46 @@ func TestAddTags(t *testing.T) {
}
}

func TestUnsafeMoves_StartingPosition(t *testing.T) {
pos := mustPosition("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
moves := engine{}.UnsafeMoves(pos)
if len(moves) != 0 {
t.Errorf("expected 0 unsafe moves in starting position, got %d", len(moves))
}
}

func TestUnsafeMoves_PinnedKnight(t *testing.T) {
pos := mustPosition("4k3/8/8/8/1b6/2N5/8/4K3 w - - 0 1")
moves := engine{}.UnsafeMoves(pos)
if len(moves) != 8 {
t.Fatalf("expected 8 unsafe moves for pinned knight, got %d", len(moves))
}
for _, m := range moves {
if m.s1 != C3 {
t.Errorf("expected unsafe move from C3, got %s", m.s1.String())
}
if !m.HasTag(inCheck) {
t.Errorf("expected unsafe move to have inCheck tag: %s", m.String())
}
}
}

func TestUnsafeMoves_KingIntoCheck(t *testing.T) {
pos := mustPosition("8/8/8/8/8/3r4/8/4K3 w - - 0 1")
moves := engine{}.UnsafeMoves(pos)
if len(moves) != 2 {
t.Fatalf("expected 2 unsafe moves, got %d", len(moves))
}
expected := map[string]bool{"d1": true, "d2": true}
for _, m := range moves {
if m.s1 != E1 {
t.Errorf("expected move from E1, got %s", m.s1.String())
}
if !expected[m.s2.String()] {
t.Errorf("unexpected unsafe move to %s", m.s2.String())
}
}
}
// Helper function to convert FEN to Position
func mustPosition(fen string) *Position {
fenObject, err := FEN(fen)
Expand Down
6 changes: 6 additions & 0 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ func (g *Game) ValidMoves() []Move {
return g.pos.ValidMoves()
}

// UnsafeMoves returns all pseudo-legal moves that leave the moving side's king in check.
// These moves are valid piece movements but illegal because they expose the king.
func (g *Game) UnsafeMoves() []Move {
return g.pos.UnsafeMoves()
}

// Moves returns the move history of the game following the main line.
func (g *Game) Moves() []*Move {
if g.rootMove == nil {
Expand Down
33 changes: 33 additions & 0 deletions game_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2515,3 +2515,36 @@ func TestMoveHistoryFromPGN(t *testing.T) {
}
}
}

func TestGameUnsafeMoves(t *testing.T) {
game := NewGame()
if len(game.UnsafeMoves()) != 0 {
t.Errorf("expected 0 unsafe moves at start, got %d", len(game.UnsafeMoves()))
}

// Verify delegation to Position
pos := game.Position()
if len(game.UnsafeMoves()) != len(pos.UnsafeMoves()) {
t.Errorf("Game.UnsafeMoves() length mismatch with Position.UnsafeMoves()")
}
}

func TestEscapeTagValue(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"plain", "hello", "hello"},
{"quote", `say "hi"`, `say \"hi\"`},
{"backslash", `path\to`, `path\\to`},
{"mixed", `a\"b`, `a\\\"b`},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := escapeTagValue(tc.input); got != tc.want {
t.Errorf("escapeTagValue(%q) = %q, want %q", tc.input, got, tc.want)
}
})
}
}
6 changes: 6 additions & 0 deletions position.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ func (pos *Position) ValidMoves() []Move {
return append([]Move(nil), pos.validMoves...)
}

// UnsafeMoves returns all pseudo-legal moves that are illegal because they leave
// the moving side's king in check. These moves should not be played via Move().
func (pos *Position) UnsafeMoves() []Move {
return engine{}.UnsafeMoves(pos)
}

// Status returns the position's status as one of the outcome methods.
// Possible returns values include Checkmate, Stalemate, and NoMethod.
func (pos *Position) Status() Method {
Expand Down
Loading