mirror of
https://github.com/imjasonh/chessh
synced 2026-07-07 00:33:53 +00:00
initial commit
Signed-off-by: Jason Hall <jason@chainguard.dev>
This commit is contained in:
commit
57b0263707
4 changed files with 942 additions and 0 deletions
580
chess.go
Normal file
580
chess.go
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Color int
|
||||
|
||||
const (
|
||||
White Color = iota
|
||||
Black
|
||||
)
|
||||
|
||||
func (c Color) String() string {
|
||||
if c == White {
|
||||
return "White"
|
||||
}
|
||||
return "Black"
|
||||
}
|
||||
|
||||
type PieceType int
|
||||
|
||||
const (
|
||||
Empty PieceType = iota
|
||||
Pawn
|
||||
Rook
|
||||
Knight
|
||||
Bishop
|
||||
Queen
|
||||
King
|
||||
)
|
||||
|
||||
type Piece struct {
|
||||
Type PieceType
|
||||
Color Color
|
||||
}
|
||||
|
||||
func (p Piece) String() string {
|
||||
if p.Type == Empty {
|
||||
return " "
|
||||
}
|
||||
|
||||
symbols := map[PieceType]string{
|
||||
Pawn: "♟",
|
||||
Rook: "♜",
|
||||
Knight: "♞",
|
||||
Bishop: "♝",
|
||||
Queen: "♛",
|
||||
King: "♚",
|
||||
}
|
||||
|
||||
if p.Color == White {
|
||||
whiteSymbols := map[PieceType]string{
|
||||
Pawn: "♙",
|
||||
Rook: "♖",
|
||||
Knight: "♘",
|
||||
Bishop: "♗",
|
||||
Queen: "♕",
|
||||
King: "♔",
|
||||
}
|
||||
return whiteSymbols[p.Type]
|
||||
}
|
||||
|
||||
return symbols[p.Type]
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Row, Col int
|
||||
}
|
||||
|
||||
func (p Position) Valid() bool {
|
||||
return p.Row >= 0 && p.Row < 8 && p.Col >= 0 && p.Col < 8
|
||||
}
|
||||
|
||||
func (p Position) String() string {
|
||||
if !p.Valid() {
|
||||
return "invalid"
|
||||
}
|
||||
return fmt.Sprintf("%c%d", 'a'+p.Col, p.Row+1)
|
||||
}
|
||||
|
||||
type Move struct {
|
||||
From, To Position
|
||||
Piece Piece
|
||||
}
|
||||
|
||||
type Board [8][8]Piece
|
||||
|
||||
func NewBoard() Board {
|
||||
var board Board
|
||||
|
||||
board[0] = [8]Piece{
|
||||
{Rook, White}, {Knight, White}, {Bishop, White}, {Queen, White},
|
||||
{King, White}, {Bishop, White}, {Knight, White}, {Rook, White},
|
||||
}
|
||||
board[1] = [8]Piece{
|
||||
{Pawn, White}, {Pawn, White}, {Pawn, White}, {Pawn, White},
|
||||
{Pawn, White}, {Pawn, White}, {Pawn, White}, {Pawn, White},
|
||||
}
|
||||
|
||||
for col := 0; col < 8; col++ {
|
||||
for row := 2; row < 6; row++ {
|
||||
board[row][col] = Piece{Empty, White}
|
||||
}
|
||||
}
|
||||
|
||||
board[6] = [8]Piece{
|
||||
{Pawn, Black}, {Pawn, Black}, {Pawn, Black}, {Pawn, Black},
|
||||
{Pawn, Black}, {Pawn, Black}, {Pawn, Black}, {Pawn, Black},
|
||||
}
|
||||
board[7] = [8]Piece{
|
||||
{Rook, Black}, {Knight, Black}, {Bishop, Black}, {Queen, Black},
|
||||
{King, Black}, {Bishop, Black}, {Knight, Black}, {Rook, Black},
|
||||
}
|
||||
|
||||
return board
|
||||
}
|
||||
|
||||
func (b *Board) At(pos Position) Piece {
|
||||
if !pos.Valid() {
|
||||
return Piece{Empty, White}
|
||||
}
|
||||
return b[pos.Row][pos.Col]
|
||||
}
|
||||
|
||||
func (b *Board) Set(pos Position, piece Piece) {
|
||||
if pos.Valid() {
|
||||
b[pos.Row][pos.Col] = piece
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Board) Move(from, to Position) bool {
|
||||
if !from.Valid() || !to.Valid() {
|
||||
return false
|
||||
}
|
||||
|
||||
piece := b.At(from)
|
||||
if piece.Type == Empty {
|
||||
return false
|
||||
}
|
||||
|
||||
b.Set(to, piece)
|
||||
b.Set(from, Piece{Empty, White})
|
||||
return true
|
||||
}
|
||||
|
||||
type Game struct {
|
||||
Board Board
|
||||
CurrentTurn Color
|
||||
MoveHistory []Move
|
||||
KingMoved [2]bool
|
||||
RookMoved [2][2]bool
|
||||
EnPassantTarget *Position
|
||||
}
|
||||
|
||||
func NewGame() *Game {
|
||||
return &Game{
|
||||
Board: NewBoard(),
|
||||
CurrentTurn: White,
|
||||
MoveHistory: make([]Move, 0),
|
||||
KingMoved: [2]bool{false, false},
|
||||
RookMoved: [2][2]bool{{false, false}, {false, false}},
|
||||
EnPassantTarget: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) IsValidMove(from, to Position) bool {
|
||||
if !from.Valid() || !to.Valid() {
|
||||
return false
|
||||
}
|
||||
|
||||
piece := g.Board.At(from)
|
||||
if piece.Type == Empty || piece.Color != g.CurrentTurn {
|
||||
return false
|
||||
}
|
||||
|
||||
target := g.Board.At(to)
|
||||
if target.Type != Empty && target.Color == piece.Color {
|
||||
return false
|
||||
}
|
||||
|
||||
return g.isValidPieceMove(piece, from, to)
|
||||
}
|
||||
|
||||
func (g *Game) isValidPieceMove(piece Piece, from, to Position) bool {
|
||||
dx := to.Col - from.Col
|
||||
dy := to.Row - from.Row
|
||||
|
||||
switch piece.Type {
|
||||
case Pawn:
|
||||
return g.isValidPawnMove(piece, from, to, dx, dy)
|
||||
case Rook:
|
||||
return g.isValidRookMove(from, to, dx, dy)
|
||||
case Knight:
|
||||
return g.isValidKnightMove(dx, dy)
|
||||
case Bishop:
|
||||
return g.isValidBishopMove(from, to, dx, dy)
|
||||
case Queen:
|
||||
return g.isValidQueenMove(from, to, dx, dy)
|
||||
case King:
|
||||
return g.isValidKingMove(piece, dx, dy)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *Game) isValidPawnMove(piece Piece, from, to Position, dx, dy int) bool {
|
||||
direction := 1
|
||||
startRow := 1
|
||||
if piece.Color == Black {
|
||||
direction = -1
|
||||
startRow = 6
|
||||
}
|
||||
|
||||
if dx == 0 && g.Board.At(to).Type == Empty {
|
||||
if dy == direction {
|
||||
return true
|
||||
}
|
||||
if from.Row == startRow && dy == 2*direction {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if abs(dx) == 1 && dy == direction {
|
||||
target := g.Board.At(to)
|
||||
if target.Type != Empty && target.Color != piece.Color {
|
||||
return true
|
||||
}
|
||||
|
||||
if g.EnPassantTarget != nil && *g.EnPassantTarget == to {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *Game) isValidRookMove(from, to Position, dx, dy int) bool {
|
||||
if dx != 0 && dy != 0 {
|
||||
return false
|
||||
}
|
||||
return g.isPathClear(from, to)
|
||||
}
|
||||
|
||||
func (g *Game) isValidKnightMove(dx, dy int) bool {
|
||||
return (abs(dx) == 2 && abs(dy) == 1) || (abs(dx) == 1 && abs(dy) == 2)
|
||||
}
|
||||
|
||||
func (g *Game) isValidBishopMove(from, to Position, dx, dy int) bool {
|
||||
if abs(dx) != abs(dy) {
|
||||
return false
|
||||
}
|
||||
return g.isPathClear(from, to)
|
||||
}
|
||||
|
||||
func (g *Game) isValidQueenMove(from, to Position, dx, dy int) bool {
|
||||
return g.isValidRookMove(from, to, dx, dy) || g.isValidBishopMove(from, to, dx, dy)
|
||||
}
|
||||
|
||||
func (g *Game) isValidKingMove(piece Piece, dx, dy int) bool {
|
||||
if abs(dx) <= 1 && abs(dy) <= 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
if abs(dx) == 2 && dy == 0 && !g.KingMoved[piece.Color] {
|
||||
return g.canCastle(piece.Color, dx > 0)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *Game) canCastle(color Color, kingSide bool) bool {
|
||||
row := 0
|
||||
if color == Black {
|
||||
row = 7
|
||||
}
|
||||
|
||||
rookCol := 0
|
||||
if kingSide {
|
||||
rookCol = 7
|
||||
}
|
||||
|
||||
if g.RookMoved[color][rookCol/7] {
|
||||
return false
|
||||
}
|
||||
|
||||
if g.IsInCheck(color) {
|
||||
return false
|
||||
}
|
||||
|
||||
start, end := 1, 3
|
||||
if kingSide {
|
||||
start, end = 5, 6
|
||||
}
|
||||
|
||||
for col := start; col <= end; col++ {
|
||||
if g.Board.At(Position{row, col}).Type != Empty {
|
||||
return false
|
||||
}
|
||||
|
||||
g.Board.Set(Position{row, 4}, Piece{Empty, White})
|
||||
g.Board.Set(Position{row, col}, Piece{King, color})
|
||||
inCheck := g.IsInCheck(color)
|
||||
g.Board.Set(Position{row, 4}, Piece{King, color})
|
||||
g.Board.Set(Position{row, col}, Piece{Empty, White})
|
||||
|
||||
if inCheck {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *Game) isPathClear(from, to Position) bool {
|
||||
dx := sign(to.Col - from.Col)
|
||||
dy := sign(to.Row - from.Row)
|
||||
|
||||
current := Position{from.Row + dy, from.Col + dx}
|
||||
|
||||
for current != to {
|
||||
if g.Board.At(current).Type != Empty {
|
||||
return false
|
||||
}
|
||||
current.Row += dy
|
||||
current.Col += dx
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *Game) MakeMove(from, to Position) bool {
|
||||
if !g.IsValidMove(from, to) {
|
||||
return false
|
||||
}
|
||||
|
||||
piece := g.Board.At(from)
|
||||
move := Move{from, to, piece}
|
||||
originalTarget := g.Board.At(to)
|
||||
|
||||
g.executeMove(from, to, piece)
|
||||
|
||||
if g.IsInCheck(g.CurrentTurn) {
|
||||
g.undoMove(from, to, piece, originalTarget)
|
||||
return false
|
||||
}
|
||||
|
||||
g.updateGameState(from, to, piece)
|
||||
g.MoveHistory = append(g.MoveHistory, move)
|
||||
g.CurrentTurn = 1 - g.CurrentTurn
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *Game) executeMove(from, to Position, piece Piece) {
|
||||
if piece.Type == King && abs(to.Col-from.Col) == 2 {
|
||||
g.executeCastle(from, to)
|
||||
} else if piece.Type == Pawn && g.EnPassantTarget != nil && *g.EnPassantTarget == to {
|
||||
g.executeEnPassant(from, to)
|
||||
} else {
|
||||
g.Board.Move(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) executeCastle(from, to Position) {
|
||||
g.Board.Move(from, to)
|
||||
|
||||
rookFromCol := 0
|
||||
rookToCol := 3
|
||||
if to.Col > from.Col {
|
||||
rookFromCol = 7
|
||||
rookToCol = 5
|
||||
}
|
||||
|
||||
rookFrom := Position{from.Row, rookFromCol}
|
||||
rookTo := Position{from.Row, rookToCol}
|
||||
g.Board.Move(rookFrom, rookTo)
|
||||
}
|
||||
|
||||
func (g *Game) executeEnPassant(from, to Position) {
|
||||
g.Board.Move(from, to)
|
||||
capturedPawnRow := from.Row
|
||||
g.Board.Set(Position{capturedPawnRow, to.Col}, Piece{Empty, White})
|
||||
}
|
||||
|
||||
func (g *Game) undoMove(from, to Position, piece Piece, originalTarget Piece) {
|
||||
if piece.Type == King && abs(to.Col-from.Col) == 2 {
|
||||
g.undoCastle(from, to, piece.Color)
|
||||
} else if piece.Type == Pawn && g.EnPassantTarget != nil && *g.EnPassantTarget == to {
|
||||
g.undoEnPassant(from, to)
|
||||
} else {
|
||||
g.Board.Set(from, piece)
|
||||
g.Board.Set(to, originalTarget)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) undoCastle(from, to Position, color Color) {
|
||||
g.Board.Set(from, Piece{King, color})
|
||||
g.Board.Set(to, Piece{Empty, White})
|
||||
|
||||
rookFromCol := 0
|
||||
rookToCol := 3
|
||||
if to.Col > from.Col {
|
||||
rookFromCol = 7
|
||||
rookToCol = 5
|
||||
}
|
||||
|
||||
rookFrom := Position{from.Row, rookFromCol}
|
||||
rookTo := Position{from.Row, rookToCol}
|
||||
g.Board.Set(rookFrom, Piece{Rook, color})
|
||||
g.Board.Set(rookTo, Piece{Empty, White})
|
||||
}
|
||||
|
||||
func (g *Game) undoEnPassant(from, to Position) {
|
||||
g.Board.Set(from, Piece{Pawn, g.CurrentTurn})
|
||||
g.Board.Set(to, Piece{Empty, White})
|
||||
capturedPawnRow := from.Row
|
||||
enemyColor := 1 - g.CurrentTurn
|
||||
g.Board.Set(Position{capturedPawnRow, to.Col}, Piece{Pawn, enemyColor})
|
||||
}
|
||||
|
||||
func (g *Game) updateGameState(from, to Position, piece Piece) {
|
||||
g.EnPassantTarget = nil
|
||||
|
||||
if piece.Type == King {
|
||||
g.KingMoved[piece.Color] = true
|
||||
} else if piece.Type == Rook {
|
||||
switch from.Col {
|
||||
case 0:
|
||||
g.RookMoved[piece.Color][0] = true
|
||||
case 7:
|
||||
g.RookMoved[piece.Color][1] = true
|
||||
}
|
||||
} else if piece.Type == Pawn && abs(to.Row-from.Row) == 2 {
|
||||
enPassantRow := (from.Row + to.Row) / 2
|
||||
g.EnPassantTarget = &Position{enPassantRow, from.Col}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) FindKing(color Color) Position {
|
||||
for row := 0; row < 8; row++ {
|
||||
for col := 0; col < 8; col++ {
|
||||
pos := Position{row, col}
|
||||
piece := g.Board.At(pos)
|
||||
if piece.Type == King && piece.Color == color {
|
||||
return pos
|
||||
}
|
||||
}
|
||||
}
|
||||
return Position{-1, -1}
|
||||
}
|
||||
|
||||
func (g *Game) IsInCheck(color Color) bool {
|
||||
kingPos := g.FindKing(color)
|
||||
if !kingPos.Valid() {
|
||||
return false
|
||||
}
|
||||
|
||||
enemyColor := 1 - color
|
||||
|
||||
for row := 0; row < 8; row++ {
|
||||
for col := 0; col < 8; col++ {
|
||||
pos := Position{row, col}
|
||||
piece := g.Board.At(pos)
|
||||
if piece.Type != Empty && piece.Color == enemyColor {
|
||||
if g.canPieceAttack(piece, pos, kingPos) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *Game) canPieceAttack(piece Piece, from, to Position) bool {
|
||||
dx := to.Col - from.Col
|
||||
dy := to.Row - from.Row
|
||||
|
||||
switch piece.Type {
|
||||
case Pawn:
|
||||
direction := 1
|
||||
if piece.Color == Black {
|
||||
direction = -1
|
||||
}
|
||||
return abs(dx) == 1 && dy == direction
|
||||
case Rook:
|
||||
return (dx == 0 || dy == 0) && g.isPathClear(from, to)
|
||||
case Knight:
|
||||
return (abs(dx) == 2 && abs(dy) == 1) || (abs(dx) == 1 && abs(dy) == 2)
|
||||
case Bishop:
|
||||
return abs(dx) == abs(dy) && g.isPathClear(from, to)
|
||||
case Queen:
|
||||
return ((dx == 0 || dy == 0) || (abs(dx) == abs(dy))) && g.isPathClear(from, to)
|
||||
case King:
|
||||
return abs(dx) <= 1 && abs(dy) <= 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *Game) IsCheckmate(color Color) bool {
|
||||
if !g.IsInCheck(color) {
|
||||
return false
|
||||
}
|
||||
|
||||
for row := 0; row < 8; row++ {
|
||||
for col := 0; col < 8; col++ {
|
||||
from := Position{row, col}
|
||||
piece := g.Board.At(from)
|
||||
if piece.Type != Empty && piece.Color == color {
|
||||
for toRow := 0; toRow < 8; toRow++ {
|
||||
for toCol := 0; toCol < 8; toCol++ {
|
||||
to := Position{toRow, toCol}
|
||||
if g.isValidMoveIgnoringCheck(from, to) {
|
||||
originalPiece := g.Board.At(to)
|
||||
g.Board.Move(from, to)
|
||||
inCheck := g.IsInCheck(color)
|
||||
g.Board.Set(from, piece)
|
||||
g.Board.Set(to, originalPiece)
|
||||
|
||||
if !inCheck {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *Game) isValidMoveIgnoringCheck(from, to Position) bool {
|
||||
if !from.Valid() || !to.Valid() {
|
||||
return false
|
||||
}
|
||||
|
||||
piece := g.Board.At(from)
|
||||
if piece.Type == Empty {
|
||||
return false
|
||||
}
|
||||
|
||||
target := g.Board.At(to)
|
||||
if target.Type != Empty && target.Color == piece.Color {
|
||||
return false
|
||||
}
|
||||
|
||||
return g.isValidPieceMove(piece, from, to)
|
||||
}
|
||||
|
||||
func (g *Game) GameStatus() string {
|
||||
if g.IsCheckmate(g.CurrentTurn) {
|
||||
winner := "White"
|
||||
if g.CurrentTurn == White {
|
||||
winner = "Black"
|
||||
}
|
||||
return fmt.Sprintf("Checkmate! %s wins!", winner)
|
||||
}
|
||||
|
||||
if g.IsInCheck(g.CurrentTurn) {
|
||||
return fmt.Sprintf("%s is in check!", g.CurrentTurn)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func sign(x int) int {
|
||||
if x > 0 {
|
||||
return 1
|
||||
}
|
||||
if x < 0 {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
26
go.mod
Normal file
26
go.mod
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
module chess-tui
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require github.com/charmbracelet/bubbletea v1.3.9
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
||||
43
go.sum
Normal file
43
go.sum
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbletea v1.3.9 h1:OBYdfRo6QnlIcXNmcoI2n1NNS65Nk6kI2L2FO1puS/4=
|
||||
github.com/charmbracelet/bubbletea v1.3.9/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
293
main.go
Normal file
293
main.go
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
game *Game
|
||||
cursorRow int
|
||||
cursorCol int
|
||||
selected *Position
|
||||
validMoves []Position
|
||||
}
|
||||
|
||||
func initialModel() model {
|
||||
return model{
|
||||
game: NewGame(),
|
||||
cursorRow: 0,
|
||||
cursorCol: 0,
|
||||
selected: nil,
|
||||
validMoves: make([]Position, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC:
|
||||
return m, tea.Quit
|
||||
case tea.KeyEscape:
|
||||
m.selected = nil
|
||||
m.validMoves = make([]Position, 0)
|
||||
}
|
||||
|
||||
switch msg.String() {
|
||||
case "q":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.cursorRow < 7 {
|
||||
m.cursorRow++
|
||||
}
|
||||
case "down", "j":
|
||||
if m.cursorRow > 0 {
|
||||
m.cursorRow--
|
||||
}
|
||||
case "left", "h":
|
||||
if m.cursorCol > 0 {
|
||||
m.cursorCol--
|
||||
}
|
||||
case "right", "l":
|
||||
if m.cursorCol < 7 {
|
||||
m.cursorCol++
|
||||
}
|
||||
case "enter", " ":
|
||||
currentPos := Position{m.cursorRow, m.cursorCol}
|
||||
|
||||
if m.selected == nil {
|
||||
piece := m.game.Board.At(currentPos)
|
||||
if piece.Type != Empty && piece.Color == m.game.CurrentTurn {
|
||||
m.selected = ¤tPos
|
||||
m.validMoves = m.getValidMoves(currentPos)
|
||||
}
|
||||
} else {
|
||||
if *m.selected == currentPos {
|
||||
m.selected = nil
|
||||
m.validMoves = make([]Position, 0)
|
||||
} else if m.game.MakeMove(*m.selected, currentPos) {
|
||||
m.selected = nil
|
||||
m.validMoves = make([]Position, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) getValidMoves(from Position) []Position {
|
||||
var moves []Position
|
||||
|
||||
for row := range 8 {
|
||||
for col := range 8 {
|
||||
to := Position{row, col}
|
||||
if m.game.IsValidMove(from, to) {
|
||||
moves = append(moves, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moves
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
var s strings.Builder
|
||||
|
||||
s.WriteString("Use arrow keys to move cursor, ENTER/SPACE to select/move, ESC to deselect, Q to quit\n\n")
|
||||
|
||||
status := m.game.GameStatus()
|
||||
if status != "" {
|
||||
s.WriteString(fmt.Sprintf("*** %s ***\n\n", status))
|
||||
}
|
||||
|
||||
s.WriteString(m.renderBoardWithInfo())
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (m model) renderBoardWithInfo() string {
|
||||
boardLines := m.getBoardLines()
|
||||
infoLines := m.getInfoLines()
|
||||
|
||||
var s strings.Builder
|
||||
maxLines := len(boardLines)
|
||||
if len(infoLines) > maxLines {
|
||||
maxLines = len(infoLines)
|
||||
}
|
||||
|
||||
for i := 0; i < maxLines; i++ {
|
||||
if i < len(boardLines) {
|
||||
s.WriteString(boardLines[i])
|
||||
} else {
|
||||
s.WriteString(strings.Repeat(" ", 27)) // Board width padding
|
||||
}
|
||||
|
||||
s.WriteString(" ")
|
||||
|
||||
if i < len(infoLines) {
|
||||
s.WriteString(infoLines[i])
|
||||
}
|
||||
|
||||
s.WriteString("\n")
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (m model) getBoardLines() []string {
|
||||
var lines []string
|
||||
|
||||
lines = append(lines, " a b c d e f g h ")
|
||||
|
||||
for row := 7; row >= 0; row-- {
|
||||
var line strings.Builder
|
||||
line.WriteString(fmt.Sprintf("%d ", row+1))
|
||||
|
||||
for col := 0; col < 8; col++ {
|
||||
pos := Position{row, col}
|
||||
piece := m.game.Board.At(pos)
|
||||
|
||||
cellChar := piece.String()
|
||||
if piece.Type == Empty {
|
||||
if (row+col)%2 == 0 {
|
||||
cellChar = "·"
|
||||
} else {
|
||||
cellChar = " "
|
||||
}
|
||||
}
|
||||
|
||||
if m.cursorRow == row && m.cursorCol == col {
|
||||
line.WriteString(fmt.Sprintf("[%s]", cellChar))
|
||||
} else if m.selected != nil && m.selected.Row == row && m.selected.Col == col {
|
||||
line.WriteString(fmt.Sprintf("<%s>", cellChar))
|
||||
} else if slices.Contains(m.validMoves, pos) {
|
||||
line.WriteString(fmt.Sprintf("*%s*", cellChar))
|
||||
} else {
|
||||
line.WriteString(fmt.Sprintf(" %s ", cellChar))
|
||||
}
|
||||
}
|
||||
|
||||
line.WriteString(fmt.Sprintf(" %d", row+1))
|
||||
lines = append(lines, line.String())
|
||||
}
|
||||
|
||||
lines = append(lines, " a b c d e f g h ")
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m model) getInfoLines() []string {
|
||||
var lines []string
|
||||
|
||||
lines = append(lines, "┌─────────────────────┐")
|
||||
lines = append(lines, "│ GAME INFO │")
|
||||
lines = append(lines, "├─────────────────────┤")
|
||||
lines = append(lines, fmt.Sprintf("│ Turn: %-13s │", m.game.CurrentTurn))
|
||||
lines = append(lines, "│ │")
|
||||
|
||||
cursorPos := Position{m.cursorRow, m.cursorCol}
|
||||
piece := m.game.Board.At(cursorPos)
|
||||
lines = append(lines, fmt.Sprintf("│ Cursor: %-11s │", cursorPos.String()))
|
||||
|
||||
if piece.Type == Empty {
|
||||
lines = append(lines, "│ Piece: Empty │")
|
||||
} else {
|
||||
pieceName := m.getPieceName(piece)
|
||||
lines = append(lines, fmt.Sprintf("│ Piece: %-12s │", pieceName))
|
||||
}
|
||||
|
||||
lines = append(lines, "│ │")
|
||||
|
||||
if m.selected != nil {
|
||||
lines = append(lines, "├─────────────────────┤")
|
||||
selectedPiece := m.game.Board.At(*m.selected)
|
||||
selectedName := m.getPieceName(selectedPiece)
|
||||
lines = append(lines, fmt.Sprintf("│ Selected: %-10s │", selectedName))
|
||||
lines = append(lines, fmt.Sprintf("│ At: %-15s │", m.selected.String()))
|
||||
|
||||
if len(m.validMoves) > 0 {
|
||||
lines = append(lines, "│ │")
|
||||
lines = append(lines, "│ Valid moves: │")
|
||||
|
||||
// Show up to 6 valid moves
|
||||
moveCount := len(m.validMoves)
|
||||
if moveCount > 6 {
|
||||
moveCount = 6
|
||||
}
|
||||
|
||||
for i := 0; i < moveCount; i += 2 {
|
||||
var moveLine strings.Builder
|
||||
moveLine.WriteString("│ ")
|
||||
moveLine.WriteString(m.validMoves[i].String())
|
||||
|
||||
if i+1 < moveCount {
|
||||
moveLine.WriteString(fmt.Sprintf(" %s", m.validMoves[i+1].String()))
|
||||
}
|
||||
|
||||
// Pad to fit box width
|
||||
for moveLine.Len() < 20 {
|
||||
moveLine.WriteString(" ")
|
||||
}
|
||||
moveLine.WriteString(" │")
|
||||
|
||||
lines = append(lines, moveLine.String())
|
||||
}
|
||||
|
||||
if len(m.validMoves) > 6 {
|
||||
lines = append(lines, fmt.Sprintf("│ ... and %d more │", len(m.validMoves)-6))
|
||||
}
|
||||
}
|
||||
}
|
||||
lines = append(lines, "└─────────────────────┘")
|
||||
|
||||
if len(m.game.MoveHistory) > 0 {
|
||||
lastMove := m.game.MoveHistory[len(m.game.MoveHistory)-1]
|
||||
lines = append(lines, fmt.Sprintf("Last move: %s -> %-12s", lastMove.From.String(), lastMove.To.String()))
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m model) getPieceName(piece Piece) string {
|
||||
if piece.Type == Empty {
|
||||
return "Empty"
|
||||
}
|
||||
|
||||
color := "White"
|
||||
if piece.Color == Black {
|
||||
color = "Black"
|
||||
}
|
||||
|
||||
pieceType := ""
|
||||
switch piece.Type {
|
||||
case Pawn:
|
||||
pieceType = "Pawn"
|
||||
case Rook:
|
||||
pieceType = "Rook"
|
||||
case Knight:
|
||||
pieceType = "Knight"
|
||||
case Bishop:
|
||||
pieceType = "Bishop"
|
||||
case Queen:
|
||||
pieceType = "Queen"
|
||||
case King:
|
||||
pieceType = "King"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", color, pieceType)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue