Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for squares background colors and themes #13

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 58 additions & 6 deletions border/border.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/maaslalani/gambit/board"
"github.com/maaslalani/gambit/position"
"github.com/maaslalani/gambit/style"
)

const (
Expand Down Expand Up @@ -44,19 +45,70 @@ func Build(left, middle, right string) string {
return withMarginLeft(border)
}

func topWithTheme(theme style.Theme) string {
var s strings.Builder
// top-left square is light
isLightSquare := true
for i := 0; i < board.Cols; i++ {
for j := 0; j < cellWidth; j++ {
s.WriteString(theme.Fg("\u2583", isLightSquare))
}
isLightSquare = !isLightSquare
}
s.WriteRune('\n')
return withMarginLeft(s.String())
}

// Top returns a built border with the top row
func Top() string {
return Build("┌", "┬", "┐")
func Top(theme style.Theme) string {
if !theme.IsValid() {
return Build("┌", "┬", "┐")
}
return topWithTheme(theme)
}

func middleWithTheme(theme style.Theme, firstSquareIsLight bool) string {
var s strings.Builder
isLightSquare := firstSquareIsLight
for i := 0; i < board.Cols; i++ {
for j := 0; j < cellWidth; j++ {
s.WriteString(theme.Border("\u2580", isLightSquare))
}
isLightSquare = !isLightSquare
}
s.WriteRune('\n')
return withMarginLeft(s.String())
}

// Middle returns a built border with the middle row
func Middle() string {
return Build("├", "┼", "┤")
func Middle(theme style.Theme, firstSquareIsLight bool) string {
if !theme.IsValid() {
return Build("├", "┼", "┤")
}
return middleWithTheme(theme, firstSquareIsLight)
}

// bottomWithTheme returns built border with the bottom row
func bottomWithTheme(theme style.Theme) string {
var s strings.Builder
// bottom-left square is dark
isLightSquare := false
for i := 0; i < board.Cols; i++ {
for j := 0; j < cellWidth; j++ {
s.WriteString(theme.Fg("\u2580", isLightSquare))
}
isLightSquare = !isLightSquare
}
s.WriteRune('\n')
return withMarginLeft(s.String())
}

// Bottom returns a built border with the bottom row
func Bottom() string {
return Build("└", "┴", "┘")
func Bottom(theme style.Theme) string {
if !theme.IsValid() {
return Build("└", "┴", "┘")
}
return bottomWithTheme(theme)
}

// BottomLabels returns the labels for the files
Expand Down
132 changes: 103 additions & 29 deletions game/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Game struct {
selected string
buffer string
flipped bool
theme Theme
}

// NewGame returns an initial model of the game board.
Expand All @@ -64,11 +65,106 @@ func NewGameWithPosition(position string) *Game {
return m
}

// NewGameWithPosition returns an initial model of the game board with the
// specified FEN position.
func NewGameWithPositionAndTheme(position string, theme Theme) *Game {
m := NewGameWithPosition(position)
m.theme = theme

return m
}

// Init Initializes the model
func (m *Game) Init() tea.Cmd {
return nil
}

// drawRankNumber returns the string with the rank number and the eventual
// border
func (m *Game) drawRankNumber(rank int) string {
rankString := Faint(fmt.Sprintf(" %d ", rank+1))
if !m.theme.IsValid() {
rankString = rankString + border.Vertical
}
return rankString
}

// renderSelectedPiece returns the string of the piece with the selected
// color based on the theme
func (m *Game) renderSelectedPiece(display string, isLightSquare bool) string {
if m.theme.IsValid() {
return m.theme.SelectedSquare(display, isLightSquare)
}
return Cyan(display)
}

// renderPieceInCheck returns the string of the piece with the check
// color based on the theme
func (m *Game) renderPieceInCheck(display string, isLightSquare bool) string {
if m.theme.IsValid() {
return m.theme.CheckSquare(display, isLightSquare)
}
return Red(display)
}

// renderAvailableMove returns the string of the piece with the availableMove
// color based on the theme
func (m *Game) renderAvailableMove(display string, isLightSquare bool) string {
if m.theme.IsValid() {
return m.theme.AvailableMove(display, isLightSquare)
}
return Magenta(display)
}

// renderNormalPiece returns the string of the piece based on the theme
func (m *Game) renderNormalPiece(display string, isWhite bool, isLightSquare bool) string {
if m.theme.IsValid() {
display = m.theme.Piece(display, isWhite, isLightSquare)
}
return display
}

// drawSquare returns the string of the piece and square to show
func (m *Game) drawSquare(piece pieces.Piece, square string) string {
var s strings.Builder
whiteTurn := m.board.Wtomove
display := piece.Display()
check := m.board.OurKingInCheck()
selected := square
isLightSquare := position.IsLightSquare(square)

if m.theme.IsValid() {
// display first space
s.WriteString(m.theme.Bg(" ", isLightSquare))
}

// The user selected the current cell, highlight it so they know it is
// selected. If it is a check, highlight the king in red.
if m.selected == selected {
display = m.renderSelectedPiece(display, isLightSquare)
} else if check && piece.IsKing() &&
((whiteTurn && piece.IsWhite()) || (!whiteTurn && piece.IsBlack())) {
display = m.renderPieceInCheck(display, isLightSquare)
} else {
display = m.renderNormalPiece(display, piece.IsWhite(), isLightSquare)
}

// Show all the cells to which the piece may move. If it is an empty cell
// we present a coloured dot, otherwise color the capturable piece.
if moves.IsLegal(m.pieceMoves, selected) && piece.IsEmpty() {
display = m.renderAvailableMove(".", isLightSquare)
}

if m.theme.IsValid() {
s.WriteString(display)
s.WriteString(m.theme.Bg(" ", isLightSquare))
} else {
s.WriteString(fmt.Sprintf(" %s %s", display, border.Vertical))
}

return s.String()
}

// View converts a FEN string into a human readable chess board. All pieces and
// empty squares are arranged in a grid-like pattern. The selected piece is
// highlighted and the legal moves for the selected piece are indicated by a
Expand Down Expand Up @@ -99,7 +195,7 @@ func (m *Game) Init() tea.Cmd {
//
func (m *Game) View() string {
var s strings.Builder
s.WriteString(border.Top())
s.WriteString(border.Top(m.theme))

// Traverse through the rows and columns of the board and print out the
// pieces and empty squares. Once a piece is selected, highlight the legal
Expand All @@ -119,43 +215,21 @@ func (m *Game) View() string {
rr = r
}

s.WriteString(Faint(fmt.Sprintf(" %d ", rr+1)) + border.Vertical)
s.WriteString(m.drawRankNumber(rr))

for c, piece := range row {
whiteTurn := m.board.Wtomove
display := piece.Display()
check := m.board.OurKingInCheck()
selected := position.ToSquare(r, c, m.flipped)

// The user selected the current cell, highlight it so they know it is
// selected. If it is a check, highlight the king in red.
if m.selected == selected {
display = Cyan(display)
} else if check && piece.IsKing() {
if (whiteTurn && piece.IsWhite()) || (!whiteTurn && piece.IsBlack()) {
display = Red(display)
}
}

// Show all the cells to which the piece may move. If it is an empty cell
// we present a coloured dot, otherwise color the capturable piece.
if moves.IsLegal(m.pieceMoves, selected) {
if piece.IsEmpty() {
display = "."
}
display = Magenta(display)
}

s.WriteString(fmt.Sprintf(" %s %s", display, border.Vertical))
square := position.ToSquare(r, c, m.flipped)
s.WriteString(m.drawSquare(piece, square))
}
s.WriteRune('\n')

if r != board.LastRow {
s.WriteString(border.Middle())
firstSquareIsLight := position.IsLightSquare(position.ToSquare(r, 0, m.flipped))
s.WriteString(border.Middle(m.theme, firstSquareIsLight))
}
}

s.WriteString(border.Bottom() + Faint(border.BottomLabels(m.flipped)))
s.WriteString(border.Bottom(m.theme) + Faint(border.BottomLabels(m.flipped)))
return s.String()
}

Expand Down
20 changes: 18 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,31 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/maaslalani/gambit/cmd"
"github.com/maaslalani/gambit/game"
"github.com/maaslalani/gambit/style"
"github.com/muesli/coral"
)

var (
Version = ""
CommitSHA = ""
ThemeFile = ""

rootCmd = &coral.Command{
Use: "gambit",
Short: "Play chess in your terminal",
RunE: func(cmd *coral.Command, args []string) error {
if len(args) == 0 {

// initialize theme
var err error
theme := style.Theme{}
if ThemeFile != "" {
theme, err = style.ParseThemeFile(ThemeFile)
if err != nil {
return err
}
}

startPos, _ := readStdin()

debug := os.Getenv("DEBUG")
Expand All @@ -36,12 +49,12 @@ var (
}

p := tea.NewProgram(
game.NewGameWithPosition(startPos),
game.NewGameWithPositionAndTheme(startPos, theme),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)

_, err := p.Run()
_, err = p.Run()
return err
}

Expand All @@ -68,6 +81,9 @@ func init() {
rootCmd.AddCommand(
cmd.ServeCmd,
)

// add flag -t (--theme) to specify the theme file to use
rootCmd.Flags().StringVarP(&ThemeFile, "theme", "t", "", "Theme file path")
}

func main() {
Expand Down
24 changes: 12 additions & 12 deletions pieces/pieces.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ type Piece string
// representations for a more human readable experience.
var display = map[Piece]string{
"": " ",
"B": "",
"K": "",
"N": "",
"P": "",
"Q": "",
"R": "",
"b": "",
"k": "",
"n": "",
"p": "",
"q": "",
"r": "",
"B": "\u2657",
"K": "\u2654",
"N": "\u2658",
"P": "\u2659",
"Q": "\u2655",
"R": "\u2656",
"b": "\u265d",
"k": "\u265a",
"n": "\u265e",
"p": "\u265f",
"q": "\u265b",
"r": "\u265c",
}

// IsWhite returns true if the piece is white.
Expand Down
14 changes: 14 additions & 0 deletions position/position.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,17 @@ func ToSquare(row, col int, flipped bool) string {
}
return colToFile(col) + strconv.Itoa(rowToRank(row))
}

// IsLightSquare returns true if the square in given position is light
func IsLightSquare(position string) bool {
file := position[0]
rank, err := strconv.Atoi(string(position[1]))
if err != nil {
panic(err)
}
// Rule: if the file and rank are both odd or both even, the square
// is dark, otherwise it is light
isOddFile := file%2 == 1
isOddRank := rank%2 == 1
return isOddFile != isOddRank
}
1 change: 0 additions & 1 deletion style/style.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ var Cyan = fg("6")
var Faint = fg("8")
var Magenta = fg("5")
var Red = fg("1")

var Title = NewStyle().Foreground(Color("5")).Italic(true).Render
Loading