506 lines
12 KiB
Go
506 lines
12 KiB
Go
package main
|
|
|
|
import "fmt"
|
|
|
|
const (
|
|
Empty = 0
|
|
Pawn = 1
|
|
Knight = 2
|
|
Bishop = 3
|
|
Rook = 4
|
|
Queen = 5
|
|
King = 6
|
|
)
|
|
|
|
const (
|
|
White = 1
|
|
Black = 2
|
|
)
|
|
|
|
type Square struct {
|
|
Piece int
|
|
Color int
|
|
}
|
|
|
|
type Position struct {
|
|
Row int
|
|
Col int
|
|
}
|
|
|
|
type Board struct {
|
|
Grid [8][8]Square
|
|
Castling map[string]bool
|
|
EnPassantTarget *Position
|
|
MoveHistory []string
|
|
}
|
|
|
|
func NewBoard() *Board {
|
|
b := &Board{
|
|
Castling: map[string]bool{"K": true, "Q": true, "k": true, "q": true},
|
|
}
|
|
backRank := []int{Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook}
|
|
for col, p := range backRank {
|
|
b.Grid[0][col] = Square{Piece: p, Color: Black}
|
|
b.Grid[7][col] = Square{Piece: p, Color: White}
|
|
}
|
|
for col := 0; col < 8; col++ {
|
|
b.Grid[1][col] = Square{Piece: Pawn, Color: Black}
|
|
b.Grid[6][col] = Square{Piece: Pawn, Color: White}
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (b *Board) Clone() *Board {
|
|
clone := *b
|
|
clone.Castling = make(map[string]bool)
|
|
for k, v := range b.Castling {
|
|
clone.Castling[k] = v
|
|
}
|
|
if b.EnPassantTarget != nil {
|
|
pt := *b.EnPassantTarget
|
|
clone.EnPassantTarget = &pt
|
|
}
|
|
return &clone
|
|
}
|
|
|
|
func (b *Board) inBounds(r, c int) bool {
|
|
return r >= 0 && r < 8 && c >= 0 && c < 8
|
|
}
|
|
|
|
func (b *Board) isSquareAttacked(pos Position, byColor int) bool {
|
|
r, c := pos.Row, pos.Col
|
|
pawnDir := -1
|
|
if byColor == Black {
|
|
pawnDir = 1
|
|
}
|
|
for dc := -1; dc <= 1; dc += 2 {
|
|
nr, nc := r-pawnDir, c+dc
|
|
if b.inBounds(nr, nc) {
|
|
if b.Grid[nr][nc].Piece == Pawn && b.Grid[nr][nc].Color == byColor {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
knightMoves := [][2]int{{-2, -1}, {-2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}, {2, -1}, {2, 1}}
|
|
for _, dm := range knightMoves {
|
|
nr, nc := r+dm[0], c+dm[1]
|
|
if b.inBounds(nr, nc) {
|
|
if b.Grid[nr][nc].Piece == Knight && b.Grid[nr][nc].Color == byColor {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
for dr := -1; dr <= 1; dr++ {
|
|
for dc := -1; dc <= 1; dc++ {
|
|
if dr == 0 && dc == 0 {
|
|
continue
|
|
}
|
|
nr, nc := r+dr, c+dc
|
|
if b.inBounds(nr, nc) {
|
|
if b.Grid[nr][nc].Piece == King && b.Grid[nr][nc].Color == byColor {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ortho := [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
|
|
for _, dir := range ortho {
|
|
nr, nc := r+dir[0], c+dir[1]
|
|
for b.inBounds(nr, nc) {
|
|
s := b.Grid[nr][nc]
|
|
if s.Piece != Empty {
|
|
if s.Color == byColor && (s.Piece == Rook || s.Piece == Queen) {
|
|
return true
|
|
}
|
|
break
|
|
}
|
|
nr += dir[0]
|
|
nc += dir[1]
|
|
}
|
|
}
|
|
diag := [][2]int{{-1, -1}, {-1, 1}, {1, -1}, {1, 1}}
|
|
for _, dir := range diag {
|
|
nr, nc := r+dir[0], c+dir[1]
|
|
for b.inBounds(nr, nc) {
|
|
s := b.Grid[nr][nc]
|
|
if s.Piece != Empty {
|
|
if s.Color == byColor && (s.Piece == Bishop || s.Piece == Queen) {
|
|
return true
|
|
}
|
|
break
|
|
}
|
|
nr += dir[0]
|
|
nc += dir[1]
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (b *Board) findKing(color int) Position {
|
|
for r := 0; r < 8; r++ {
|
|
for c := 0; c < 8; c++ {
|
|
if b.Grid[r][c].Piece == King && b.Grid[r][c].Color == color {
|
|
return Position{r, c}
|
|
}
|
|
}
|
|
}
|
|
return Position{}
|
|
}
|
|
|
|
func (b *Board) isInCheck(color int) bool {
|
|
kingPos := b.findKing(color)
|
|
oppColor := Black
|
|
if color == Black {
|
|
oppColor = White
|
|
}
|
|
return b.isSquareAttacked(kingPos, oppColor)
|
|
}
|
|
|
|
type Move struct {
|
|
From Position
|
|
To Position
|
|
Promotion int
|
|
}
|
|
|
|
func (b *Board) generatePseudoLegalMoves(color int) []Move {
|
|
var moves []Move
|
|
for r := 0; r < 8; r++ {
|
|
for c := 0; c < 8; c++ {
|
|
s := b.Grid[r][c]
|
|
if s.Piece == Empty || s.Color != color {
|
|
continue
|
|
}
|
|
from := Position{r, c}
|
|
switch s.Piece {
|
|
case Pawn:
|
|
moves = append(moves, b.pawnMoves(from, color)...)
|
|
case Knight:
|
|
moves = append(moves, b.knightMoves(from, color)...)
|
|
case Bishop:
|
|
moves = append(moves, b.slideMoves(from, color, diagDirs)...)
|
|
case Rook:
|
|
moves = append(moves, b.slideMoves(from, color, orthoDirs)...)
|
|
case Queen:
|
|
all := append(append([][2]int{}, diagDirs...), orthoDirs...)
|
|
moves = append(moves, b.slideMoves(from, color, all)...)
|
|
case King:
|
|
moves = append(moves, b.kingMoves(from, color)...)
|
|
}
|
|
}
|
|
}
|
|
return moves
|
|
}
|
|
|
|
var orthoDirs = [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
|
|
var diagDirs = [][2]int{{-1, -1}, {-1, 1}, {1, -1}, {1, 1}}
|
|
|
|
func (b *Board) pawnMoves(from Position, color int) []Move {
|
|
var moves []Move
|
|
r, c := from.Row, from.Col
|
|
dir := -1
|
|
if color == Black {
|
|
dir = 1
|
|
}
|
|
nr := r + dir
|
|
if b.inBounds(nr, c) && b.Grid[nr][c].Piece == Empty {
|
|
if (color == White && nr == 0) || (color == Black && nr == 7) {
|
|
for _, p := range []int{Queen, Rook, Bishop, Knight} {
|
|
moves = append(moves, Move{From: from, To: Position{nr, c}, Promotion: p})
|
|
}
|
|
} else {
|
|
moves = append(moves, Move{From: from, To: Position{nr, c}})
|
|
}
|
|
startRow := 6
|
|
if color == Black {
|
|
startRow = 1
|
|
}
|
|
if r == startRow {
|
|
nr2 := r + 2*dir
|
|
if b.inBounds(nr2, c) && b.Grid[nr2][c].Piece == Empty {
|
|
moves = append(moves, Move{From: from, To: Position{nr2, c}})
|
|
}
|
|
}
|
|
}
|
|
for dc := -1; dc <= 1; dc += 2 {
|
|
nc := c + dc
|
|
if !b.inBounds(nr, nc) {
|
|
continue
|
|
}
|
|
target := b.Grid[nr][nc]
|
|
if target.Piece != Empty && target.Color != color {
|
|
if (color == White && nr == 0) || (color == Black && nr == 7) {
|
|
for _, p := range []int{Queen, Rook, Bishop, Knight} {
|
|
moves = append(moves, Move{From: from, To: Position{nr, nc}, Promotion: p})
|
|
}
|
|
} else {
|
|
moves = append(moves, Move{From: from, To: Position{nr, nc}})
|
|
}
|
|
}
|
|
if b.EnPassantTarget != nil && b.EnPassantTarget.Row == nr && b.EnPassantTarget.Col == nc {
|
|
moves = append(moves, Move{From: from, To: Position{nr, nc}})
|
|
}
|
|
}
|
|
return moves
|
|
}
|
|
|
|
func (b *Board) knightMoves(from Position, color int) []Move {
|
|
var moves []Move
|
|
kd := [][2]int{{-2, -1}, {-2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}, {2, -1}, {2, 1}}
|
|
for _, d := range kd {
|
|
nr, nc := from.Row+d[0], from.Col+d[1]
|
|
if !b.inBounds(nr, nc) {
|
|
continue
|
|
}
|
|
if b.Grid[nr][nc].Color != color {
|
|
moves = append(moves, Move{From: from, To: Position{nr, nc}})
|
|
}
|
|
}
|
|
return moves
|
|
}
|
|
|
|
func (b *Board) slideMoves(from Position, color int, dirs [][2]int) []Move {
|
|
var moves []Move
|
|
for _, d := range dirs {
|
|
nr, nc := from.Row+d[0], from.Col+d[1]
|
|
for b.inBounds(nr, nc) {
|
|
if b.Grid[nr][nc].Piece == Empty {
|
|
moves = append(moves, Move{From: from, To: Position{nr, nc}})
|
|
} else {
|
|
if b.Grid[nr][nc].Color != color {
|
|
moves = append(moves, Move{From: from, To: Position{nr, nc}})
|
|
}
|
|
break
|
|
}
|
|
nr += d[0]
|
|
nc += d[1]
|
|
}
|
|
}
|
|
return moves
|
|
}
|
|
|
|
func (b *Board) kingMoves(from Position, color int) []Move {
|
|
var moves []Move
|
|
for dr := -1; dr <= 1; dr++ {
|
|
for dc := -1; dc <= 1; dc++ {
|
|
if dr == 0 && dc == 0 {
|
|
continue
|
|
}
|
|
nr, nc := from.Row+dr, from.Col+dc
|
|
if !b.inBounds(nr, nc) {
|
|
continue
|
|
}
|
|
if b.Grid[nr][nc].Color != color {
|
|
moves = append(moves, Move{From: from, To: Position{nr, nc}})
|
|
}
|
|
}
|
|
}
|
|
oppColor := Black
|
|
if color == White {
|
|
oppColor = Black
|
|
} else {
|
|
oppColor = White
|
|
}
|
|
if color == White && from.Row == 7 && from.Col == 4 {
|
|
if b.Castling["K"] && b.Grid[7][5].Piece == Empty && b.Grid[7][6].Piece == Empty {
|
|
if !b.isSquareAttacked(Position{7, 4}, oppColor) &&
|
|
!b.isSquareAttacked(Position{7, 5}, oppColor) &&
|
|
!b.isSquareAttacked(Position{7, 6}, oppColor) {
|
|
moves = append(moves, Move{From: from, To: Position{7, 6}})
|
|
}
|
|
}
|
|
if b.Castling["Q"] && b.Grid[7][3].Piece == Empty && b.Grid[7][2].Piece == Empty && b.Grid[7][1].Piece == Empty {
|
|
if !b.isSquareAttacked(Position{7, 4}, oppColor) &&
|
|
!b.isSquareAttacked(Position{7, 3}, oppColor) &&
|
|
!b.isSquareAttacked(Position{7, 2}, oppColor) {
|
|
moves = append(moves, Move{From: from, To: Position{7, 2}})
|
|
}
|
|
}
|
|
} else if color == Black && from.Row == 0 && from.Col == 4 {
|
|
if b.Castling["k"] && b.Grid[0][5].Piece == Empty && b.Grid[0][6].Piece == Empty {
|
|
if !b.isSquareAttacked(Position{0, 4}, oppColor) &&
|
|
!b.isSquareAttacked(Position{0, 5}, oppColor) &&
|
|
!b.isSquareAttacked(Position{0, 6}, oppColor) {
|
|
moves = append(moves, Move{From: from, To: Position{0, 6}})
|
|
}
|
|
}
|
|
if b.Castling["q"] && b.Grid[0][3].Piece == Empty && b.Grid[0][2].Piece == Empty && b.Grid[0][1].Piece == Empty {
|
|
if !b.isSquareAttacked(Position{0, 4}, oppColor) &&
|
|
!b.isSquareAttacked(Position{0, 3}, oppColor) &&
|
|
!b.isSquareAttacked(Position{0, 2}, oppColor) {
|
|
moves = append(moves, Move{From: from, To: Position{0, 2}})
|
|
}
|
|
}
|
|
}
|
|
return moves
|
|
}
|
|
|
|
func (b *Board) generateLegalMoves(color int) []Move {
|
|
pseudo := b.generatePseudoLegalMoves(color)
|
|
var legal []Move
|
|
for _, m := range pseudo {
|
|
clone := b.Clone()
|
|
clone.applyMove(m)
|
|
if !clone.isInCheck(color) {
|
|
legal = append(legal, m)
|
|
}
|
|
}
|
|
return legal
|
|
}
|
|
|
|
func (b *Board) applyMove(m Move) {
|
|
from, to := m.From, m.To
|
|
movingPiece := b.Grid[from.Row][from.Col]
|
|
if movingPiece.Piece == Pawn && to.Col != from.Col && b.Grid[to.Row][to.Col].Piece == Empty {
|
|
b.Grid[from.Row][to.Col] = Square{}
|
|
}
|
|
b.Grid[to.Row][to.Col] = movingPiece
|
|
b.Grid[from.Row][from.Col] = Square{}
|
|
if movingPiece.Piece == Pawn && m.Promotion != 0 {
|
|
b.Grid[to.Row][to.Col].Piece = m.Promotion
|
|
}
|
|
if movingPiece.Piece == King && MathAbs(to.Col-from.Col) == 2 {
|
|
row := from.Row
|
|
if to.Col == 6 {
|
|
b.Grid[row][5] = b.Grid[row][7]
|
|
b.Grid[row][7] = Square{}
|
|
} else if to.Col == 2 {
|
|
b.Grid[row][3] = b.Grid[row][0]
|
|
b.Grid[row][0] = Square{}
|
|
}
|
|
}
|
|
if movingPiece.Piece == King {
|
|
if movingPiece.Color == White {
|
|
b.Castling["K"] = false
|
|
b.Castling["Q"] = false
|
|
} else {
|
|
b.Castling["k"] = false
|
|
b.Castling["q"] = false
|
|
}
|
|
}
|
|
if movingPiece.Piece == Rook {
|
|
if from.Row == 7 && from.Col == 0 {
|
|
b.Castling["Q"] = false
|
|
}
|
|
if from.Row == 7 && from.Col == 7 {
|
|
b.Castling["K"] = false
|
|
}
|
|
if from.Row == 0 && from.Col == 0 {
|
|
b.Castling["q"] = false
|
|
}
|
|
if from.Row == 0 && from.Col == 7 {
|
|
b.Castling["k"] = false
|
|
}
|
|
}
|
|
if to.Row == 7 && to.Col == 0 {
|
|
b.Castling["Q"] = false
|
|
}
|
|
if to.Row == 7 && to.Col == 7 {
|
|
b.Castling["K"] = false
|
|
}
|
|
if to.Row == 0 && to.Col == 0 {
|
|
b.Castling["q"] = false
|
|
}
|
|
if to.Row == 0 && to.Col == 7 {
|
|
b.Castling["k"] = false
|
|
}
|
|
b.EnPassantTarget = nil
|
|
if movingPiece.Piece == Pawn {
|
|
if MathAbs(to.Row-from.Row) == 2 {
|
|
b.EnPassantTarget = &Position{(from.Row + to.Row) / 2, from.Col}
|
|
}
|
|
}
|
|
}
|
|
|
|
func MathAbs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|
|
|
|
func (b *Board) Display(checkColor int) string {
|
|
pieceChars := map[int]string{
|
|
Pawn: "P", Knight: "N", Bishop: "B", Rook: "R", Queen: "Q", King: "K",
|
|
}
|
|
var out string
|
|
out += " a b c d e f g h\n"
|
|
for r := 0; r < 8; r++ {
|
|
out += fmt.Sprintf("%d", 8-r)
|
|
for c := 0; c < 8; c++ {
|
|
s := b.Grid[r][c]
|
|
if s.Piece == Empty {
|
|
out += " ."
|
|
} else {
|
|
ch := pieceChars[s.Piece]
|
|
if s.Color == White {
|
|
out += " " + ch
|
|
} else {
|
|
out += " " + string(rune(97+int(ch[0])-65))
|
|
}
|
|
}
|
|
}
|
|
out += fmt.Sprintf(" %d\n", 8-r)
|
|
}
|
|
out += " a b c d e f g h\n"
|
|
if b.isInCheck(checkColor) {
|
|
out += fmt.Sprintf("\n !! %s IS IN CHECK !!\n", colorName(checkColor))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func colorName(c int) string {
|
|
if c == White {
|
|
return "WHITE"
|
|
}
|
|
return "BLACK"
|
|
}
|
|
|
|
func (b *Board) ParseMove(input string) (*Move, error) {
|
|
if len(input) < 4 || len(input) > 5 {
|
|
return nil, fmt.Errorf("invalid move format: %s (use e.g. e2e4)", input)
|
|
}
|
|
fromCol := int(input[0] - 'a')
|
|
fromRow := 8 - int(input[1] - '0')
|
|
toCol := int(input[2] - 'a')
|
|
toRow := 8 - int(input[3] - '0')
|
|
if fromCol < 0 || fromCol > 7 || toCol < 0 || toCol > 7 {
|
|
return nil, fmt.Errorf("invalid columns")
|
|
}
|
|
if fromRow < 0 || fromRow > 7 || toRow < 0 || toRow > 7 {
|
|
return nil, fmt.Errorf("invalid rows")
|
|
}
|
|
promotion := 0
|
|
if len(input) == 5 {
|
|
switch input[4] {
|
|
case 'q', 'Q':
|
|
promotion = Queen
|
|
case 'r', 'R':
|
|
promotion = Rook
|
|
case 'b', 'B':
|
|
promotion = Bishop
|
|
case 'n', 'N':
|
|
promotion = Knight
|
|
default:
|
|
return nil, fmt.Errorf("invalid promotion piece: %c", input[4])
|
|
}
|
|
}
|
|
m := Move{
|
|
From: Position{fromRow, fromCol},
|
|
To: Position{toRow, toCol},
|
|
Promotion: promotion,
|
|
}
|
|
return &m, nil
|
|
}
|
|
|
|
func MoveToAlgebraic(m Move) string {
|
|
from := string(rune('a'+m.From.Col)) + string(rune('0'+(8-m.From.Row)))
|
|
to := string(rune('a'+m.To.Col)) + string(rune('0'+(8-m.To.Row)))
|
|
s := from + to
|
|
if m.Promotion != 0 {
|
|
pieceChars := map[int]byte{Queen: 'q', Rook: 'r', Bishop: 'b', Knight: 'n'}
|
|
s += string(pieceChars[m.Promotion])
|
|
}
|
|
return s
|
|
}
|
|
|