This commit is contained in:
kyodesle 2026-08-28 08:35:40 -07:00
commit 7f22b4f4b3
8 changed files with 1303 additions and 0 deletions

294
ai.go Normal file
View File

@ -0,0 +1,294 @@
package main
import (
"fmt"
"sort"
)
const aiDepth = 3
var pieceValues = map[int]int{
Empty: 0, Pawn: 100, Knight: 320, Bishop: 330,
Rook: 500, Queen: 900, King: 20000,
}
var pawnTable = [8][8]int{
{0, 0, 0, 0, 0, 0, 0, 0},
{50, 50, 50, 50, 50, 50, 50, 50},
{10, 10, 20, 30, 30, 20, 10, 10},
{5, 5, 10, 25, 25, 10, 5, 5},
{0, 0, 0, 20, 20, 0, 0, 0},
{5, -5, -10, 0, 0, -10, -5, 5},
{5, 10, 10, -20, -20, 10, 10, 5},
{0, 0, 0, 0, 0, 0, 0, 0},
}
var knightTable = [8][8]int{
{-50, -40, -30, -30, -30, -30, -40, -50},
{-40, -20, 0, 0, 0, 0, -20, -40},
{-30, 0, 10, 15, 15, 10, 0, -30},
{-30, 5, 15, 20, 20, 15, 5, -30},
{-30, 0, 15, 20, 20, 15, 0, -30},
{-30, 5, 10, 15, 15, 10, 5, -30},
{-40, -20, 0, 5, 5, 0, -20, -40},
{-50, -40, -30, -30, -30, -30, -40, -50},
}
var bishopTable = [8][8]int{
{-20, -10, -10, -10, -10, -10, -10, -20},
{-10, 0, 0, 0, 0, 0, 0, -10},
{-10, 0, 5, 10, 10, 5, 0, -10},
{-10, 5, 5, 10, 10, 5, 5, -10},
{-10, 0, 10, 10, 10, 10, 0, -10},
{-10, 10, 10, 10, 10, 10, 10, -10},
{-10, 5, 0, 0, 0, 0, 5, -10},
{-20, -10, -10, -10, -10, -10, -10, -20},
}
var rookTable = [8][8]int{
{0, 0, 0, 0, 0, 0, 0, 0},
{5, 10, 10, 10, 10, 10, 10, 5},
{-5, 0, 0, 0, 0, 0, 0, -5},
{-5, 0, 0, 0, 0, 0, 0, -5},
{-5, 0, 0, 0, 0, 0, 0, -5},
{-5, 0, 0, 0, 0, 0, 0, -5},
{-5, 0, 0, 0, 0, 0, 0, -5},
{0, 0, 0, 5, 5, 0, 0, 0},
}
var queenTable = [8][8]int{
{-20, -10, -10, -5, -5, -10, -10, -20},
{-10, 0, 0, 0, 0, 0, 0, -10},
{-10, 0, 5, 5, 5, 5, 0, -10},
{-5, 0, 5, 5, 5, 5, 0, -5},
{0, 0, 5, 5, 5, 5, 0, -5},
{-10, 5, 5, 5, 5, 5, 0, -10},
{-10, 0, 5, 0, 0, 0, 0, -10},
{-20, -10, -10, -5, -5, -10, -10, -20},
}
var kingTable = [8][8]int{
{-30, -40, -40, -50, -50, -40, -40, -30},
{-30, -40, -40, -50, -50, -40, -40, -30},
{-30, -40, -40, -50, -50, -40, -40, -30},
{-30, -40, -40, -50, -50, -40, -40, -30},
{-20, -30, -30, -40, -40, -30, -30, -20},
{-10, -20, -20, -20, -20, -20, -20, -10},
{20, 20, 0, 0, 0, 0, 20, 20},
{20, 30, 10, 0, 0, 10, 30, 20},
}
func evaluate(b *Board) int {
score := 0
for r := 0; r < 8; r++ {
for c := 0; c < 8; c++ {
s := b.Grid[r][c]
if s.Piece == Empty {
continue
}
val := pieceValues[s.Piece]
var bonus int
switch s.Piece {
case Pawn:
if s.Color == White {
bonus = pawnTable[r][c]
} else {
bonus = pawnTable[7-r][c]
}
case Knight:
if s.Color == White {
bonus = knightTable[r][c]
} else {
bonus = knightTable[7-r][c]
}
case Bishop:
if s.Color == White {
bonus = bishopTable[r][c]
} else {
bonus = bishopTable[7-r][c]
}
case Rook:
if s.Color == White {
bonus = rookTable[r][c]
} else {
bonus = rookTable[7-r][c]
}
case Queen:
if s.Color == White {
bonus = queenTable[r][c]
} else {
bonus = queenTable[7-r][c]
}
case King:
if s.Color == White {
bonus = kingTable[r][c]
} else {
bonus = kingTable[7-r][c]
}
}
if s.Color == White {
score += val + bonus
} else {
score -= val + bonus
}
}
}
return score
}
func minimax(b *Board, depth int, alpha, beta int, color int) int {
if depth == 0 {
return evaluate(b)
}
moves := b.generateLegalMoves(color)
if len(moves) == 0 {
if b.isInCheck(color) {
if color == White {
return -100000 - depth
}
return 100000 + depth
}
return 0
}
oppColor := Black
if color == White {
oppColor = Black
} else {
oppColor = White
}
if color == White {
maxEval := -999999
for _, m := range moves {
clone := b.Clone()
clone.applyMove(m)
eval := minimax(clone, depth-1, alpha, beta, oppColor)
if eval > maxEval {
maxEval = eval
}
if maxEval > alpha {
alpha = maxEval
}
if alpha >= beta {
break
}
}
return maxEval
} else {
minEval := 999999
for _, m := range moves {
clone := b.Clone()
clone.applyMove(m)
eval := minimax(clone, depth-1, alpha, beta, oppColor)
if eval < minEval {
minEval = eval
}
if minEval < beta {
beta = minEval
}
if alpha >= beta {
break
}
}
return minEval
}
}
func GetBestMove(b *Board, color int) (string, error) {
moves := b.generateLegalMoves(color)
if len(moves) == 0 {
return "", fmt.Errorf("no legal moves")
}
oppColor := Black
if color == White {
oppColor = Black
} else {
oppColor = White
}
var bestMove *Move
var bestScore int
first := true
alpha := -999999
beta := 999999
sort.Slice(moves, func(i, j int) bool {
vi := 0
vj := 0
if b.Grid[moves[i].To.Row][moves[i].To.Col].Piece != Empty {
vi = pieceValues[b.Grid[moves[i].To.Row][moves[i].To.Col].Piece]
}
if b.Grid[moves[j].To.Row][moves[j].To.Col].Piece != Empty {
vj = pieceValues[b.Grid[moves[j].To.Row][moves[j].To.Col].Piece]
}
return vi > vj
})
for _, m := range moves {
clone := b.Clone()
clone.applyMove(m)
score := minimax(clone, aiDepth-1, alpha, beta, oppColor)
if first {
bestMove = &m
bestScore = score
first = false
} else if color == White && score > bestScore {
bestScore = score
bestMove = &m
alpha = score
} else if color == Black && score < bestScore {
bestScore = score
bestMove = &m
beta = score
}
}
if bestMove == nil {
return "", fmt.Errorf("no move found")
}
return MoveToAlgebraic(*bestMove), nil
}
func PlayVsComputer() {
fmt.Println("=== CHESS VS COMPUTER ===")
fmt.Println("You play WHITE. Enter moves like: e2e4, g1f3, e7e8q")
fmt.Println("Type 'quit' to exit.")
fmt.Println()
g := NewGame()
fmt.Print(g.Render())
for !g.Over {
fmt.Print("White> ")
var input string
fmt.Scanln(&input)
if input == "quit" || input == "q" {
fmt.Println("Goodbye!")
return
}
_, err := g.MakeMove(input)
if err != nil {
fmt.Printf(" Error: %v\n\n", err)
continue
}
fmt.Print(g.Render())
if g.Over {
break
}
fmt.Print(" Black thinking... ")
aiMove, err := GetBestMove(g.Board, Black)
if err != nil {
fmt.Printf("AI error: %v\n", err)
break
}
fmt.Printf("Black plays %s\n", aiMove)
_, err = g.MakeMove(aiMove)
if err != nil {
fmt.Printf(" Error: %v\n", err)
continue
}
fmt.Print(g.Render())
}
switch g.Result {
case WhiteWins:
fmt.Println(" *** YOU WIN! ***")
case BlackWins:
fmt.Println(" *** COMPUTER WINS! ***")
case Draw:
fmt.Println(" *** DRAW! ***")
}
}

505
board.go Normal file
View File

@ -0,0 +1,505 @@
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
}

BIN
chess Executable file

Binary file not shown.

85
client.go Normal file
View File

@ -0,0 +1,85 @@
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func PlayMultiplayer(addr string) {
conn, err := net.Dial("tcp", addr)
if err != nil {
fmt.Printf("Error connecting to %s: %v\n", addr, err)
os.Exit(1)
}
defer conn.Close()
fmt.Printf("Connected to %s\n", addr)
fmt.Fprintln(conn, "HELLO")
serverReader := bufio.NewReader(conn)
clientReader := bufio.NewReader(os.Stdin)
welcome, _ := serverReader.ReadString('\n')
fmt.Print(welcome)
line, _ := serverReader.ReadString('\n')
line = strings.TrimSpace(line)
if line == "WAITING" {
fmt.Println("Waiting for an opponent...")
line, _ = serverReader.ReadString('\n')
line = strings.TrimSpace(line)
}
if !strings.HasPrefix(line, "MATCHED") {
fmt.Println("Failed to join a game.")
return
}
parts := strings.Split(line, " ")
myColor := parts[2]
fmt.Printf("\nMatched! You play %s.\n", myColor)
fmt.Println("Moves: e2e4, g1f3, e7e8q | RESIGN | QUIT")
fmt.Println()
gameOver := false
for !gameOver {
header, err := serverReader.ReadString('\n')
if err != nil {
fmt.Println("\nConnection lost.")
break
}
header = strings.TrimSpace(header)
switch {
case header == "BOARD":
var lines []string
for {
bl, err := serverReader.ReadString('\n')
if err != nil {
break
}
bl = strings.TrimRight(bl, "\n")
if bl == "" {
break
}
lines = append(lines, bl)
}
joined := strings.Join(lines, " ")
fmt.Println(strings.Join(lines, "\n"))
fmt.Println()
if strings.Contains(joined, "WINS") || strings.Contains(joined, "STALEMATE") {
gameOver = true
}
case strings.HasPrefix(header, "ERROR"):
fmt.Printf(" %s\n", header)
default:
}
if !gameOver {
fmt.Print("You> ")
input, _ := clientReader.ReadString('\n')
input = strings.TrimSpace(input)
if input == "QUIT" {
fmt.Println("Disconnecting...")
break
}
fmt.Fprintln(conn, input)
}
}
fmt.Println("Goodbye!")
}

99
game.go Normal file
View File

@ -0,0 +1,99 @@
package main
import "fmt"
type GameResult int
const (
GameOngoing GameResult = iota
WhiteWins
BlackWins
Draw
)
type Game struct {
Board *Board
Turn int
Over bool
Result GameResult
MoveNum int
}
func NewGame() *Game {
return &Game{
Board: NewBoard(),
Turn: White,
Over: false,
Result: GameOngoing,
MoveNum: 1,
}
}
func (g *Game) MakeMove(input string) (string, error) {
if g.Over {
return "", fmt.Errorf("game is over")
}
m, err := g.Board.ParseMove(input)
if err != nil {
return "", err
}
s := g.Board.Grid[m.From.Row][m.From.Col]
if s.Piece == Empty || s.Color != g.Turn {
return "", fmt.Errorf("no %s piece on %s", colorName(g.Turn), algebraic(m.From))
}
legalMoves := g.Board.generateLegalMoves(g.Turn)
found := false
for _, lm := range legalMoves {
if lm.From == m.From && lm.To == m.To && lm.Promotion == m.Promotion {
found = true
break
}
}
if !found {
return "", fmt.Errorf("illegal move: %s", input)
}
g.Board.MoveHistory = append(g.Board.MoveHistory, input)
g.Board.applyMove(*m)
if g.Turn == White {
g.Turn = Black
g.MoveNum++
} else {
g.Turn = White
}
oppMoves := g.Board.generateLegalMoves(g.Turn)
if len(oppMoves) == 0 {
g.Over = true
if g.Board.isInCheck(g.Turn) {
if g.Turn == White {
g.Result = BlackWins
} else {
g.Result = WhiteWins
}
} else {
g.Result = Draw
}
}
return input, nil
}
func (g *Game) Render() string {
out := g.Board.Display(g.Turn)
if g.Over {
switch g.Result {
case WhiteWins:
out += "\n === CHECKMATE! WHITE WINS! ===\n"
case BlackWins:
out += "\n === CHECKMATE! BLACK WINS! ===\n"
case Draw:
out += "\n === STALEMATE! DRAW ===\n"
}
} else {
out += fmt.Sprintf("\n Move %d | %s to move\n", g.MoveNum, colorName(g.Turn))
}
return out
}
func algebraic(p Position) string {
return string(rune('a'+p.Col)) + string(rune('0'+(8-p.Row)))
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module golang-chess
go 1.21

51
main.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
switch os.Args[1] {
case "play":
PlayVsComputer()
case "connect":
if len(os.Args) < 3 {
fmt.Println("Usage: chess connect <host:port>")
os.Exit(1)
}
PlayMultiplayer(os.Args[2])
case "server":
addr := ":8080"
if len(os.Args) >= 3 {
addr = os.Args[2]
}
srv := NewServer()
if err := srv.Start(addr); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
case "help", "--help", "-h":
printUsage()
default:
fmt.Printf("Unknown command: %s\n\n", os.Args[1])
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Println(`
CHESS - Go CLI Chess
=====================
chess play Play against the computer
chess server [addr] Start multiplayer server (default :8080)
chess connect <addr> Connect and play vs another person
chess help Show this help
`)
}

266
server.go Normal file
View File

@ -0,0 +1,266 @@
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
"sync"
)
type Player struct {
ID string
Conn net.Conn
}
type GameRoom struct {
White *Player
Black *Player
Game *Game
Mutex sync.Mutex
}
type Server struct {
Listener net.Listener
Waiting []*Player
Rooms map[string]*GameRoom
RoomMutex sync.Mutex
RoomCount int
}
func NewServer() *Server {
return &Server{
Rooms: make(map[string]*GameRoom),
}
}
func (s *Server) Start(addr string) error {
var err error
s.Listener, err = net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to listen on %s: %v", addr, err)
}
fmt.Printf("=== CHESS SERVER ===\n")
fmt.Printf("Listening on %s\n", addr)
fmt.Printf("Type 'status' to show active games, 'quit' to exit.\n\n")
go func() {
for {
conn, err := s.Listener.Accept()
if err != nil {
return
}
go s.handleClient(conn)
}
}()
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
switch line {
case "status":
s.RoomMutex.Lock()
fmt.Printf("Active games: %d | Waiting: %d\n", len(s.Rooms), len(s.Waiting))
for _, room := range s.Rooms {
fmt.Printf(" %s vs %s (move %d)\n", room.White.ID, room.Black.ID, room.Game.MoveNum)
}
s.RoomMutex.Unlock()
case "quit", "q":
s.Listener.Close()
fmt.Println("Server shutting down.")
return nil
case "":
continue
default:
fmt.Printf("Unknown: %s (try 'status' or 'quit')\n", line)
}
}
return nil
}
func (s *Server) handleClient(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
playerID := fmt.Sprintf("Player-%d", s.RoomCount+1)
fmt.Printf("[+] %s connected (%s)\n", playerID, conn.RemoteAddr())
player := &Player{ID: playerID, Conn: conn}
fmt.Fprintln(conn, "CHESS-SERVER v1.0")
line, err := reader.ReadString('\n')
if err != nil {
return
}
if strings.TrimSpace(line) != "HELLO" {
fmt.Fprintln(conn, "ERROR expected HELLO")
return
}
s.RoomMutex.Lock()
s.Waiting = append(s.Waiting, player)
foundOpponent := false
for i, p := range s.Waiting {
if p == player {
continue
}
s.Waiting = append(s.Waiting[:i], s.Waiting[i+1:]...)
foundOpponent = true
s.RoomCount++
roomID := fmt.Sprintf("game-%d", s.RoomCount)
room := &GameRoom{
White: player,
Black: p,
Game: NewGame(),
}
s.Rooms[roomID] = room
fmt.Fprintf(player.Conn, "MATCHED %s WHITE\n", roomID)
fmt.Fprintf(p.Conn, "MATCHED %s BLACK\n", roomID)
boardStr := room.Game.Render()
fmt.Fprintf(player.Conn, "BOARD\n%s\n", boardStr)
fmt.Fprintf(p.Conn, "BOARD\n%s\n", boardStr)
go s.gameLoop(room)
break
}
s.RoomMutex.Unlock()
if !foundOpponent {
fmt.Fprintln(player.Conn, "WAITING")
fmt.Printf(" %s waiting for opponent...\n", playerID)
}
}
func (s *Server) gameLoop(room *GameRoom) {
whiteReader := bufio.NewReader(room.White.Conn)
blackReader := bufio.NewReader(room.Black.Conn)
fmt.Printf("[=] Game: %s (W) vs %s (B)\n", room.White.ID, room.Black.ID)
whiteCh := make(chan string, 4)
blackCh := make(chan string, 4)
doneCh := make(chan struct{})
go func() {
defer close(whiteCh)
for {
line, err := whiteReader.ReadString('\n')
if err != nil {
close(doneCh)
return
}
line = strings.TrimSpace(line)
if line == "RESIGN" {
room.Mutex.Lock()
room.Game.Over = true
room.Game.Result = BlackWins
room.Mutex.Unlock()
close(doneCh)
return
}
if line == "QUIT" {
close(doneCh)
return
}
whiteCh <- line
}
}()
go func() {
defer close(blackCh)
for {
line, err := blackReader.ReadString('\n')
if err != nil {
close(doneCh)
return
}
line = strings.TrimSpace(line)
if line == "RESIGN" {
room.Mutex.Lock()
room.Game.Over = true
room.Game.Result = WhiteWins
room.Mutex.Unlock()
close(doneCh)
return
}
if line == "QUIT" {
close(doneCh)
return
}
blackCh <- line
}
}()
for {
select {
case <-doneCh:
s.finishRoom(room)
return
default:
}
room.Mutex.Lock()
if room.Game.Over {
room.Mutex.Unlock()
s.sendBoard(room)
s.finishRoom(room)
return
}
var moveStr string
var ok bool
if room.Game.Turn == White {
select {
case moveStr, ok = <-whiteCh:
if !ok {
room.Mutex.Unlock()
s.finishRoom(room)
return
}
case <-doneCh:
room.Mutex.Unlock()
s.finishRoom(room)
return
}
} else {
select {
case moveStr, ok = <-blackCh:
if !ok {
room.Mutex.Unlock()
s.finishRoom(room)
return
}
case <-doneCh:
room.Mutex.Unlock()
s.finishRoom(room)
return
}
}
_, err := room.Game.MakeMove(moveStr)
over := room.Game.Over
room.Mutex.Unlock()
if err != nil {
if room.Game.Turn == White {
fmt.Fprintf(room.White.Conn, "ERROR %v\n", err)
} else {
fmt.Fprintf(room.Black.Conn, "ERROR %v\n", err)
}
continue
}
fmt.Printf(" %s: %s\n", moveStr, room.Game.Turn)
s.sendBoard(room)
if over {
s.finishRoom(room)
return
}
}
}
func (s *Server) sendBoard(room *GameRoom) {
boardStr := room.Game.Render()
fmt.Fprintf(room.White.Conn, "BOARD\n%s\n", boardStr)
fmt.Fprintf(room.Black.Conn, "BOARD\n%s\n", boardStr)
}
func (s *Server) finishRoom(room *GameRoom) {
s.RoomMutex.Lock()
for k, r := range s.Rooms {
if r == room {
delete(s.Rooms, k)
break
}
}
s.RoomMutex.Unlock()
room.White.Conn.Close()
room.Black.Conn.Close()
fmt.Printf("[=] Game ended: %s vs %s\n", room.White.ID, room.Black.ID)
}