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
12 changes: 6 additions & 6 deletions context/spec/001-chess-engine-foundation/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@
## Slice 8: Pawn Promotion
*Goal: Implement pawn promotion.*

- [ ] **Slice 8: Pawn promotion implementation**
- [ ] Update pawn move generation to require promotion piece when reaching 8th rank
- [ ] Update `MakeMove()` to replace pawn with promoted piece
- [ ] Update `ParseMove()` to handle promotion suffix ("e7e8q")
- [ ] Add unit tests for promotion to all 4 piece types (Q, R, B, N)
- [ ] Add unit test that promotion is required (move without promotion piece fails)
- [x] **Slice 8: Pawn promotion implementation**
- [x] Update pawn move generation to require promotion piece when reaching 8th rank
- [x] Update `MakeMove()` to replace pawn with promoted piece
- [x] Update `ParseMove()` to handle promotion suffix ("e7e8q")
- [x] Add unit tests for promotion to all 4 piece types (Q, R, B, N)
- [x] Add unit test that promotion is required (move without promotion piece fails)

---

Expand Down
23 changes: 23 additions & 0 deletions internal/engine/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,23 @@ func (b *Board) Copy() *Board {
// MakeMove applies a move to the board.
// It validates that the move is legal before applying it.
// Returns an error if the move is illegal (invalid piece, wrong color, or leaves king in check).
// For pawn promotion, the move must include a valid promotion piece (Queen, Rook, Bishop, Knight).
func (b *Board) MakeMove(m Move) error {
// Get the piece at the from square
piece := b.Squares[m.From]

// Check if this is a valid pawn promotion move missing the promotion piece
// Only trigger this error if the pawn is actually in position to promote (one rank away)
if piece.Type() == Pawn {
fromRank := m.From.Rank()
toRank := m.To.Rank()
isValidPromotion := (piece.Color() == White && fromRank == 6 && toRank == 7) ||
(piece.Color() == Black && fromRank == 1 && toRank == 0)
if isValidPromotion && m.Promotion == Empty {
return fmt.Errorf("pawn promotion requires specifying a piece (q, r, b, n)")
}
}

// Check if the move is legal using the full legality check
if !b.IsLegalMove(m) {
return fmt.Errorf("illegal move: %s", m.String())
Expand Down Expand Up @@ -151,6 +167,13 @@ func (b *Board) applyMove(m Move) {
b.Squares[m.To] = piece
b.Squares[m.From] = Piece(Empty)

// Handle pawn promotion: replace pawn with promoted piece
if piece.Type() == Pawn && m.Promotion != Empty {
// Create the promoted piece with the same color as the pawn
promotedPiece := NewPiece(piece.Color(), m.Promotion)
b.Squares[m.To] = promotedPiece
}

// Handle castling: if king moves 2 squares horizontally, also move the rook
if piece.Type() == King {
fileDiff := m.To.File() - m.From.File()
Expand Down
43 changes: 34 additions & 9 deletions internal/engine/moves.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,36 @@ func (b *Board) generatePawnMoves() []Move {
file := sq.File()
rank := sq.Rank()

// Determine promotion rank based on color
// White pawns promote on rank 7 (index 7), black pawns on rank 0
var promotionRank int
if b.ActiveColor == White {
promotionRank = 7
} else {
promotionRank = 0
}

// One square forward
forwardRank := rank + direction
if forwardRank >= 0 && forwardRank <= 7 {
forwardSq := NewSquare(file, forwardRank)
if b.Squares[forwardSq].IsEmpty() {
moves = append(moves, Move{From: sq, To: forwardSq})

// Two squares forward from starting position
if rank == startRank {
twoForwardRank := rank + 2*direction
twoForwardSq := NewSquare(file, twoForwardRank)
if b.Squares[twoForwardSq].IsEmpty() {
moves = append(moves, Move{From: sq, To: twoForwardSq})
// Check if this is a promotion move
if forwardRank == promotionRank {
// Generate 4 promotion moves
for _, promoType := range []PieceType{Queen, Rook, Bishop, Knight} {
moves = append(moves, Move{From: sq, To: forwardSq, Promotion: promoType})
}
} else {
moves = append(moves, Move{From: sq, To: forwardSq})

// Two squares forward from starting position
if rank == startRank {
twoForwardRank := rank + 2*direction
twoForwardSq := NewSquare(file, twoForwardRank)
if b.Squares[twoForwardSq].IsEmpty() {
moves = append(moves, Move{From: sq, To: twoForwardSq})
}
}
}
}
Expand All @@ -138,7 +155,15 @@ func (b *Board) generatePawnMoves() []Move {

// Can capture if there's an enemy piece
if !targetPiece.IsEmpty() && targetPiece.Color() != b.ActiveColor {
moves = append(moves, Move{From: sq, To: captureSq})
// Check if this is a promotion capture
if captureRank == promotionRank {
// Generate 4 promotion moves for capture
for _, promoType := range []PieceType{Queen, Rook, Bishop, Knight} {
moves = append(moves, Move{From: sq, To: captureSq, Promotion: promoType})
}
} else {
moves = append(moves, Move{From: sq, To: captureSq})
}
}
}
}
Expand Down
Loading
Loading