FIBS (online backgammon) client library
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.

855 lines
19 KiB

2 months ago
package fibs
import (
"bytes"
"fmt"
"log"
"strconv"
"strings"
"sync"
)
const (
BoxDrawingsLightVertical = '|'
)
const (
StateLength = iota
StatePlayerScore
StateOpponentScore
StateBoardSpace0
)
const (
StatePlayerName = iota
StateOpponentName
)
2 months ago
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
)
const initialState = "FIBS:Welcome:5:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:-1:0:0:0:0:1:1:1:0:1:-1:0:25:0:0:0:0:4:0:0:0"
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 {
client *Client
state string
s []string
v []int
moves [][2]int
movesColor int
validMoves map[int][][]int
2 months ago
from map[int]int
to map[int]int
selectedNum int
selectedSpace int
2 months ago
premove [][2]int
Premovefrom map[int]int
Premoveto map[int]int
2 months ago
dragFromX int
dragFromY int
sync.Mutex
}
func NewBoard(client *Client) *Board {
b := &Board{
client: client,
2 months ago
s: make([]string, 52),
v: make([]int, 50),
2 months ago
}
b.ResetMoves()
b.ResetPreMoves()
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
}
// TODO refactor
func (b *Board) GetStringState() []string {
2 months ago
b.Lock()
defer b.Unlock()
return b.s
}
2 months ago
func (b *Board) GetIntState() []int {
2 months ago
b.Lock()
defer b.Unlock()
2 months ago
return b.v
}
func (b *Board) resetSelection() {
b.selectedSpace = 0
b.selectedNum = 0
2 months ago
}
func (b *Board) autoSendMoves() {
movable := 2
if b.v[StatePlayerDice1] > 0 && b.v[StatePlayerDice1] == b.v[StatePlayerDice2] {
2 months ago
movable = 4
}
if b.v[StateMovablePieces] > 0 {
movable = b.v[StateMovablePieces]
}
if len(b.premove) < movable {
return
}
moveCommand := []byte("move")
for j := 0; j < 2; j++ {
for i := range b.premove {
var from string
if b.premove[i][0] == 0 || b.premove[i][0] == 25 {
from = "bar"
} else {
from = strconv.Itoa(b.premove[i][0])
}
if (j == 0) != (from == "bar") {
continue // Always send bar moves first
}
var to string
2 months ago
if b.premove[i][1] == b.PlayerHomeSpace() {
2 months ago
to = "off"
} else {
to = strconv.Itoa(b.premove[i][1])
}
moveCommand = append(moveCommand, []byte(" "+from+"-"+to)...)
}
}
b.ResetPreMoves()
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.Lock()
/*lastTurn := 0
2 months ago
if len(b.v) > 0 {
lastTurn = b.v[StateTurn]
}*/
2 months ago
2 months ago
s := strings.Split(state, ":")
newPlayers := s[StatePlayerName] != b.s[StatePlayerName] || s[StateOpponentName] != b.s[StateOpponentName]
2 months ago
copy(b.s, s)
2 months ago
2 months ago
v := make([]int, 50)
var err error
2 months ago
for i := 0; i < 50; i++ {
v[i], err = strconv.Atoi(b.s[i+2])
2 months ago
if err != nil {
log.Fatal(err)
}
}
newTurn := v[StateTurn] != b.v[StateTurn]
// Retain dice rolls
if !newPlayers && !newTurn {
copyDice := []int{
StatePlayerDice1,
StatePlayerDice2,
StateOpponentDice1,
StateOpponentDice2,
}
for _, vi := range copyDice {
if v[vi] == 0 {
v[vi] = b.v[vi]
}
}
}
2 months ago
copy(b.v, v)
2 months ago
/*if b.v[StateTurn] != lastTurn {
2 months ago
if lastTurn == b.v[StatePlayerColor] {
b.playerDice = [2]int{0, 0}
} else {
b.opponentDice = [2]int{0, 0}
}
}*/ // TODO disabled
2 months ago
// TODO only overwrite dice when player names change
/*if b.v[StatePlayerDice1] > 0 {
2 months ago
b.playerDice = [2]int{b.v[StatePlayerDice1], b.v[StatePlayerDice2]}
}
if b.v[StateOpponentDice1] > 0 {
b.opponentDice = [2]int{b.v[StateOpponentDice1], b.v[StateOpponentDice2]}
}*/
2 months ago
b.Unlock()
b.Draw()
}
func (b *Board) Draw() {
2 months ago
b.client.Event <- &EventDraw{}
2 months ago
}
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]
2 months ago
if index == b.PlayerBarSpace() {
2 months ago
value = b.v[StatePlayerBar]
pieceColor = playerColor
2 months ago
} else if index == 25-b.PlayerBarSpace() {
2 months ago
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 = "#303030"
} 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"
}
}
}
// Highlight legal moves
highlightSpace := b.ValidMove(b.selectedSpace, index)
2 months ago
highlightSpace = false // TODO Make configurable, disable by default
//+(b.v[StatePlayerDice1]*b.v[StatePlayerColor]) ||b.selectedSpace == index+(b.v[StatePlayerDice2]*b.v[StatePlayerColor])) && b.selectedNum > 0
if b.selectedNum > 0 && highlightSpace && index != 25 && index != 0 {
2 months ago
foregroundColor = "black"
backgroundColor = "yellow"
}
if abs > 0 && spaceValue <= abs {
r = []byte(pieceColor)
} else {
r = []byte(" ")
}
rightArrowFrom := (b.v[StateDirection] == b.movesColor) == (index > 12)
if b.selectedSpace == index && b.selectedNum > 0 && spaceValue <= abs && spaceValue > abs-b.selectedNum {
2 months ago
r = []byte("*")
} else if b.Premovefrom[index] > 0 && spaceValue > (abs+b.Premoveto[index])-b.Premovefrom[index] && spaceValue <= abs+b.Premoveto[index] {
2 months ago
if index == 25-b.PlayerBarSpace() {
2 months ago
r = []byte("▾")
2 months ago
} else if index == b.PlayerBarSpace() {
2 months ago
r = []byte("▴")
} else if rightArrowFrom {
r = []byte("▸")
} else {
r = []byte("◂")
}
foregroundColor = "yellow"
} else if b.Premoveto[index] > 0 && spaceValue > abs && spaceValue <= abs+(b.Premoveto[index]) {
2 months ago
r = []byte(playerColor)
foregroundColor = "yellow"
} else if b.from[index] > 0 && spaceValue > abs && spaceValue <= abs+b.from[index] {
if rightArrowFrom {
r = []byte("▸")
} else {
r = []byte("◂")
}
if b.movesColor == b.v[StatePlayerColor] {
foregroundColor = "yellow"
} else {
foregroundColor = "green"
}
} else if b.to[index] > 0 && spaceValue > abs-(b.to[index]+b.from[index]) {
if b.movesColor == b.v[StatePlayerColor] {
foregroundColor = "yellow"
} else {
foregroundColor = "green"
}
}
return append(append([]byte(fmt.Sprintf("[\"space-%d\"][%s:%s:b] ", index, foregroundColor, backgroundColor)), r...), []byte(" [-:-:-][\"\"]")...)
}
func (b *Board) ResetMoves() {
b.moves = nil
b.movesColor = 0
b.validMoves = make(map[int][][]int)
2 months ago
b.from = make(map[int]int)
b.to = make(map[int]int)
}
func (b *Board) ResetPreMoves() {
b.premove = nil
b.Premovefrom = make(map[int]int)
b.Premoveto = make(map[int]int)
2 months ago
}
func (b *Board) homeBoardSpaces() (int, int) {
2 months ago
homeBoardStart := 1
homeBoardEnd := 6
if (b.v[StateDirection] == -1) == (b.v[StatePlayerColor] == -1) {
2 months ago
homeBoardStart = 19
homeBoardEnd = 24
}
return homeBoardStart, homeBoardEnd
}
func (b *Board) allPlayerPiecesInHomeBoard() bool {
homeBoardStart, homeBoardEnd := b.homeBoardSpaces()
2 months ago
hasPlayerPiece := func(index int) bool {
if index < 0 || index > 25 {
return false
}
value := b.v[StateBoardSpace0+index]
// Include pre-moves
mod := b.v[StatePlayerColor]
value -= b.client.Board.Premovefrom[index] * mod
if b.v[StatePlayerColor] == -1 {
return value < 0
}
return value > 0
2 months ago
}
for i := 1; i < 24; i++ {
if i >= homeBoardStart && i <= homeBoardEnd {
continue
}
if hasPlayerPiece(i) {
return false
}
}
return true
}
func (b *Board) spaceAvailable(index int) bool {
if index < 0 || index > 25 {
return false
}
if index == 0 || index == 25 {
return b.allPlayerPiecesInHomeBoard()
}
return (b.v[StatePlayerColor] == 1 && b.v[StateBoardSpace0+index] >= -1) ||
(b.v[StatePlayerColor] == -1 && b.v[StateBoardSpace0+index] <= 1)
}
func (b *Board) GetValidMoves(from int) [][]int {
2 months ago
if validMoves, ok := b.validMoves[from]; ok {
return validMoves
}
var validMoves [][]int
2 months ago
defer func() {
b.validMoves[from] = validMoves
}()
if b.v[StateTurn] != b.v[StatePlayerColor] || b.v[StatePlayerDice1] == 0 || b.v[StatePlayerDice2] == 0 {
2 months ago
return validMoves
}
trySpaces := [][]int{
{b.v[StatePlayerDice1]},
{b.v[StatePlayerDice2]},
{b.v[StatePlayerDice1], b.v[StatePlayerDice2]},
{b.v[StatePlayerDice2], b.v[StatePlayerDice1]},
2 months ago
}
if b.v[StatePlayerDice1] == b.v[StatePlayerDice2] {
2 months ago
trySpaces = append(trySpaces,
[]int{b.v[StatePlayerDice1], b.v[StatePlayerDice1], b.v[StatePlayerDice1]},
[]int{b.v[StatePlayerDice1], b.v[StatePlayerDice1], b.v[StatePlayerDice1], b.v[StatePlayerDice1]})
2 months ago
}
if b.allPlayerPiecesInHomeBoard() {
2 months ago
homeSpace := b.PlayerHomeSpace()
2 months ago
spacesHome := from - homeSpace
if spacesHome < 0 {
spacesHome *= -1
}
if spacesHome <= b.v[StatePlayerDice1] || spacesHome <= b.v[StatePlayerDice2] {
2 months ago
trySpaces = append(trySpaces, []int{spacesHome})
}
}
foundMoves := make(map[int]bool)
CHECKSPACES:
for i := range trySpaces {
checkSpace := 0
for _, space := range trySpaces[i] {
checkSpace += space
if !b.spaceAvailable(from + (checkSpace * b.v[StateDirection])) {
continue CHECKSPACES
}
}
space := from + (checkSpace * b.v[StateDirection])
if _, value := foundMoves[space]; !value {
foundMoves[space] = true
validMoves = append(validMoves, trySpaces[i])
2 months ago
}
}
return validMoves
}
2 months ago
func (b *Board) PlayerBarSpace() int {
return 25 - b.PlayerHomeSpace()
2 months ago
}
2 months ago
func (b *Board) PlayerHomeSpace() int {
2 months ago
if b.v[StateDirection] == -1 {
return 0
}
return 25
}
2 months ago
func (b *Board) ValidMove(f int, t int) bool {
if b.v[StateTurn] != b.v[StatePlayerColor] || b.v[StatePlayerDice1] == 0 || b.v[StatePlayerDice2] == 0 {
2 months ago
return false
}
2 months ago
if t == b.PlayerHomeSpace() {
2 months ago
// TODO bear off logic, only allow high roll
return b.allPlayerPiecesInHomeBoard()
}
validMoves := b.GetValidMoves(f)
CHECKVALID:
2 months ago
for i := range validMoves {
checkSpace := 0
for _, space := range validMoves[i] {
checkSpace += space
if !b.spaceAvailable(f + (checkSpace * b.v[StateDirection])) {
continue CHECKVALID
}
}
if f+(checkSpace*b.v[StateDirection]) == t {
2 months ago
return true
}
}
return false
}
func (b *Board) parseMoveString(player int, s string) int {
space, err := strconv.Atoi(s)
if err != nil {
space = SpaceUnknown
if s == "bar" {
2 months ago
barSpace := b.PlayerBarSpace()
2 months ago
if b.v[StatePlayerColor] == player {
space = barSpace
} else {
space = 25 - barSpace
}
} else if s == "off" {
2 months ago
space = b.PlayerHomeSpace()
2 months ago
}
}
return space
}
func (b *Board) Move(player int, f string, t string) {
from := b.parseMoveString(player, f)
to := b.parseMoveString(player, t)
if from == SpaceUnknown || to == SpaceUnknown {
// TODO debug print ("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.movesColor = player
b.from[from]++
b.to[to]++
b.v[StateTurn] = player * -1
b.validMoves = make(map[int][][]int)
2 months ago
b.ResetPreMoves()
}
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) GetSelection() (num int, space int) {
return b.selectedNum, b.selectedSpace
}
func (b *Board) SetSelection(num int, space int) {
b.selectedNum, b.selectedSpace = num, space
}
func (b *Board) ResetSelection() {
b.selectedNum, b.selectedSpace = 0, 0
}
func (b *Board) addPreMove(from int, to int, num int) bool {
// Allow bearing off when the player moves their own pieces on to the bar
if to == 0 || to == 25 {
2 months ago
to = b.PlayerHomeSpace()
}
// Expand combined move
moves := b.client.Board.GetValidMoves(from)
CHECKPREMOVES:
for i := range moves {
checkSpace := 0
for _, space := range moves[i] {
checkSpace += space
lf("CHECK %d %d", checkSpace, from+(checkSpace*b.v[StateDirection]))
if !b.spaceAvailable(from + (checkSpace * b.v[StateDirection])) {
continue CHECKPREMOVES
}
}
lf("SECOND PHASE %+v %d", moves[i], num)
if (from+(checkSpace*b.v[StateDirection]) == to) && len(moves[i]) > 1 {
lf("SECOND.5 PHASE %+v %d", moves[i], num)
for j := 0; j < num; j++ {
checkSpace = 0
lastSpace := 0
for _, space := range moves[i] {
checkSpace += space
lf("THIRD PHASE %d %d", from+(lastSpace*b.v[StateDirection]), from+(checkSpace*b.v[StateDirection]))
if !b.addPreMove(from+(lastSpace*b.v[StateDirection]), from+(checkSpace*b.v[StateDirection]), 1) {
return false
}
lastSpace = checkSpace
}
}
return true
}
}
2 months ago
if !b.ValidMove(from, to) {
2 months ago
return false
}
for i := 0; i < num; i++ {
b.premove = append(b.premove, [2]int{from, to})
b.Premovefrom[from]++
b.Premoveto[to]++
}
lf("ADD %+v", b.premove)
return true
}
func (b *Board) AddPreMove(from int, to int) bool {
if !b.addPreMove(from, to, b.selectedNum) {
return false
}
lf("FINAL %+v", b.premove)
b.resetSelection()
b.autoSendMoves()
2 months ago
return true
}
1 month ago
func (b *Board) GetPreMoves() [][2]int {
return b.premove
}
2 months ago
func (b *Board) Render() []byte {
b.Lock()
var white bool
if b.v[StatePlayerColor] == 1 {
white = true
}
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 {
2 months ago
return b.renderSpace(25-b.PlayerBarSpace(), spaceValue)
2 months ago
}
2 months ago
return b.renderSpace(b.PlayerBarSpace(), spaceValue)
2 months ago
}
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)
if i == 5 {
return []byte("[-:#000000] [-:-]")
}
return b.renderSpace(index, spaceValue)
}
for i := 0; i < 11; i++ {
t.Write([]byte("[\"space-off\"]"))
if i == 5 && b.v[StateDoublingValue] > 1 {
t.WriteString(fmt.Sprintf("%2d ", b.v[StateDoublingValue]))
if b.v[StatePlayerMayDouble] == 1 {
t.WriteByte('v')
} else {
t.WriteByte('^')
}
} else {
t.WriteByte(' ')
t.WriteByte(' ')
t.WriteByte(' ')
t.WriteByte(' ')
}
t.WriteRune(BoxDrawingsLightVertical)
t.Write([]byte("[\"\"]"))
for j := 0; j < 12; j++ {
t.Write(space(i, j))
if j == 5 {
t.WriteRune(BoxDrawingsLightVertical)
t.Write(space(i, -1))
t.WriteRune(BoxDrawingsLightVertical)
}
}
t.Write([]byte("[\"space-off\"]" + string(BoxDrawingsLightVertical) + " "))
playerRollColor := "yellow"
playerBold := "b"
opponentRollColor := "white"
opponentBold := ""
if b.v[StateTurn] != b.v[StatePlayerColor] {
playerRollColor = "white"
opponentRollColor = "green"
playerBold = ""
opponentBold = "b"
}
if i == 0 {
t.Write([]byte("[" + opponentRollColor + "::" + opponentBold + "]" + 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 {
if b.v[StateOpponentDice1] > 0 {
t.Write([]byte(fmt.Sprintf(" [%s::%s]%d %d[-::-] ", opponentRollColor, opponentBold, b.v[StateOpponentDice1], b.v[StateOpponentDice2])))
2 months ago
} else {
t.Write([]byte(fmt.Sprintf(" [%s]- -[-] ", opponentRollColor)))
}
} else if i == 8 {
if b.v[StatePlayerDice1] > 0 {
t.Write([]byte(fmt.Sprintf(" [%s::%s]%d %d[-::-] ", playerRollColor, playerBold, b.v[StatePlayerDice1], b.v[StatePlayerDice2])))
2 months ago
} else {
t.Write([]byte(fmt.Sprintf(" [%s]- -[-] ", playerRollColor)))
}
} else if i == 10 {
t.Write([]byte("[" + playerRollColor + "::" + playerBold + "]" + 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\"] [\"\"]")
b.Unlock()
return t.Bytes()
}