Initial commit

This commit is contained in:
Trevor Slocum 2021-08-30 18:26:34 -07:00
commit ad9ece3682
10 changed files with 1561 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# fibs
[![GoDoc](https://code.rocketnine.space/tslocum/godoc-static/raw/branch/master/badge.svg)](https://docs.rocketnine.space/code.rocketnine.space/tslocum/fibs)
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space)
[FIBS](https://fibs.com) (online backgammon) client library
**Currently in pre-alpha state. Here be dragons.**
## Documentation
Documentation is available via [godoc](https://docs.rocketnine.space/code.rocketnine.space/tslocum/fibs).
## Support
Please share issues and suggestions [here](https://code.rocketnine.space/tslocum/fibs/issues).

758
board.go Normal file
View File

@ -0,0 +1,758 @@
package fibs
import (
"bytes"
"fmt"
"log"
"sort"
"strconv"
"strings"
"sync"
)
// TODO Add PlayerName, etc
const (
BoxDrawingsLightVertical = '|'
)
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
)
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
from map[int]int
to map[int]int
playerDice [2]int
opponentDice [2]int
selected [2]int
premove [][2]int
premovefrom map[int]int
premoveto map[int]int
dragFromX int
dragFromY int
sync.Mutex
}
func NewBoard(client *Client) *Board {
b := &Board{
client: client,
}
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) GetIntState() []int {
return b.v
}
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) == 1 {
abs := b.premove[0][1] - b.premove[0][0]
direction := 1
if abs < 0 {
abs *= -1
direction = -1
}
if b.playerDice[0] == b.playerDice[1] {
for expandDoubles := 4; expandDoubles >= 2; expandDoubles-- {
if abs != b.playerDice[0]*expandDoubles {
continue
}
from, _ := b.premove[0][0], b.premove[0][1]
b.premove = nil
for i := 1; i <= expandDoubles; i++ {
b.premove = append(b.premove, [2]int{from + ((b.playerDice[0]*i - 1) * direction), from + ((b.playerDice[0] * i) * direction)})
}
break
}
}
}
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
if b.premove[i][1] == b.playerHomeSpace() {
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
if len(b.v) > 0 {
lastTurn = b.v[StateTurn]
}
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[StateTurn] != lastTurn {
if lastTurn == b.v[StatePlayerColor] {
b.playerDice = [2]int{0, 0}
} else {
b.opponentDice = [2]int{0, 0}
}
}
if b.v[StatePlayerDice1] > 0 {
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]}
}
b.Unlock()
b.Draw()
}
func (b *Board) Draw() {
// TODO
}
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 == b.playerBarSpace() {
value = b.v[StatePlayerBar]
pieceColor = playerColor
} else if index == 25-b.playerBarSpace() {
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.selected[0], index)
highlightSpace = false // TODO Make configurable, disable by default
//+(b.playerDice[0]*b.v[StatePlayerColor]) ||b.selected[0] == index+(b.playerDice[1]*b.v[StatePlayerColor])) && b.selected[1] > 0
if b.selected[1] > 0 && highlightSpace && index != 25 && index != 0 {
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.selected[0] == index && b.selected[1] > 0 && spaceValue <= abs && spaceValue > abs-b.selected[1] {
r = []byte("*")
} else if b.premovefrom[index] > 0 && spaceValue > (abs+b.premoveto[index])-b.premovefrom[index] && spaceValue <= abs+b.premoveto[index] {
if index == 25-b.playerBarSpace() {
r = []byte("▾")
} else if index == b.playerBarSpace() {
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]) {
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)
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)
}
func (b *Board) allPlayerPiecesInHomeBoard() bool {
homeBoardStart := 1
homeBoardEnd := 6
if b.v[StateDirection] == -1 {
homeBoardStart = 19
homeBoardEnd = 24
}
hasPlayerPiece := func(index int) bool {
if index < 0 || index > 25 {
return false
}
return (b.v[StatePlayerColor] == 1 && b.v[StateBoardSpace0+index] > 0) ||
(b.v[StatePlayerColor] == -1 && b.v[StateBoardSpace0+index] < 0)
}
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 {
if validMoves, ok := b.validMoves[from]; ok {
return validMoves
}
var validMoves []int
defer func() {
b.validMoves[from] = validMoves
}()
if b.v[StateTurn] != b.v[StatePlayerColor] || b.playerDice[0] == 0 || b.playerDice[1] == 0 {
return validMoves
}
// TODO consider opponent blocking midway In full move
trySpaces := [][]int{
{b.playerDice[0]},
{b.playerDice[1]},
{b.playerDice[0], b.playerDice[1]},
{b.playerDice[1], b.playerDice[0]},
}
if b.playerDice[0] == b.playerDice[1] {
trySpaces = append(trySpaces,
[]int{b.playerDice[0], b.playerDice[0], b.playerDice[0]},
[]int{b.playerDice[0], b.playerDice[0], b.playerDice[0], b.playerDice[0]})
}
if b.allPlayerPiecesInHomeBoard() {
homeSpace := b.playerHomeSpace()
spacesHome := from - homeSpace
if spacesHome < 0 {
spacesHome *= -1
}
if spacesHome <= b.playerDice[0] || spacesHome <= b.playerDice[1] {
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, space)
}
}
sort.Ints(validMoves)
return validMoves
}
func (b *Board) playerBarSpace() int {
return 25 - b.playerHomeSpace()
}
func (b *Board) playerHomeSpace() int {
if b.v[StateDirection] == -1 {
return 0
}
return 25
}
func (b *Board) validMove(f int, t int) bool {
if b.v[StateTurn] != b.v[StatePlayerColor] || b.playerDice[0] == 0 || b.playerDice[1] == 0 {
return false
}
if t == b.playerHomeSpace() {
// TODO bear off logic, only allow high roll
return b.allPlayerPiecesInHomeBoard()
}
validMoves := b.GetValidMoves(f)
for i := range validMoves {
if validMoves[i] == t {
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" {
barSpace := b.playerBarSpace()
if b.v[StatePlayerColor] == player {
space = barSpace
} else {
space = 25 - barSpace
}
} else if s == "off" {
space = b.playerHomeSpace()
}
}
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)
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) addPreMove(from int, to int) bool {
if !b.validMove(from, to) {
return false
}
b.premove = append(b.premove, [2]int{from, to})
b.premovefrom[from]++
b.premoveto[to]++
return true
}
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 {
return b.renderSpace(25-b.playerBarSpace(), spaceValue)
}
return b.renderSpace(b.playerBarSpace(), spaceValue)
}
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.opponentDice[0] > 0 {
t.Write([]byte(fmt.Sprintf(" [%s::%s]%d %d[-::-] ", opponentRollColor, opponentBold, b.opponentDice[0], b.opponentDice[1])))
} else {
t.Write([]byte(fmt.Sprintf(" [%s]- -[-] ", opponentRollColor)))
}
} else if i == 8 {
if b.playerDice[0] > 0 {
t.Write([]byte(fmt.Sprintf(" [%s::%s]%d %d[-::-] ", playerRollColor, playerBold, b.playerDice[0], b.playerDice[1])))
} 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()
}

62
board_test.go Normal file
View File

@ -0,0 +1,62 @@
package fibs
import (
"fmt"
"testing"
)
func TestBoard_GetValidMoves(t *testing.T) {
client := NewClient("", "", "")
b := NewBoard(client)
b.v[StatePlayerColor] = 1
b.v[StateTurn] = 1
b.v[StateDirection] = -1
b.v[StateBoardSpace0+11] = 3
b.v[StateBoardSpace0+9] = 7
b.v[StateBoardSpace0+13] = -4
b.v[StateBoardSpace0+24] = -3
b.v[StatePlayerBar] = 1
testCases := []struct {
roll [2]int
from int
moves []int
}{
{
roll: [2]int{1, 5},
from: 25,
moves: []int{19, 20},
},
{
roll: [2]int{1, 5},
from: 1,
moves: []int{},
},
}
for _, c := range testCases {
c := c
t.Run(fmt.Sprintf("With-%d-%d-From-%s", c.roll[0], c.roll[1], fmt.Sprintf("%02d", c.from)), func(t *testing.T) {
b.playerDice = c.roll
b.Draw()
validMoves := b.GetValidMoves(c.from)
if !equalInts(validMoves, c.moves) {
t.Errorf("unexpected valid moves: expected %+v, got %+v\n%s", c.moves, validMoves, b.Render())
}
})
}
}
func equalInts(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

643
client.go Normal file
View File

@ -0,0 +1,643 @@
package fibs
import (
"bytes"
"fmt"
"log"
"math/rand"
"regexp"
"strconv"
"strings"
"time"
"github.com/reiver/go-oi"
"github.com/reiver/go-telnet"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
const debug = 1 // TODO
const whoInfoSize = 12
const (
whoInfoDataName = iota
whoInfoDataOpponent
whoInfoDataWatching
whoInfoDataReady
whoInfoDataAway
whoInfoDataRating
whoInfoDataExperience
whoInfoDataIdleTime
whoInfoDataLoginTime
whoInfoDataHostname
whoInfoDataClientName
whoInfoDataEmail
)
var (
TypeWelcome = []byte("1")
TypeOwnInfo = []byte("2")
TypeMOTD = []byte("3")
TypeEndMOTD = []byte("4")
TypeWhoInfo = []byte("5")
TypeEndWhoInfo = []byte("6")
TypeLogin = []byte("7")
TypeLogout = []byte("8")
TypeMsg = []byte("9")
TypeMsgDelivered = []byte("10")
TypeMsgSaved = []byte("11")
TypeSay = []byte("12")
TypeShout = []byte("13")
TypeWhisper = []byte("14")
TypeKibitz = []byte("15")
TypeYouSay = []byte("16")
TypeYouShout = []byte("17")
TypeYouWhisper = []byte("18")
TypeYouKibitz = []byte("19")
TypeBoardState = []byte("board:")
)
var numberPrinter = message.NewPrinter(language.English)
type WhoInfo struct {
Username string
Opponent string
Watching string
Ready bool
Away bool
Rating int
Experience int
Idle int
LoginTime int
ClientName string
}
func (w *WhoInfo) String() string {
opponent := "In the lobby"
if w.Opponent != "" && w.Opponent != "-" {
opponent = "playing against " + w.Opponent
}
clientName := ""
if w.ClientName != "" && w.ClientName != "-" {
clientName = " using " + w.ClientName
}
return fmt.Sprintf("%s (rated %d with %d exp) is %s%s", w.Username, w.Rating, w.Experience, opponent, clientName)
}
type Client struct {
In chan []byte
Out chan []byte
Event chan interface{}
Username string
Password string
loggedin bool
motd []byte
rawMode bool
who map[string]*WhoInfo
notified map[string]bool
serverAddress string
Board *Board
}
func NewClient(serverAddress string, username string, password string) *Client {
c := &Client{
In: make(chan []byte, 100),
Out: make(chan []byte, 100),
Event: make(chan interface{}, 100),
serverAddress: serverAddress,
Username: username,
Password: password,
who: make(map[string]*WhoInfo),
notified: make(map[string]bool),
}
c.Board = NewBoard(c)
go c.eventLoop()
return c
}
// CallTELNET is called when a connection is made with the server.
func (c *Client) CallTELNET(ctx telnet.Context, w telnet.Writer, r telnet.Reader) {
go func() {
var b = &bytes.Buffer{}
var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up.
p := buffer[:]
var motd bool // Parse all messages as MOTD text until MsgEndMOTD
for {
// Read 1 byte.
n, err := r.Read(p)
if n <= 0 && nil == err {
continue
} else if n <= 0 && nil != err {
if err.Error() != "EOF" {
lf("** Disconnected: %s", err)
} else {
l("** Disconnected")
}
return
}
b.WriteByte(p[0])
if p[0] == '\n' {
buf := make([]byte, b.Len())
copy(buf, b.Bytes())
if debug > 0 {
l("<- " + string(bytes.TrimSpace(buf)))
}
if c.loggedin {
if !motd {
buf = bytes.TrimSpace(buf)
if len(buf) == 0 {
b.Reset()
continue
}
}
if c.rawMode {
c.In <- append([]byte("** "), buf...)
b.Reset()
continue
}
if bytes.HasPrefix(b.Bytes(), TypeMOTD) && !motd {
motd = true
c.motd = append(c.motd, buf[1:]...)
b.Reset()
continue
} else if bytes.HasPrefix(b.Bytes(), TypeEndMOTD) && motd {
motd = false
c.motd = bytes.TrimSpace(c.motd)
c.In <- append([]byte("3 "), c.motd...)
b.Reset()
continue
} else if motd {
c.motd = append(c.motd, buf...)
b.Reset()
continue
}
c.In <- buf
}
b.Reset()
}
if !c.loggedin {
bt := strings.TrimSpace(b.String())
if bt == "login:" {
b.Reset()
c.Out <- []byte(fmt.Sprintf("login bgammon 1008 %s %s", c.Username, c.Password))
c.loggedin = true
}
}
}
}()
var buffer bytes.Buffer
var p []byte
var crlfBuffer [2]byte = [2]byte{'\r', '\n'}
crlf := crlfBuffer[:]
help := []byte("help")
who := []byte("who")
quit := []byte("quit")
bye := []byte("bye")
watch := []byte("watch")
about := []byte("about")
show := []byte("show")
average := []byte("average")
dicetest := []byte("dicetest")
boardstate := []byte("boardstate")
stat := []byte("stat")
for b := range c.Out {
if bytes.Equal(bytes.ToLower(b), watch) {
c.WatchRandomGame()
continue
} else if bytes.Equal(bytes.ToLower(b), who) {
for username := range c.who {
lf("%s", c.who[username])
}
continue
} else if bytes.HasPrefix(bytes.ToLower(b), help) || bytes.HasPrefix(bytes.ToLower(b), about) || bytes.HasPrefix(bytes.ToLower(b), average) ||
bytes.HasPrefix(bytes.ToLower(b), dicetest) || bytes.HasPrefix(bytes.ToLower(b), show) || bytes.HasPrefix(bytes.ToLower(b), stat) {
c.rawMode = true
go func() {
time.Sleep(time.Second)
c.rawMode = false
}()
} else if bytes.Equal(bytes.ToLower(b), boardstate) {
lf("Board state: %s", c.Board.GetState())
lf("Player color: %d", c.Board.v[StatePlayerColor])
lf("Direction: %d", c.Board.v[StateDirection])
lf("Current turn: %d", c.Board.v[StateTurn])
lf("Moves: %+v", c.Board.moves)
lf("Pre-moves: %+v", c.Board.premove)
continue
} else if bytes.Equal(bytes.ToLower(b), quit) || bytes.Equal(bytes.ToLower(b), bye) {
// TODO match command, not exact string
c.rawMode = true
}
if debug > 0 {
l("-> " + string(bytes.TrimSpace(b)))
}
buffer.Write(b)
buffer.Write(crlf)
p = buffer.Bytes()
n, err := oi.LongWrite(w, p)
if nil != err {
break
}
if expected, actual := int64(len(p)), n; expected != actual {
log.Fatalf("Transmission problem: tried sending %d bytes, but actually only sent %d bytes.", expected, actual)
}
buffer.Reset()
}
}
func (c *Client) keepAlive() {
t := time.NewTicker(5 * time.Minute)
for range t.C {
c.Out <- []byte("set boardstyle 3")
}
}
func (c *Client) updateWhoInfo(b []byte) {
s := bytes.Split(b, []byte(" "))
if len(s) != whoInfoSize {
return
}
info := &WhoInfo{
Username: string(s[whoInfoDataName]),
}
r := s[whoInfoDataRating]
if bytes.ContainsRune(r, '.') {
r = s[whoInfoDataRating][:bytes.IndexByte(s[whoInfoDataRating], '.')]
}
rating, err := strconv.Atoi(string(r))
if err != nil {
rating = 0
}
info.Rating = rating
experience, err := strconv.Atoi(string(s[whoInfoDataExperience]))
if err != nil {
experience = 0
}
info.Experience = experience
opponent := ""
if len(s[whoInfoDataOpponent]) > 1 || s[whoInfoDataOpponent][0] != '-' {
opponent = string(s[whoInfoDataOpponent])
}
info.Opponent = opponent
watching := ""
if len(s[whoInfoDataWatching]) > 1 || s[whoInfoDataWatching][0] != '-' {
watching = string(s[whoInfoDataWatching])
}
info.Watching = watching
ready := false
if string(s[whoInfoDataReady]) == "1" {
ready = true
}
info.Ready = ready
clientName := ""
if len(s[whoInfoDataClientName]) > 1 || s[whoInfoDataClientName][0] != '-' {
clientName = string(s[whoInfoDataClientName])
}
info.ClientName = clientName
status := "Unavailable"
if info.Opponent != "" {
status = "vs. " + info.Opponent
} else if info.Ready {
status = "Available"
}
itemText := info.Username + strings.Repeat(" ", 18-len(info.Username))
ratingString := numberPrinter.Sprintf("%d", info.Rating)
itemText += ratingString + strings.Repeat(" ", 8-len(ratingString))
experienceString := numberPrinter.Sprintf("%d", info.Experience)
itemText += experienceString + strings.Repeat(" ", 12-len(experienceString))
itemText += status
c.who[string(s[whoInfoDataName])] = info
// TODO who info event
}
func (c *Client) GetAllWhoInfo() []*WhoInfo {
w := make([]*WhoInfo, len(c.who))
var i int
for _, whoInfo := range c.who {
w[i] = whoInfo
i++
}
return w
}
func (c *Client) LoggedIn() bool {
// TODO lock
return c.loggedin
}
func (c *Client) WatchRandomGame() {
var options []string
for username, whoInfo := range c.who {
if username != "" && whoInfo.Opponent != "" &&
!strings.Contains(username, "Bot") && !strings.Contains(whoInfo.Opponent, "Bot") {
options = append(options, username, whoInfo.Opponent)
}
}
if len(options) == 0 {
for username, whoInfo := range c.who {
if username != "" && whoInfo.Opponent != "" {
options = append(options, username, whoInfo.Opponent)
}
}
if len(options) == 0 {
return
}
}
option := options[rand.Intn(len(options))]
c.Out <- []byte("watch " + option)
}
func (c *Client) Connect() error {
l(fmt.Sprintf("Connecting to %s...", c.serverAddress))
go c.keepAlive()
err := telnet.DialToAndCall(c.serverAddress, c)
if err != nil {
lf("** Disconnected: %s", err)
}
return err
}
func (c *Client) eventLoop() {
var setBoardStyle bool
var turnRegexp = regexp.MustCompile(`^turn: (\w+)\.`)
var movesRegexp = regexp.MustCompile(`^(\w+) moves (.*)`)
var rollsRegexp = regexp.MustCompile(`^(\w+) rolls? (.*)`)
var logInOutRegexp = regexp.MustCompile(`^\w+ logs [In|Out]\.`)
var dropsConnection = regexp.MustCompile(`^\w+ drops connection\.`)
var startMatchRegexp = regexp.MustCompile(`^\w+ and \w+ start a .*`)
var winsMatchRegexp = regexp.MustCompile(`^\w+ wins a [0-9]+ point match against .*`)
var newGameRegexp = regexp.MustCompile(`^Starting a new game with .*`)
var gameBufferRegexp = regexp.MustCompile(`^\w+ (makes|roll|rolls|rolled|move|moves) .*`)
var pleaseMoveRegexp = regexp.MustCompile(`^Please move ([0-9]) pieces?.`)
var cantMoveRegexp = regexp.MustCompile(`^(\w+) can't move.`)
var doublesRegexp = regexp.MustCompile(`^\w+ doubles.`)
var acceptDoubleRegexp = regexp.MustCompile(`^\w+ accepts the double.`)
for b := range c.In {
b = bytes.Replace(b, []byte{7}, []byte{}, -1)
b = bytes.TrimSpace(b)
bl := bytes.ToLower(b)
// Select 10+ first to read prefixes correctly
if bytes.HasPrefix(b, TypeMsgSaved) {
lf("Message to %s saved", b[3:])
continue
} else if bytes.HasPrefix(b, TypeMsgDelivered) {
lf("Message to %s delivered", b[3:])
continue
} else if bytes.HasPrefix(b, TypeSay) {
s := bytes.SplitN(b[3:], []byte(" "), 2)
lf("%s says: %s", s[0], s[1])
continue
} else if bytes.HasPrefix(b, TypeShout) {
s := bytes.SplitN(b[3:], []byte(" "), 2)
lf("%s shouts: %s", s[0], s[1])
continue
} else if bytes.HasPrefix(b, TypeWhisper) {
s := bytes.SplitN(b[3:], []byte(" "), 2)
lf("%s whispers: %s", s[0], s[1])
continue
} else if bytes.HasPrefix(b, TypeKibitz) {
s := bytes.SplitN(b[3:], []byte(" "), 2)
lf("%s kibitzes: %s", s[0], s[1])
continue
} else if bytes.HasPrefix(b, TypeYouSay) {
s := bytes.SplitN(b[3:], []byte(" "), 2)
lf("You say to %s: %s", s[0], s[1])
continue
} else if bytes.HasPrefix(b, TypeYouShout) {
lf("You shout: %s", b[3:])
continue
} else if bytes.HasPrefix(b, TypeYouWhisper) {
lf("You whisper: %s", b[3:])
continue
} else if bytes.HasPrefix(b, TypeYouKibitz) {
lf("You kibitz: %s", b[3:])
continue
} else if bytes.HasPrefix(b, TypeWelcome) {
s := bytes.Split(b[2:], []byte(" "))
loginTimestamp, err := strconv.Atoi(string(s[1]))
if err != nil {
loginTimestamp = 0
}
loginTime := time.Unix(int64(loginTimestamp), 0)
lf("Welcome, %s! Last login at %s", s[0], loginTime)
continue
} else if bytes.HasPrefix(b, TypeOwnInfo) {
// TODO Print own info
continue
} else if bytes.HasPrefix(b, TypeMOTD) {
for _, line := range bytes.Split(c.motd, []byte("\n")) {
l(string(line))
}
if !setBoardStyle {
c.Out <- []byte("set boardstyle 3")
setBoardStyle = true
}
continue
} else if bytes.HasPrefix(b, TypeWhoInfo) {
c.updateWhoInfo(b[2:])
// TODO who window
continue
} else if bytes.HasPrefix(b, TypeEndWhoInfo) {
// TODO draw who info event
continue
} else if bytes.HasPrefix(b, TypeLogin) || bytes.HasPrefix(b, TypeLogout) {
b = b[2:]
b = b[bytes.IndexByte(b, ' ')+1:]
l(string(b))
// TODO enable showing log In and Out messages
// TODO remove from who
continue
} else if bytes.HasPrefix(b, TypeMsg) {
lf("Received message: %s", b[3:])
continue
} else if bytes.HasPrefix(b, TypeBoardState) {
c.Board.SetState(string(bytes.SplitN(b, []byte(" "), 2)[0][6:]))
continue
} else if turnRegexp.Match(b) {
turn := turnRegexp.FindSubmatch(b)
if string(turn[1]) == c.Username || string(turn[1]) == c.Board.s[0] || string(turn[1]) == "You" {
c.Board.v[StateTurn] = c.Board.v[StatePlayerColor]
} else {
c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] * -1
}
} else if rollsRegexp.Match(b) {
roll := rollsRegexp.FindSubmatch(b)
periodIndex := bytes.IndexRune(roll[2], '.')
if periodIndex > -1 {
roll[2] = roll[2][:periodIndex]
}
s := bytes.Split(roll[2], []byte(" "))
var dice [2]int
var i int
for _, m := range s {
v, err := strconv.Atoi(string(m))
if err == nil {
dice[i] = v
i++
}
}
if string(roll[1]) == c.Board.s[0] || string(roll[1]) == "You" {
c.Board.v[StateTurn] = c.Board.v[StatePlayerColor]
c.Board.playerDice = dice
} else {
c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] * -1
c.Board.opponentDice = dice
}
c.Board.ResetMoves()
c.Board.Draw()
} else if movesRegexp.Match(b) {
match := movesRegexp.FindSubmatch(b)
player := c.Board.v[StatePlayerColor]
if string(match[1]) == c.Board.s[1] {
player *= -1
}
c.Board.ResetMoves()
from := -1
to := -1
s := bytes.Split(match[2], []byte(" "))
for _, m := range s {
move := bytes.Split(m, []byte("-"))
if len(move) == 2 {
from = c.Board.parseMoveString(player, string(move[0]))
to = c.Board.parseMoveString(player, string(move[1]))
if from >= 0 && to >= 0 {
c.Event <- &EventMove{
Player: player,
From: from,
To: to,
}
}
c.Board.Move(player, string(move[0]), string(move[1]))
}
}
c.Board.SimplifyMoves()
c.Board.Draw()
bs := string(b)
if strings.HasSuffix(bs, " .") {
bs = bs[:len(bs)-2] + "."
}
lg(bs)
continue
} else if string(b) == "Value of 'boardstyle' set to 3." {
continue
} else if string(b) == "It's your turn to move." || strings.TrimSpace(string(b)) == "It's your turn to roll or double." || strings.TrimSpace(string(b)) == "It's your turn. Please roll or double" {
c.Board.v[StateTurn] = c.Board.v[StatePlayerColor]
c.Board.Draw()
if strings.TrimSpace(string(b)) == "It's your turn to roll or double." || strings.TrimSpace(string(b)) == "It's your turn. Please roll or double" {
c.Out <- []byte("roll") // TODO Delay and allow configuring
}
} else if cantMoveRegexp.Match(b) {
match := cantMoveRegexp.FindSubmatch(b)
u := string(match[1])
if u == c.Username || u == c.Board.s[0] || u == "You" {
c.Board.opponentDice[0] = 0
c.Board.opponentDice[1] = 0
c.Board.v[StateTurn] = c.Board.v[StatePlayerColor] * -1
} else if u == c.Board.s[1] {
c.Board.playerDice[0] = 0
c.Board.playerDice[1] = 0
c.Board.v[StateTurn] = c.Board.v[StatePlayerColor]
}
} else if pleaseMoveRegexp.Match(b) {
match := pleaseMoveRegexp.FindSubmatch(b)
n, err := strconv.Atoi(string(match[1]))
if err == nil {
c.Board.v[StateMovablePieces] = n
}
} else if bytes.HasPrefix(bl, []byte("you're now watching")) {
// Board state is not sent automatically when watching
log.Println("WRITE BOARDD")
c.Out <- []byte("board")
} else if logInOutRegexp.Match(b) {
continue
} else if dropsConnection.Match(b) {
continue
} else if startMatchRegexp.Match(b) {
continue
} else if winsMatchRegexp.Match(b) {
continue
} else if newGameRegexp.Match(b) {
c.Board.ResetMoves()
c.Board.resetSelection()
// TODO reset premove
continue
}
if gameBufferRegexp.Match(b) || cantMoveRegexp.Match(b) ||
doublesRegexp.Match(b) || acceptDoubleRegexp.Match(b) ||
bytes.HasPrefix(bl, []byte("you're now watching")) || bytes.HasPrefix(bl, []byte("** you stop watching")) ||
strings.HasPrefix(string(b), "Bearing off:") || strings.HasPrefix(string(b), "The only possible move") {
lg(string(b))
if !bytes.HasPrefix(bl, []byte("you're now watching")) && !bytes.HasPrefix(bl, []byte("** you stop watching")) {
continue
}
}
l(string(b))
}
}

4
doc.go Normal file
View File

@ -0,0 +1,4 @@
/*
Package fibs provides a FIBS.com (online backgammon) client library.
*/
package fibs

14
event.go Normal file
View File

@ -0,0 +1,14 @@
package fibs
type EventGame struct {
}
type EventMove struct {
Player int
From int
To int
}
type EventMessage struct {
Message string
}

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module code.rocketnine.space/tslocum/fibs
go 1.17
require (
github.com/reiver/go-oi v1.0.0
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
golang.org/x/text v0.3.7
)

7
go.sum Normal file
View File

@ -0,0 +1,7 @@
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

48
util.go Normal file
View File

@ -0,0 +1,48 @@
package fibs
import (
"fmt"
"io"
"log"
"time"
)
var (
StatusWriter io.Writer
GameWriter io.Writer
statusLogged bool
gameLogged bool
)
func l(s string) {
m := time.Now().Format("15:04") + "| " + s
if StatusWriter != nil {
if statusLogged {
StatusWriter.Write([]byte("\n" + m))
return
}
StatusWriter.Write([]byte(m))
statusLogged = true
return
}
log.Print(m)
}
func lf(format string, a ...interface{}) {
l(fmt.Sprintf(format, a...))
}
func lg(s string) {
m := time.Now().Format("15:04") + "| " + s
if GameWriter != nil {
if gameLogged {
GameWriter.Write([]byte("\n" + m))
return
}
GameWriter.Write([]byte(m))
gameLogged = true
return
}
log.Print(m)
}