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.

553 lines
12 KiB

package main
import (
"bytes"
"fmt"
"log"
"strconv"
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/gdamore/tcell/v2"
)
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
SpacePlayerBar = -2
SpaceOpponentBar = -3
SpaceHome = -4
)
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
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
}
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.SetState(initialState)
// TODO
/*
b.v[StatePlayerColor] = -1
b.v[StateBoardSpace0+11] = 2
b.v[StateBoardSpace0+9] = 4
b.v[StateBoardSpace0+13] = -2
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
}
lf("%d - %d", len(b.premove), movable)
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] == -1 {
to = "off"
} else {
to = strconv.Itoa(b.premove[i][1])
}
moveCommand = append(moveCommand, []byte(" "+from+"-"+to)...)
}
b.premove = nil
b.client.out <- moveCommand
}
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) 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.WriteByte('\n')
t.WriteByte(' ')
t.WriteByte(' ')
if white {
t.Write(boardTopWhite)
} else {
t.Write(boardTopBlack)
}
t.WriteByte('\n')
space := func(i int, j int) []byte {
spaceValue := i + 1
if i > 5 {
spaceValue = 5 - (i - 6)
}
if j == -1 {
r := []byte(" ")
if i <= 4 {
if b.v[StateOpponentBar] > i {
r = []byte(opponentColor)
}
} else if i > 5 { // TODO
if b.v[StatePlayerBar] >= spaceValue {
if b.selected[0] == 0 && spaceValue <= b.selected[1] {
r = []byte("*")
} else {
r = []byte(playerColor)
}
}
}
return append(append([]byte("[\"space-0\"] "), r...), []byte(" [\"\"]")...)
}
if i == 5 {
return []byte(" ")
}
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)
var r []byte
value := b.v[StateBoardSpace0+index]
if value < 0 && value <= spaceValue*-1 {
r = []byte("X")
} else if value > 0 && value >= spaceValue {
r = []byte("O")
} else {
r = []byte("[lightgray]·[-]")
}
abs := value
if value < 0 {
abs = value * -1
}
_ = abs
if b.selected[0] == index && b.selected[1] > 0 && spaceValue <= abs && spaceValue > abs-b.selected[1] {
r = []byte("*")
} else if b.from[index] > 0 && spaceValue > abs && spaceValue <= abs+b.from[index] {
r = []byte("[yellow::b]■[-:-:-]")
} else if b.to[index] > 0 && spaceValue > abs-b.to[index] {
r = []byte("[yellow::b]" + string(r) + "[-:-:-]")
}
return append(append([]byte(fmt.Sprintf("[\"space-%d\"] ", index)), r...), []byte(" [\"\"]")...)
}
for i := 0; i < 11; i++ {
t.WriteByte(' ')
if i == 5 {
t.WriteByte('v') // TODO
} else {
t.WriteByte(' ')
}
t.WriteRune(cview.BoxDrawingsLightVertical)
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.WriteByte('\n')
}
t.WriteByte(' ')
t.WriteByte(' ')
if white {
t.Write(boardBottomWhite)
} else {
t.Write(boardBottomBlack)
}
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("invalid move: %d %s %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) handleHighlight(added, removed, remaining []string) {
defer b.Update()
if len(added) > 0 && len(remaining) > 0 {
if added[0] == "space-0" {
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
}
}
for i := 0; i < b.selected[1]; i++ {
b.premove = append(b.premove, [2]int{from, 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" && b.v[StatePlayerBar] == 0 {
lf("%+v bar", b.v[StatePlayerBar])
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" {
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" {
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.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)
})
}