commit 7f22b4f4b37f123cf4329f142fbabd0239bc1c78 Author: kyodesle Date: Fri Aug 28 08:35:40 2026 -0700 initial diff --git a/ai.go b/ai.go new file mode 100644 index 0000000..447ec11 --- /dev/null +++ b/ai.go @@ -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! ***") + } +} + diff --git a/board.go b/board.go new file mode 100644 index 0000000..5be3dbd --- /dev/null +++ b/board.go @@ -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 +} + diff --git a/chess b/chess new file mode 100755 index 0000000..acb43a1 Binary files /dev/null and b/chess differ diff --git a/client.go b/client.go new file mode 100644 index 0000000..a76c005 --- /dev/null +++ b/client.go @@ -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!") +} + diff --git a/game.go b/game.go new file mode 100644 index 0000000..8d4f93a --- /dev/null +++ b/game.go @@ -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))) +} + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f64ff50 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module golang-chess + +go 1.21 diff --git a/main.go b/main.go new file mode 100644 index 0000000..59bd841 --- /dev/null +++ b/main.go @@ -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 ") + 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 Connect and play vs another person + chess help Show this help +`) +} + diff --git a/server.go b/server.go new file mode 100644 index 0000000..2243919 --- /dev/null +++ b/server.go @@ -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) +} +