Terminal-based online backgammon client (FIBS)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

748 lines
16 KiB

package main
import (
"bytes"
"fmt"
"log"
"strconv"
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/gdamore/tcell/v2"
)
// TODO Add PlayerName, etc
const (
StateLength = iota
StatePlayerScore
StateOpponentScore
StateBoardSpace0
)
const (
StateTurn = 29 + iota
StatePlayerDice1
StatePlayerDice2
StateOpponentDice1
StateOpponentDice2
StateDoublingValue
StatePlayerMayDouble
StateOpponentMayDouble
StateWasDoubled
StatePlayerColor
StateDirection
StateObsoleteHome
StateObsoleteBar
StatePlayerHome
StateOpponentHome
StatePlayerBar
StateOpponentBar
StateMovablePieces
StateObsoletePlayerForced
StateObsoleteOpponentForced
StateRedoubles
)
const (
SpaceUnknown = -1
SpaceHome = -2
)
var boardTopWhite = []byte("+13-14-15-16-17-18-+---+19-20-21-22-23-24-+")
var boardBottomWhite = []byte("+12-11-10--9--8--7-+---+-6--5--4--3--2--1-+")
var boardTopBlack = []byte("+-1--2--3--4--5--6-+---+-7--8--9-10-11-12-+")
var boardBottomBlack = []byte("+24-23-22-21-20-19-+---+18-17-16-15-14-13-+")
type Board struct {
*cview.TextView
client *Client
state string
s []string
v []int
moves [][2]int
from map[int]int
to map[int]int
playerDice [2]int
opponentDice [2]int
selected [2]int
currentTurn int
premove [][2]int
premovefrom map[int]int
premoveto map[int]int
dragFromX int
dragFromY int
}
func NewBoard(client *Client) *Board {
b := &Board{
TextView: cview.NewTextView(),
client: client,
}
tv := b.TextView
tv.SetRegions(true)
tv.SetDynamicColors(true)
tv.SetToggleHighlights(true)
tv.SetHighlightedFunc(b.handleHighlight)
b.ResetMoves()
b.premovefrom = make(map[int]int)
b.premoveto = make(map[int]int)
b.SetState(initialState)
// TODO
/*
b.v[StatePlayerColor] = -1
b.v[StateBoardSpace0+11] = 12
b.v[StateBoardSpace0+9] = 7
b.v[StateBoardSpace0+13] = -13
b.v[StateBoardSpace0+24] = -3
b.v[StatePlayerBar] = 3
b.Update()
*/
return b
}
func (b *Board) resetSelection() {
b.selected[0] = 0
b.selected[1] = 0
}
func (b *Board) autoSendMoves() {
movable := 2
if b.playerDice[0] > 0 && b.playerDice[0] == b.playerDice[1] {
movable = 4
}
if b.v[StateMovablePieces] > 0 {
movable = b.v[StateMovablePieces]
}
if len(b.premove) < movable {
return
}
moveCommand := []byte("move")
for i := range b.premove {
var from string
var to string
if b.premove[i][0] == 0 || b.premove[i][0] == 25 {
from = "bar"
} else {
from = strconv.Itoa(b.premove[i][0])
}
if b.premove[i][1] == SpaceHome {
to = "off"
} else {
to = strconv.Itoa(b.premove[i][1])
}
moveCommand = append(moveCommand, []byte(" "+from+"-"+to)...)
}
b.premove = nil
b.premovefrom = make(map[int]int)
b.premoveto = make(map[int]int)
b.client.out <- moveCommand
}
func (b *Board) GetState() string {
var s = strings.Join(b.s, ":")
for i := range b.v {
s += ":" + strconv.Itoa(b.v[i])
}
return s
}
func (b *Board) SetState(state string) {
b.s = strings.Split(state, ":")
b.v = make([]int, len(b.s)-2)
var err error
for i := 0; i < 46; i++ {
b.v[i], err = strconv.Atoi(b.s[i+2])
if err != nil {
log.Fatal(err)
}
}
if b.v[StatePlayerDice1] > 0 {
b.playerDice = [2]int{b.v[StatePlayerDice1], b.v[StatePlayerDice2]}
if b.v[StateOpponentDice1] == 0 {
b.opponentDice = [2]int{0, 0}
}
}
if b.v[StateOpponentDice1] > 0 {
b.opponentDice = [2]int{b.v[StateOpponentDice1], b.v[StateOpponentDice2]}
if b.v[StatePlayerDice1] == 0 {
b.playerDice = [2]int{0, 0}
}
}
b.Update()
}
func (b *Board) renderSpace(index int, spaceValue int) []byte {
var playerColor = "X"
var opponentColor = "O"
if b.v[StatePlayerColor] == 1 {
playerColor = "O"
opponentColor = "X"
}
var pieceColor string
value := b.v[StateBoardSpace0+index]
if index == 0 {
value = b.v[StatePlayerBar]
pieceColor = playerColor
} else if index == 25 {
value = b.v[StateOpponentBar]
pieceColor = opponentColor
} else {
if value < 0 {
pieceColor = "X"
} else if value > 0 {
pieceColor = "O"
} else {
pieceColor = playerColor
}
}
abs := value
if value < 0 {
abs = value * -1
}
top := index <= 12
if b.v[StatePlayerColor] == 1 {
top = !top
}
firstDigit := 4
secondDigit := 5
if !top {
firstDigit = 5
secondDigit = 4
}
var firstNumeral string
var secondNumeral string
if abs > 5 {
if abs > 9 {
firstNumeral = "1"
} else {
firstNumeral = strconv.Itoa(abs)
}
if abs > 9 {
secondNumeral = strconv.Itoa(abs - 10)
}
if spaceValue == firstDigit && (!top || abs > 9) {
pieceColor = firstNumeral
} else if spaceValue == secondDigit && abs > 9 {
pieceColor = secondNumeral
} else if top && spaceValue == secondDigit {
pieceColor = firstNumeral
}
}
// TODO
if abs > 5 {
abs = 5
}
var r []byte
foregroundColor := "#FFFFFF"
backgroundColor := "#000000"
if index != 0 && index != 25 {
if true { // default theme
if index%2 == 0 {
backgroundColor = "#202020"
} else {
backgroundColor = "#101010"
}
} else { // rainbow
foregroundColor = "#000000"
switch index % 6 {
case 1:
backgroundColor = "#FF0000"
case 2:
backgroundColor = "#FFA500"
case 3:
backgroundColor = "#FFFF00"
case 4:
backgroundColor = "#008000"
case 5:
backgroundColor = "#0000FF"
case 0:
backgroundColor = "#4B0082"
}
}
}
if abs > 0 && spaceValue <= abs {
r = []byte(pieceColor)
} else {
r = []byte(" ")
}
if b.selected[0] == index && b.selected[1] > 0 && spaceValue <= abs && spaceValue > abs-b.selected[1] {
r = []byte("*")
} else if b.premovefrom[index] > 0 && spaceValue <= abs && spaceValue > abs-b.premovefrom[index] {
r = []byte("■")
foregroundColor = "yellow"
// TODO bold
} else if b.premoveto[index] > 0 && spaceValue > abs && spaceValue <= abs+b.premoveto[index] {
r = []byte(playerColor)
foregroundColor = "yellow"
// TODO bold
} else if b.from[index] > 0 && spaceValue > abs && spaceValue <= abs+b.from[index] {
r = []byte("■")
foregroundColor = "green"
// TODO bold
} else if b.to[index] > 0 && spaceValue > abs-(b.to[index]+b.from[index]) {
foregroundColor = "green"
// TODO bold
}
// Use entire bar as space 0
if index == 25 {
index = 0
}
return append(append([]byte(fmt.Sprintf("[\"space-%d\"][%s:%s] ", index, foregroundColor, backgroundColor)), r...), []byte(" [-:-][\"\"]")...)
}
func (b *Board) Update() {
var white bool
if b.v[StatePlayerColor] == 1 {
white = true
}
if b.playerDice[0] > 0 && b.opponentDice[0] == 0 {
b.currentTurn = b.v[StatePlayerColor]
} else if b.opponentDice[0] > 0 && b.playerDice[0] == 0 {
b.currentTurn = b.v[StatePlayerColor] * -1
}
var opponentName = b.s[1]
var playerName = b.s[0]
var playerColor = "X"
var opponentColor = "O"
if white {
playerColor = "O"
opponentColor = "X"
}
var t bytes.Buffer
t.WriteString("[\"space-off\"] [\"\"] \n")
t.WriteString("[\"space-off\"] [\"\"] \n")
t.WriteString("[\"space-off\"] ")
if white {
t.Write(boardTopWhite)
} else {
t.Write(boardTopBlack)
}
t.WriteString("[\"\"] ")
t.WriteByte('\n')
space := func(i int, j int) []byte {
spaceValue := i + 1
if i > 5 {
spaceValue = 5 - (i - 6)
}
if j == -1 {
if i <= 4 {
return b.renderSpace(25, spaceValue)
}
return b.renderSpace(0, spaceValue)
}
if i == 5 {
return []byte("[-:#000000] [-:-]") // TODO color or remove or expand middle area?
}
var index int
if !white {
if i < 6 {
j = 12 - j
} else {
j = 11 - j
}
index = 12 + j
if i > 5 {
index = 12 - j
}
} else {
index = 12 + j
if i > 5 {
index = 11 - j
}
}
if !white {
index = 24 - index
}
index++ // increment to get actual space number (0 is bar)
return b.renderSpace(index, spaceValue)
}
for i := 0; i < 11; i++ {
t.Write([]byte("[\"space-off\"] "))
if i == 5 {
t.WriteByte('v') // TODO
} else {
t.WriteByte(' ')
}
t.WriteRune(cview.BoxDrawingsLightVertical)
t.Write([]byte("[\"\"]"))
for j := 0; j < 12; j++ {
t.Write(space(i, j))
if j == 5 {
t.WriteRune(cview.BoxDrawingsLightVertical)
t.Write(space(i, -1))
t.WriteRune(cview.BoxDrawingsLightVertical)
}
}
t.Write([]byte("[\"space-off\"]" + string(cview.BoxDrawingsLightVertical) + " "))
playerRollColor := "white"
opponentRollColor := "lightgray"
if b.currentTurn != b.v[StatePlayerColor] {
playerRollColor = "lightgray"
opponentRollColor = "white"
}
if i == 0 {
t.Write([]byte("[" + opponentRollColor + "]" + opponentColor + " " + opponentName + " (" + b.s[4] + ")"))
if b.v[StateOpponentHome] > 0 {
t.Write([]byte(fmt.Sprintf(" %d off", b.v[StateOpponentHome])))
}
t.Write([]byte("[-]"))
} else if i == 2 && b.opponentDice[0] > 0 {
if b.currentTurn != b.v[StatePlayerColor] {
t.WriteByte('*')
} else {
t.WriteByte(' ')
}
t.Write([]byte(fmt.Sprintf(" [%s]%d %d[-] ", opponentRollColor, b.opponentDice[0], b.opponentDice[1])))
if b.currentTurn != b.v[StatePlayerColor] {
t.WriteByte('*')
}
} else if i == 8 && b.playerDice[0] > 0 {
if b.currentTurn == b.v[StatePlayerColor] {
t.WriteByte('*')
} else {
t.WriteByte(' ')
}
t.Write([]byte(fmt.Sprintf(" [%s]%d %d[-] ", playerRollColor, b.playerDice[0], b.playerDice[1])))
if b.currentTurn == b.v[StatePlayerColor] {
t.WriteByte('*')
}
} else if i == 10 {
t.Write([]byte("[" + playerRollColor + "]" + playerColor + " " + playerName + " (" + b.s[3] + ")"))
if b.v[StatePlayerHome] > 0 {
t.Write([]byte(fmt.Sprintf(" %d off", b.v[StatePlayerHome])))
}
t.Write([]byte("[-]"))
}
t.Write([]byte("[\"\"] "))
t.WriteByte('\n')
}
t.WriteString("[\"space-off\"] ")
if white {
t.Write(boardBottomWhite)
} else {
t.Write(boardBottomBlack)
}
t.WriteString(" [\"\"] \n")
t.WriteString("[\"space-off\"] [\"\"] \n")
t.WriteString("[\"space-off\"] [\"\"]")
b.TextView.SetBytes(t.Bytes())
}
func (b *Board) ResetMoves() {
b.moves = nil
b.from = make(map[int]int)
b.to = make(map[int]int)
}
func (b *Board) Move(player int, f string, t string) {
from, err := strconv.Atoi(f)
if err != nil {
from = SpaceUnknown
if f == "bar" {
if b.v[StatePlayerColor] == player {
from = 0
} else {
from = 25 // TODO verify
}
}
}
to, err := strconv.Atoi(t)
if err != nil {
to = SpaceUnknown
if t == "off" {
to = SpaceHome
}
}
if from == SpaceUnknown || to == SpaceUnknown {
lf("error: failed to parse move: player %d, from %s, to %s", player, f, t)
return
}
b.moves = append(b.moves, [2]int{from, to})
b.from[from]++
b.to[to]++
}
func (b *Board) SimplifyMoves() {
for i := range b.moves {
for j := range b.moves {
if b.moves[i][1] == b.moves[j][0] {
// Same to space as from space
b.moves[j][0] = b.moves[i][0] // Set from space to earlier from space
b.moves = append(b.moves[:i], b.moves[i+1:]...)
b.SimplifyMoves()
return
} else if b.moves[i][0] == b.moves[j][1] {
// Same to space as from space
b.moves[j][1] = b.moves[i][1] // Set to space to earlier to space
b.moves = append(b.moves[:i], b.moves[i+1:]...)
b.SimplifyMoves()
return
}
}
}
}
func (b *Board) addPreMove(from int, to int) {
b.premove = append(b.premove, [2]int{from, to})
b.premovefrom[from]++
b.premoveto[to]++
}
func (b *Board) handleHighlight(added, removed, remaining []string) {
defer b.Update()
if len(added) > 0 && len(remaining) > 0 {
if added[0] == "space-0" || added[0] == "space-0" { // TODO
b.TextView.SetHighlightedFunc(nil)
b.TextView.Highlight(added[0])
b.TextView.SetHighlightedFunc(b.handleHighlight)
b.resetSelection()
return
}
from, err := strconv.Atoi(remaining[0][6:])
if err != nil {
b.TextView.SetHighlightedFunc(nil)
b.TextView.Highlight(remaining[0])
b.TextView.SetHighlightedFunc(b.handleHighlight)
b.resetSelection()
return
}
to, err := strconv.Atoi(added[0][6:])
if err != nil {
if added[0] == "space-off" {
to = SpaceHome
} else {
return
}
}
var spaces int
calcFrom := from
if from == 0 && b.v[StatePlayerColor] == 1 {
calcFrom = 25
}
if to > 0 && to < 25 {
if to >= calcFrom {
spaces = to - calcFrom
} else {
spaces = calcFrom - to
}
}
var mid = SpaceUnknown
if (b.playerDice[0] != b.playerDice[1] && spaces*b.selected[1] == b.playerDice[0]+b.playerDice[1]) ||
(b.playerDice[0] == b.playerDice[1] && spaces*b.selected[1] == (b.playerDice[0]+b.playerDice[1])*2) {
// Prefer any move that will bar opponent
for i := 0; i < 2; i++ {
dice := b.playerDice[i]
index := calcFrom + (dice * b.v[StateDirection])
if index == to {
continue
}
if (b.v[StateBoardSpace0+index] == -1 && b.v[StatePlayerColor] > 0) ||
(b.v[StateBoardSpace0+index] == 1 && b.v[StatePlayerColor] < 0) {
mid = index
break
}
}
if mid == SpaceUnknown {
// Send any valid move
for i := 0; i < 2; i++ {
dice := b.playerDice[i]
index := calcFrom + (dice * b.v[StateDirection])
if index == to {
continue
}
if (b.v[StateBoardSpace0+index] >= 0 && b.v[StatePlayerColor] > 0) ||
(b.v[StateBoardSpace0+index] <= 0 && b.v[StatePlayerColor] < 0) {
mid = index
break
}
}
}
}
for i := 0; i < b.selected[1]; i++ {
if mid < 0 {
b.addPreMove(from, to)
} else {
b.addPreMove(from, mid)
b.addPreMove(mid, to)
}
}
b.TextView.SetHighlightedFunc(nil)
b.TextView.Highlight(added[0])
b.TextView.Highlight(remaining[0])
b.TextView.SetHighlightedFunc(b.handleHighlight)
b.resetSelection()
b.autoSendMoves()
} else if len(added) > 0 {
if added[0] == "space-off" {
b.TextView.SetHighlightedFunc(nil)
b.TextView.Highlight(added[0])
b.TextView.SetHighlightedFunc(b.handleHighlight)
b.resetSelection()
return
} else if (added[0] == "space-0" || added[0] == "space-25") && b.v[StatePlayerBar] == 0 {
b.TextView.SetHighlightedFunc(nil)
b.TextView.Highlight(added[0])
b.TextView.SetHighlightedFunc(b.handleHighlight)
b.resetSelection()
return
}
index, err := strconv.Atoi(added[0][6:])
if err == nil {
abs := b.v[StateBoardSpace0+index]
if abs < 0 {
abs *= -1
}
if added[0] == "space-0" || added[0] == "space-25" {
abs = b.v[StatePlayerBar]
}
if b.selected[1] >= abs && false { // TODO or has premove piece in space
b.TextView.SetHighlightedFunc(nil)
b.TextView.Highlight(added[0])
b.TextView.SetHighlightedFunc(b.handleHighlight)
return
}
b.selected[0] = index
b.selected[1]++
}
} else if len(removed) > 0 {
index, err := strconv.Atoi(removed[0][6:])
if err == nil {
abs := b.v[StateBoardSpace0+index]
if abs < 0 {
abs *= -1
}
if removed[0] == "space-0" || removed[0] == "space-25" {
abs = b.v[StatePlayerBar]
}
if b.selected[1] < abs {
b.selected[0] = index
b.selected[1]++
}
b.TextView.SetHighlightedFunc(nil)
b.TextView.Highlight(removed[0])
b.TextView.SetHighlightedFunc(b.handleHighlight)
}
}
}
// MouseHandler returns the mouse handler for this primitive.
func (b *Board) MouseHandler() func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) {
return b.WrapMouseHandler(func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) {
x, y := event.Position()
if !b.InRect(x, y) {
return false, nil
}
switch action {
case cview.MouseLeftDown:
b.dragFromX, b.dragFromY = x, y
case cview.MouseLeftUp:
if b.dragFromX != x || b.dragFromY != y {
app.QueueUpdateDraw(func() {
// Simulate click event at start of drag
fromEvent := tcell.NewEventMouse(b.dragFromX, b.dragFromY, tcell.ButtonPrimary, event.Modifiers())
b.TextView.MouseHandler()(cview.MouseLeftClick, fromEvent, setFocus)
consumed, _ = b.TextView.MouseHandler()(cview.MouseLeftClick, event, setFocus)
if consumed {
// Succeeded
return
}
// Failed, undo
b.TextView.MouseHandler()(cview.MouseLeftClick, fromEvent, setFocus)
})
}
case cview.MouseRightClick:
b.TextView.SetHighlightedFunc(nil)
h := b.GetHighlights()
for i := range h {
b.Highlight(h[i])
}
b.TextView.SetHighlightedFunc(b.handleHighlight)
b.resetSelection()
b.Update()
consumed = true
return
}
return b.TextView.MouseHandler()(action, event, setFocus)
})
}