1
0
mirror of https://github.com/maaslalani/gambit.git synced 2024-11-24 08:22:12 +02:00

holy shit

This commit is contained in:
Maas Lalani 2021-12-24 12:55:46 -05:00
parent 139ce2b1a1
commit c3744d422c
No known key found for this signature in database
GPG Key ID: 5A6ED5CBF1A0A000
12 changed files with 44 additions and 588 deletions

View File

@ -1,29 +0,0 @@
package board
import (
"github.com/maaslalani/gambit/piece"
"github.com/maaslalani/gambit/position"
)
type Board struct {
// The board is represented as a 2D array of cells.
// The first index is the row, the second is the column.
Grid [8][8]piece.Piece
Reversed bool
Selected position.Position
Turn piece.Color
}
func New() Board {
ep := piece.Empty()
er := [8]piece.Piece{ep, ep, ep, ep, ep, ep, ep, ep}
return Board{
Grid: [8][8]piece.Piece{er, er, er, er, er, er, er, er},
Turn: piece.White,
Selected: position.NoPosition,
}
}
func (b *Board) At(p position.Position) piece.Piece {
return b.Grid[p.Row][p.Col]
}

View File

@ -1,121 +0,0 @@
package board
import (
"strconv"
"strings"
"github.com/maaslalani/gambit/piece"
)
// FromFen parses a FEN string and returns a Board with the corresponding
// attributes and pieces
func FromFen(fen string) (Board, error) {
var b Board = New()
// Split the FEN string into its component parts
parts := strings.Split(fen, " ")
// 1. Piece placement (from White's perspective). Each rank
// is described, starting with rank 8 and ending with rank 1;
// within each rank, the contents of each square are described
// from file "a" through file "h". Following the Standard
// Algebraic Notation (SAN), each piece is identified by a
// single letter taken from the standard English names (pawn =
// "P", knight = "N", bishop = "B", rook = "R", queen = "Q" and
// king = "K"). White pieces are designated using upper-case
// letters ("PNBRQK") while black pieces use lowercase
// ("pnbrqk"). Empty squares are noted using digits 1 through 8
// (the number of empty squares), and "/" separates ranks.
ranks := strings.Split(parts[0], "/")
for r, rank := range ranks {
col := 0
for _, char := range rank {
if char >= '1' && char <= '8' {
col += int(char - '0')
continue
}
p := piece.FromFen(string(char))
b.Grid[7-r][col] = p
col += 1
}
}
// 2. Active color.
// "w" means White moves next, "b" means Black moves next.
b.Turn = piece.Color(parts[1])
// 3. Castling availability.
// If neither side can castle, this is "-". Otherwise, this has one or more
// letters: "K" (White can castle kingside), "Q" (White can castle queenside),
// "k" (Black can castle kingside), and/or "q" (Black can castle queenside). A
// move that temporarily prevents castling does not negate this notation.
// 4. En passant target square in algebraic notation.
// If there's no en passant target square, this is "-". If a pawn has just
// made a two-square move, this is the position "behind" the pawn. This is
// recorded regardless of whether there is a pawn in position to make an en
// passant capture.
// 5. Halfmove clock.
// The number of halfmoves since the last capture or pawn advance, used for
// the fifty-move rule.
// 6. Fullmove number.
// The number of the full move. It starts at 1, and is incremented after
// Black's move.
return b, nil
}
// ToFen converts a board into a FEN string
func (b Board) ToFen() string {
var sb strings.Builder
// Loop through the entire board and build the FEN string for each rank
// The FEN string is built from the bottom up, so we need to reverse the grid
for r := len(b.Grid) - 1; r >= 0; r-- {
// Track the number of empty squares we have encountered so far before reaching a
// non-empty square, dump this count as a replacement for pieces
emptyCounter := 0
// Loop through each column in the rank and convert it to its FEN equivalent
for c := 0; c < len(b.Grid[r]); c++ {
p := b.Grid[r][c]
if p.Color == piece.NoColor {
// Empty square
emptyCounter += 1
} else {
// If we have encountered an empty square, dump the number of
// empty squares we have encountered so far
if emptyCounter > 0 {
sb.WriteString(strconv.Itoa(emptyCounter))
emptyCounter = 0
}
}
// Display the piece's Fen representation
sb.WriteString(p.ToFen())
}
// If we have reached the end of the rank and we have encountered
// empty squares dump the number of empty squares
if emptyCounter > 0 {
sb.WriteString(strconv.Itoa(emptyCounter))
emptyCounter = 0
}
if r > 0 {
sb.WriteRune('/')
}
}
sb.WriteString(" " + string(b.Turn) + " ")
// TODO: Add castling
sb.WriteString("KQkq")
// TODO: Add En passant target squares
sb.WriteString(" - ")
// TODO: Add halfmove + fullmove clock
sb.WriteString("0 1")
return sb.String()
}

View File

@ -1,62 +0,0 @@
package board_test
import (
"testing"
"github.com/maaslalani/gambit/board"
. "github.com/maaslalani/gambit/piece"
)
func TestFen(t *testing.T) {
fen := "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
grid := [8][8]Piece{
{RW, NW, BW, QW, KW, BW, NW, RW},
{PW, PW, PW, PW, PW, PW, PW, PW},
{OO, OO, OO, OO, OO, OO, OO, OO},
{OO, OO, OO, OO, OO, OO, OO, OO},
{OO, OO, OO, OO, OO, OO, OO, OO},
{OO, OO, OO, OO, OO, OO, OO, OO},
{PB, PB, PB, PB, PB, PB, PB, PB},
{RB, NB, BB, QB, KB, BB, NB, RB},
}
b := board.Board{
Grid: grid,
Turn: White,
}
if b.ToFen() != fen {
t.Errorf("ToFen()\nActual = %s\nTarget = %s", b.ToFen(), fen)
}
fromFen, _ := board.FromFen(fen)
if fromFen.ToFen() != fen {
t.Errorf("FromFen()\nActual = %s\nTarget = %s", fromFen.ToFen(), fen)
}
}
func TestFenMoved(t *testing.T) {
fen := "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1"
grid := [8][8]Piece{
{RW, NW, BW, QW, KW, BW, NW, RW},
{PW, PW, PW, PW, OO, PW, PW, PW},
{OO, OO, OO, OO, OO, OO, OO, OO},
{OO, OO, OO, OO, PW, OO, OO, OO},
{OO, OO, OO, OO, OO, OO, OO, OO},
{OO, OO, OO, OO, OO, OO, OO, OO},
{PB, PB, PB, PB, PB, PB, PB, PB},
{RB, NB, BB, QB, KB, BB, NB, RB},
}
b := board.Board{
Grid: grid,
Turn: White,
}
if b.ToFen() != fen {
t.Errorf("ToFen()\nActual = %s\nTarget = %s", b.ToFen(), fen)
}
fromFen, _ := board.FromFen(fen)
if fromFen.ToFen() != fen {
t.Errorf("FromFen()\nActual = %s\nTarget = %s", fromFen, fen)
}
}

View File

@ -1,19 +0,0 @@
package board
import (
"github.com/maaslalani/gambit/piece"
"github.com/maaslalani/gambit/position"
"github.com/maaslalani/gambit/squares"
)
type Move struct {
From squares.Square
To squares.Square
}
func (b *Board) Move(m Move) {
f := position.ToPosition(m.From)
t := position.ToPosition(m.To)
b.Grid[t.Row][t.Col] = b.Grid[f.Row][f.Col]
b.Grid[f.Row][f.Col] = piece.Empty()
}

View File

@ -1,32 +0,0 @@
package board
import (
"testing"
. "github.com/maaslalani/gambit/squares"
)
func TestMove(t *testing.T) {
initial := "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
tests := []struct {
board string
moves []Move
expected string
}{
{initial, []Move{{E2, E4}}, "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1"},
{initial, []Move{{A7, A6}}, "rnbqkbnr/1ppppppp/p7/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"},
{initial, []Move{{G1, F3}}, "rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R w KQkq - 0 1"},
{initial, []Move{{E2, E4}, {C7, C5}, {G1, F3}}, "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 0 1"},
}
for _, tc := range tests {
b, _ := FromFen(tc.board)
for _, move := range tc.moves {
b.Move(move)
}
if b.ToFen() != tc.expected {
t.Errorf("\nwant %s\ngot %s", tc.expected, b.ToFen())
// t.Log(b)
}
}
}

View File

@ -1,27 +1,43 @@
package game
import (
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/maaslalani/gambit/board"
dt "github.com/dylhunn/dragontoothmg"
"github.com/maaslalani/gambit/piece"
"github.com/maaslalani/gambit/position"
. "github.com/maaslalani/gambit/squares"
)
type model struct {
board board.Board
moves []board.Move
board dt.Board
}
func Model() tea.Model {
b, _ := board.FromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
board := dt.ParseFen("rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 1")
return model{
board: b,
board: board,
}
}
func (m model) Init() tea.Cmd { return nil }
func (m model) View() string { return m.board.String() }
func (m model) View() string {
var s strings.Builder
ranks := strings.Split(strings.Split(m.board.ToFen(), " ")[0], "/")
for _, r := range ranks {
for _, c := range r {
if c >= '1' && c <= '8' {
for i := 0; i < int(c-'0'); i++ {
s.WriteString(piece.Display[""])
}
} else {
s.WriteString(piece.Display[string(c)])
}
}
s.WriteRune('\n')
}
return s.String()
}
const (
cellHeight = 2
@ -36,53 +52,11 @@ const (
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
col := (msg.X - marginLeft) / cellWidth
row := (msg.Y - marginTop) / cellHeight
if col < 0 || col > maxCol || row < 0 || row > maxRow {
m.board.Selected = position.NoPosition
return m, nil
}
if !m.board.Reversed {
row = maxRow - row
}
if msg.Type != tea.MouseRelease {
return m, nil
}
if m.board.Selected == position.NoPosition {
pos := position.Position{Row: row, Col: col}
if m.board.At(pos) == piece.EmptyPiece {
return m, nil
}
m.board.Selected = pos
} else {
from := Square(m.board.Selected.String())
toPos := position.Position{Row: row, Col: col}
to := Square(toPos.String())
// Don't allow moving to the same square
if from == to {
return m, nil
}
// Don't allow moving to a square with a piece of the same
// color as the selected piece
if m.board.At(m.board.Selected).Color == m.board.At(toPos).Color {
m.board.Selected = toPos
return m, nil
}
// Valid move
move := board.Move{From: from, To: to}
m.moves = append(m.moves, move)
m.board.Move(move)
m.board.Selected = position.NoPosition
}
case tea.KeyMsg:
switch msg.String() {
case " ":
move := m.board.GenerateLegalMoves()[0]
m.board.Apply(move)
case "ctrl+c", "q":
return m, tea.Quit
}

1
go.mod
View File

@ -9,6 +9,7 @@ require (
require (
github.com/containerd/console v1.0.2 // indirect
github.com/dylhunn/dragontoothmg v0.0.0-20170905201839-b0146de1e275 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect

2
go.sum
View File

@ -4,6 +4,8 @@ github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+Pp
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE=
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/dylhunn/dragontoothmg v0.0.0-20170905201839-b0146de1e275 h1:PDfC81w74XKfP1qDVvKrcL+isYNi7FBunA4zU+eMlME=
github.com/dylhunn/dragontoothmg v0.0.0-20170905201839-b0146de1e275/go.mod h1:L6ZI7rasNVYqjj/tpfqYRowKPuSQO71UCBBhPxamiDQ=
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.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=

View File

@ -1,110 +1,17 @@
package piece
import (
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/maaslalani/gambit/style"
)
// Color of pieces
type Color string
const (
White Color = "w"
Black Color = "b"
NoColor Color = ""
)
// Types of pieces
type Type string
const (
Pawn Type = "P"
Knight Type = "N"
Bishop Type = "B"
Rook Type = "R"
Queen Type = "Q"
King Type = "K"
NoType Type = ""
)
func (t Type) String() string {
return string(t)
var Display = map[string]string{
"": " ",
"B": "♗",
"K": "♔",
"N": "♘",
"P": "♙",
"Q": "♕",
"R": "♖",
"b": "♝",
"k": "♚",
"n": "♞",
"p": "♟",
"q": "♛",
"r": "♜",
}
// Pieces
type Piece struct {
Type Type
Color Color
}
func (p Piece) String() string {
return Display[p.Type]
}
func Empty() Piece {
return Piece{NoType, NoColor}
}
var Display = map[Type]string{
Bishop: "♝",
King: "♚",
Knight: "♞",
Pawn: "♟",
Queen: "♛",
Rook: "♜",
NoType: " ",
}
// ToFen converts a piece into its FEN representation
// i.e. White Knight -> "N"
// Black Bishop -> "b"
// if a piece is empty, it returns an empty string
func (p Piece) ToFen() string {
t := string(p.Type)
if p.Color == Black {
return strings.ToLower(t)
}
return t
}
// FromFen converts a FEN representation of a piece into a
// piece. Reverses ToFen.
func FromFen(fen string) Piece {
u := strings.ToUpper(fen)
t := Type(u)
var c Color
if u == fen {
c = White
} else {
c = Black
}
return Piece{Type: t, Color: c}
}
func (p Piece) Style() lipgloss.Style {
if p.Color == White {
return style.White
} else {
return style.Black
}
}
var EmptyPiece = Piece{NoType, NoColor}
var (
BB = Piece{Bishop, Black}
BW = Piece{Bishop, White}
KB = Piece{King, Black}
KW = Piece{King, White}
NB = Piece{Knight, Black}
NW = Piece{Knight, White}
PB = Piece{Pawn, Black}
PW = Piece{Pawn, White}
QB = Piece{Queen, Black}
QW = Piece{Queen, White}
RB = Piece{Rook, Black}
RW = Piece{Rook, White}
OO = Piece{NoType, NoColor}
)

View File

@ -1,53 +0,0 @@
package position
import (
"fmt"
"strconv"
"github.com/maaslalani/gambit/squares"
)
// Position represents a position on the board
type Position struct {
// Row represents the row number of the cell in the board,
// this can easily be converted to a human readable rank
Row int // rank
// Col represents the column number of the cell in the board,
// this can easily be converted to a human readable file
Col int // file
}
// String takes the current position and returns a human readable file and
// rank in chess notation
func (p Position) String() string {
return ColumnToFile(p.Col) + RowToRank(p.Row)
}
// ToPosition reads a rank and file number and returns the corresponding
// position on the board's grid
func ToPosition(s squares.Square) Position {
return Position{RankToRow(s[1]), FileToColumn(s[0])}
}
// RowToRank converts a row number to a human readable rank
func RowToRank(row int) string {
return fmt.Sprintf("%d", row+1)
}
// RankToRow converts a human readable rank number to a board row number
func RankToRow(rank byte) int {
parsed, _ := strconv.Atoi(string(rank))
return parsed - 1
}
// ColumnToFile converts a column number to a human readable file
func ColumnToFile(column int) string {
return fmt.Sprintf("%c", column+'A')
}
// FileToColumn converts a human readable file to a board column number
func FileToColumn(file byte) int {
return int(file - 'A')
}
var NoPosition = Position{-1, -1}

View File

@ -1,38 +0,0 @@
package position
import (
"testing"
. "github.com/maaslalani/gambit/squares"
)
func TestPosition(t *testing.T) {
tt := []struct {
s Square
row int
col int
}{
{A8, 7, 0},
{B7, 6, 1},
{C6, 5, 2},
{D5, 4, 3},
{E4, 3, 4},
{F3, 2, 5},
{G2, 1, 6},
{H1, 0, 7},
}
for i, tc := range tt {
p := Position{tc.row, tc.col}
if p.String() != string(tc.s) {
t.Errorf("Test %d: expected %s, got %s", i, tc.s, p.String())
}
}
for i, tc := range tt {
p := ToPosition(tc.s)
if p.Col != tc.col || p.Row != tc.row {
t.Errorf("Test %d: expected %s, got %s", i, tc.s, p.String())
}
}
}

View File

@ -1,74 +0,0 @@
package squares
type Square string
const (
A1 Square = "A1"
A2 = "A2"
A3 = "A3"
A4 = "A4"
A5 = "A5"
A6 = "A6"
A7 = "A7"
A8 = "A8"
B1 = "B1"
B2 = "B2"
B3 = "B3"
B4 = "B4"
B5 = "B5"
B6 = "B6"
B7 = "B7"
B8 = "B8"
C1 = "C1"
C2 = "C2"
C3 = "C3"
C4 = "C4"
C5 = "C5"
C6 = "C6"
C7 = "C7"
C8 = "C8"
D1 = "D1"
D2 = "D2"
D3 = "D3"
D4 = "D4"
D5 = "D5"
D6 = "D6"
D7 = "D7"
D8 = "D8"
E1 = "E1"
E2 = "E2"
E3 = "E3"
E4 = "E4"
E5 = "E5"
E6 = "E6"
E7 = "E7"
E8 = "E8"
F1 = "F1"
F2 = "F2"
F3 = "F3"
F4 = "F4"
F5 = "F5"
F6 = "F6"
F7 = "F7"
F8 = "F8"
G1 = "G1"
G2 = "G2"
G3 = "G3"
G4 = "G4"
G5 = "G5"
G6 = "G6"
G7 = "G7"
G8 = "G8"
H1 = "H1"
H2 = "H2"
H3 = "H3"
H4 = "H4"
H5 = "H5"
H6 = "H6"
H7 = "H7"
H8 = "H8"
)
func (s Square) String() string {
return string(s)
}