Parse file and rank from move

I realized that ambiguous moves might require the file _and_ rank.

Also, ambiguous moves are not limited to capturing moves.
This commit is contained in:
ekzyis 2024-09-26 06:25:40 +02:00
parent 87856f694d
commit e6f9c529f8
2 changed files with 193 additions and 122 deletions

View File

@ -271,36 +271,20 @@ func (b *Board) Parse(pgn string) error {
func (b *Board) Move(move string) error { func (b *Board) Move(move string) error {
var ( var (
to string
piece string piece string
targetPosition string // if the move is ambiguous, the originating square rank must be given
// the x column from which the piece is captured. // see https://en.wikipedia.org/wiki/Algebraic_notation_(chess)#Disambiguating_moves
// for example, this would be 'e' for exd4 and 'e' for Nexd4. fromX int
captureFrom int fromY int
collisionPiece *Piece collisionPiece *Piece
err error err error
) )
if captureFrom, err = parseCaptureMove(move); err != nil { if piece, fromX, fromY, to, err = parseMove(move); err != nil {
return err return err
} }
if captureFrom != -1 {
parts := strings.Split(move, "x")
piece := parts[0]
position := parts[1]
if strings.ToLower(piece) == piece {
// pawn capture move like exd4
move = position
} else {
// piece capture move like Nbxd4
// remove the capture column from the piece
move = piece[:1] + position
}
} else {
// even if captureFrom is not set, it might still be a capture move just unambiguous like Nxe4
move = strings.Replace(move, "x", "", 1)
}
// TODO: parse ambiguous captures for all pieces // TODO: parse ambiguous captures for all pieces
// TODO: parse promotions // TODO: parse promotions
// TODO: parse checks e.g. e5+ // TODO: parse checks e.g. e5+
@ -311,40 +295,32 @@ func (b *Board) Move(move string) error {
// ( this avoids moving into check and moving a piece that exposes the king to check e.g. pinned pieces ) // ( this avoids moving into check and moving a piece that exposes the king to check e.g. pinned pieces )
move_ := func() error { move_ := func() error {
if len(move) == 2 {
return b.movePawn(move, captureFrom)
}
if len(move) == 3 {
piece = move[0:1]
targetPosition = move[1:3]
// collision detection // collision detection
if collisionPiece, err = b.getCollision(targetPosition); err != nil { if collisionPiece, err = b.getCollision(to); err != nil {
return fmt.Errorf("invalid move %s: %v", move, err) return fmt.Errorf("invalid move %s: %v", move, err)
} else if collisionPiece != nil { } else if collisionPiece != nil {
return fmt.Errorf("invalid move %s: position %s blocked by %s", move, targetPosition, collisionPiece) return fmt.Errorf("invalid move %s: position %s blocked by %s", move, to, collisionPiece)
} }
switch strings.ToLower(piece) { switch strings.ToLower(piece) {
case "p":
return b.movePawn(to, fromX, fromY)
case "r": case "r":
return b.moveRook(targetPosition, false) return b.moveRook(to, false, fromX, fromY)
case "b": case "b":
return b.moveBishop(targetPosition, false) return b.moveBishop(to, false)
case "n": case "n":
return b.moveKnight(targetPosition, captureFrom) return b.moveKnight(to, fromX, fromY)
case "q": case "q":
return b.moveQueen(targetPosition) return b.moveQueen(to)
case "k": case "k":
return b.moveKing(targetPosition) return b.moveKing(to)
default: default:
return fmt.Errorf("invalid move %s: %v", move, err) return fmt.Errorf("invalid move %s: %v", move, err)
} }
} }
return fmt.Errorf("invalid move %s: %v", move, err)
}
if err = move_(); err != nil { if err = move_(); err != nil {
return err return err
} }
@ -360,65 +336,154 @@ func (b *Board) Move(move string) error {
return nil return nil
} }
func parseCaptureMove(move string) (int, error) { func parseMove(move string) (string, int, int, string, error) {
var ( var (
parts = strings.Split(move, "x") piece string
from = parts[0] fromX = -1
fromY = -1
to string
from string
) )
toIndex := func(column string) int { if strings.Contains(move, "x") {
switch column { return parseCaptureMove(move)
case "a": }
return 0
case "b": if len(move) < 2 {
return 1 return piece, fromX, fromY, to, fmt.Errorf("invalid move: %s", move)
case "c": }
return 2
case "d": if len(move) == 2 {
return 3 // pawn move
case "e": piece = "p"
return 4 to = move
case "f": return piece, fromX, fromY, to, nil
return 5 }
case "g":
return 6 if len(move) == 3 {
case "h": // unambiguous piece move
return 7 piece = move[0:1]
to = move[1:]
return piece, fromX, fromY, to, nil
}
// last two characters are the target position
to = move[len(move)-2:]
// everything before is about origin
from = move[:len(move)-2]
// piece is first character
piece = from[0:1]
// we can ignore piece from here on
from = from[1:]
if len(from) == 1 {
// rank or file given
if from[0] >= 'a' && from[0] <= 'h' {
// file given
return piece, toFile(rune(from[0])), -1, to, nil
}
if from[0] >= '1' && from[0] <= '8' {
// rank given
return piece, -1, toRank(rune(from[0])), to, nil
}
return "", -1, -1, "", fmt.Errorf("invalid move: %s", move)
}
if len(from) == 2 {
// file and rank given
return piece, toFile(rune(from[0])), toRank(rune(from[1])), to, nil
}
return "", -1, -1, "", fmt.Errorf("invalid move: %s", move)
}
func parseCaptureMove(move string) (string, int, int, string, error) {
var (
parts = strings.Split(move, "x")
piece string
fromX = -1
fromY = -1
from = parts[0]
to = parts[1]
)
if len(from) == 1 {
// pawn move with rank given (exd4) or piece move (Nxe5)
if strings.ToLower(from) == from {
piece = "p"
fromX = toFile(rune(from[0]))
return piece, fromX, fromY, to, nil
}
piece = from
return piece, fromX, fromY, to, nil
}
// piece is always first character
piece = from[0:1]
if len(from) == 2 {
// rank or file given
fromX = toFile(rune(from[1]))
fromY = toRank(rune(from[1]))
if fromX == -1 && fromY == -1 {
return "", -1, -1, "", fmt.Errorf("invalid move: %s", move)
}
return piece, fromX, fromY, to, nil
}
if len(from) == 3 {
// both file and rank given
fromX = toFile(rune(from[1]))
fromY = toRank(rune(from[1]))
if fromX == -1 || fromY == -1 {
return "", -1, -1, "", fmt.Errorf("invalid move: %s", move)
}
return piece, fromX, fromY, to, nil
}
return "", -1, -1, "", fmt.Errorf("invalid move: %s", move)
}
func toFile(r rune) int {
if r >= 'a' && r <= 'h' {
return int(r - 'a')
} }
return -1 return -1
} }
if !strings.Contains(move, "x") { func toRank(r rune) int {
return -1, nil if r >= '1' && r <= '8' {
return int('8' - r)
}
return -1
} }
if len(parts) != 2 { func (b *Board) validateMove(search PieceName, fromX int, fromY int) func(*Piece, int, int) bool {
return -1, fmt.Errorf("invalid move: %s", move) return func(p *Piece, xPrev int, yPrev int) bool {
// does piece originate from given origin?
from := (fromX == -1 || xPrev == fromX) && (fromY == -1 || yPrev == fromY)
// is this the piece we're looking for?
found := p != nil && p.Name == search && p.Color == b.turn
return from && found
}
} }
if len(from) == 2 { func (b *Board) movePawn(position string, fromX int, fromY int) error {
// example: Nexd4
return toIndex(from[1:]), nil
}
if len(from) == 1 {
if strings.ToLower(from) == from {
// example: exd4
return toIndex(from), nil
}
// example: Nxd4
return -1, nil
}
return -1, fmt.Errorf("invalid move: %s", move)
}
func (b *Board) movePawn(position string, captureFrom int) error {
var ( var (
toX int toX int
toY int toY int
fromX = captureFrom
fromY int
yPrev int yPrev int
piece *Piece piece *Piece
err error err error
@ -428,7 +493,7 @@ func (b *Board) movePawn(position string, captureFrom int) error {
return err return err
} }
if captureFrom != -1 { if fromX != -1 {
if b.turn == Light { if b.turn == Light {
fromY = toY + 1 fromY = toY + 1
} else { } else {
@ -486,7 +551,7 @@ func (b *Board) movePawn(position string, captureFrom int) error {
return fmt.Errorf("no pawn found that can move to %s", position) return fmt.Errorf("no pawn found that can move to %s", position)
} }
func (b *Board) moveRook(position string, queen bool) error { func (b *Board) moveRook(position string, queen bool, fromX int, fromY int) error {
var ( var (
x int x int
y int y int
@ -500,13 +565,16 @@ func (b *Board) moveRook(position string, queen bool) error {
return err return err
} }
checkRookMove := b.validateMove(Rook, fromX, fromY)
checkQueenMove := b.validateMove(Queen, fromX, fromY)
xPrev = x xPrev = x
yPrev = y yPrev = y
for xPrev >= 0 && xPrev < 8 && yPrev >= 0 && yPrev < 8 { for xPrev >= 0 && xPrev < 8 && yPrev >= 0 && yPrev < 8 {
xPrev++ xPrev++
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if piece != nil { if piece != nil {
if ((!queen && piece.Name == Rook) || (queen && piece.Name == Queen)) && piece.Color == b.turn { if (!queen && checkRookMove(piece, xPrev, yPrev)) || (queen && checkQueenMove(piece, xPrev, yPrev)) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -523,7 +591,7 @@ func (b *Board) moveRook(position string, queen bool) error {
yPrev-- yPrev--
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if piece != nil { if piece != nil {
if ((!queen && piece.Name == Rook) || (queen && piece.Name == Queen)) && piece.Color == b.turn { if (!queen && checkRookMove(piece, xPrev, yPrev)) || (queen && checkQueenMove(piece, xPrev, yPrev)) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -539,7 +607,7 @@ func (b *Board) moveRook(position string, queen bool) error {
xPrev-- xPrev--
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if piece != nil { if piece != nil {
if ((!queen && piece.Name == Rook) || (queen && piece.Name == Queen)) && piece.Color == b.turn { if (!queen && checkRookMove(piece, xPrev, yPrev)) || (queen && checkQueenMove(piece, xPrev, yPrev)) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -555,7 +623,7 @@ func (b *Board) moveRook(position string, queen bool) error {
yPrev++ yPrev++
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if piece != nil { if piece != nil {
if ((!queen && piece.Name == Rook) || (queen && piece.Name == Queen)) && piece.Color == b.turn { if (!queen && checkRookMove(piece, xPrev, yPrev)) || (queen && checkQueenMove(piece, xPrev, yPrev)) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -654,7 +722,7 @@ func (b *Board) moveBishop(position string, queen bool) error {
return fmt.Errorf("no bishop found that can move to %s", position) return fmt.Errorf("no bishop found that can move to %s", position)
} }
func (b *Board) moveKnight(position string, captureFrom int) error { func (b *Board) moveKnight(position string, fromX int, fromY int) error {
var ( var (
x int x int
y int y int
@ -668,17 +736,12 @@ func (b *Board) moveKnight(position string, captureFrom int) error {
return err return err
} }
checkMove := func(p *Piece, xPrev int) bool { checkMove := b.validateMove(Knight, fromX, fromY)
// capture condition fulfilled if capture
capture := captureFrom == -1 || xPrev == captureFrom
found := p != nil && p.Name == Knight && p.Color == b.turn
return capture && found
}
xPrev = x + 1 xPrev = x + 1
yPrev = y - 2 yPrev = y - 2
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if checkMove(piece, xPrev) { if checkMove(piece, xPrev, yPrev) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -687,7 +750,7 @@ func (b *Board) moveKnight(position string, captureFrom int) error {
xPrev = x + 2 xPrev = x + 2
yPrev = y - 1 yPrev = y - 1
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if checkMove(piece, xPrev) { if checkMove(piece, xPrev, yPrev) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -696,7 +759,7 @@ func (b *Board) moveKnight(position string, captureFrom int) error {
xPrev = x + 2 xPrev = x + 2
yPrev = y + 1 yPrev = y + 1
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if checkMove(piece, xPrev) { if checkMove(piece, xPrev, yPrev) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -705,7 +768,7 @@ func (b *Board) moveKnight(position string, captureFrom int) error {
xPrev = x + 1 xPrev = x + 1
yPrev = y + 2 yPrev = y + 2
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if checkMove(piece, xPrev) { if checkMove(piece, xPrev, yPrev) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -714,7 +777,7 @@ func (b *Board) moveKnight(position string, captureFrom int) error {
xPrev = x - 1 xPrev = x - 1
yPrev = y + 2 yPrev = y + 2
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if checkMove(piece, xPrev) { if checkMove(piece, xPrev, yPrev) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -723,7 +786,7 @@ func (b *Board) moveKnight(position string, captureFrom int) error {
xPrev = x - 2 xPrev = x - 2
yPrev = y + 1 yPrev = y + 1
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if checkMove(piece, xPrev) { if checkMove(piece, xPrev, yPrev) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -732,7 +795,7 @@ func (b *Board) moveKnight(position string, captureFrom int) error {
xPrev = x - 2 xPrev = x - 2
yPrev = y - 1 yPrev = y - 1
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if checkMove(piece, xPrev) { if checkMove(piece, xPrev, yPrev) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -741,7 +804,7 @@ func (b *Board) moveKnight(position string, captureFrom int) error {
xPrev = x - 1 xPrev = x - 1
yPrev = y - 2 yPrev = y - 2
piece = b.getPiece(xPrev, yPrev) piece = b.getPiece(xPrev, yPrev)
if checkMove(piece, xPrev) { if checkMove(piece, xPrev, yPrev) {
b.tiles[xPrev][yPrev] = nil b.tiles[xPrev][yPrev] = nil
b.tiles[x][y] = piece b.tiles[x][y] = piece
return nil return nil
@ -759,7 +822,7 @@ func (b *Board) moveQueen(position string) error {
return nil return nil
} }
if err = b.moveRook(position, true); err == nil { if err = b.moveRook(position, true, -1, -1); err == nil {
return nil return nil
} }

View File

@ -197,7 +197,6 @@ func TestBoardMoveKnightCapture(t *testing.T) {
b := chess.NewBoard() b := chess.NewBoard()
assertParse(t, b, "e4 Nf6 d4 Nxe4") assertParse(t, b, "e4 Nf6 d4 Nxe4")
assertPiece(t, b, "e4", chess.Knight, chess.Dark) assertPiece(t, b, "e4", chess.Knight, chess.Dark)
assertPiece(t, b, "d4", chess.Pawn, chess.Light) assertPiece(t, b, "d4", chess.Pawn, chess.Light)
assertNoPiece(t, b, "g8") assertNoPiece(t, b, "g8")
@ -296,10 +295,7 @@ func TestBoardMoveRook(t *testing.T) {
assertMoveError(t, b, "Ra3", "no rook found that can move to a3") assertMoveError(t, b, "Ra3", "no rook found that can move to a3")
assertParse(t, b, "a4") assertParse(t, b, "a4 a5 Ra3")
assertParse(t, b, "a5")
assertParse(t, b, "Ra3")
assertPiece(t, b, "a3", chess.Rook, chess.Light) assertPiece(t, b, "a3", chess.Rook, chess.Light)
assertNoPiece(t, b, "a1") assertNoPiece(t, b, "a1")
@ -310,6 +306,18 @@ func TestBoardMoveRook(t *testing.T) {
assertNoPiece(t, b, "a8") assertNoPiece(t, b, "a8")
} }
func TestBoardMoveRookCapture(t *testing.T) {
t.Parallel()
b := chess.NewBoard()
assertParse(t, b, "a4 e6 h4 e5 Ra3 e4 Rhh3 e3 Raxe3")
assertPiece(t, b, "e3", chess.Rook, chess.Light)
assertPiece(t, b, "h3", chess.Rook, chess.Light)
assertNoPiece(t, b, "a3")
}
func TestBoardMoveRookInvalid(t *testing.T) { func TestBoardMoveRookInvalid(t *testing.T) {
t.Parallel() t.Parallel()