Browse Source

Extract FIBS-related code as library

master
Trevor Slocum 1 week ago
parent
commit
7381027a05
  1. 8
      LICENSE
  2. 84
      app.go
  3. 799
      board.go
  4. 63
      board_test.go
  5. 621
      client.go
  6. 9
      go.mod
  7. 82
      go.sum
  8. 57
      main.go

8
LICENSE

@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

84
app.go

@ -6,6 +6,7 @@ import (
"time"
"code.rocketnine.space/tslocum/cview"
"code.rocketnine.space/tslocum/fibs"
"github.com/gdamore/tcell/v2"
)
@ -19,7 +20,7 @@ var (
uiGrid *cview.Grid
userList *cview.List
userListGrid *cview.Grid
board *Board
board *GameBoard
actionBuffer *cview.TextView
gameBuffer *cview.TextView
statusBuffer *cview.TextView
@ -75,14 +76,14 @@ func updateClock() {
}
}
func logIn(c *Client) {
func logIn(c *fibs.Client) {
app.SetRoot(uiGrid, true)
app.SetFocus(inputField)
go c.connect()
go c.Connect()
}
func setScreen(c *Client, screen int) {
func setScreen(c *fibs.Client, screen int) {
viewScreen = screen
if viewScreen == ScreenLobby {
updateUserList(c)
@ -95,38 +96,37 @@ func setScreen(c *Client, screen int) {
buildLayout(c)
}
func updateUserList(c *Client) {
var infos []*WhoInfo
for _, whoInfo := range c.who {
infos = append(infos, whoInfo)
}
func updateUserList(c *fibs.Client) {
infos := c.GetAllWhoInfo()
sort.Slice(infos, func(i, j int) bool {
if (infos[i].opponent == "") != (infos[j].opponent == "") {
return infos[i].opponent == ""
if (infos[i].Opponent == "") != (infos[j].Opponent == "") {
return infos[i].Opponent == ""
}
if (infos[i].watching == "") != (infos[j].watching == "") {
return infos[i].watching == ""
if (infos[i].Watching == "") != (infos[j].Watching == "") {
return infos[i].Watching == ""
}
if infos[i].ready != infos[j].ready {
return infos[i].ready
if infos[i].Ready != infos[j].Ready {
return infos[i].Ready
}
if infos[i].rating != infos[j].rating {
return infos[i].rating < infos[j].rating
if infos[i].Rating != infos[j].Rating {
return infos[i].Rating < infos[j].Rating
}
return strings.ToLower(infos[i].username) < strings.ToLower(infos[j].username)
return strings.ToLower(infos[i].Username) < strings.ToLower(infos[j].Username)
})
userList.Clear()
for _, whoInfo := range infos {
userList.AddItem(whoInfo.listItem)
//userList.AddItem(whoInfo.listItem)
// TODO add list item
_ = whoInfo
}
}
func buildLayout(c *Client) {
func buildLayout(c *fibs.Client) {
uiGrid.Clear()
var currentScreen cview.Primitive
@ -158,14 +158,14 @@ func buildLayout(c *Client) {
uiGrid.AddItem(inputField, 3, 0, 1, 3, 0, 0, true)
}
func RunApp(c *Client) error {
func RunApp(c *fibs.Client, b *GameBoard) error {
app = cview.NewApplication()
app.EnableMouse(true)
var focusFunc func(p cview.Primitive) bool
focusFunc = func(p cview.Primitive) bool {
if !c.loggedin {
if !c.LoggedIn() {
return true
}
@ -210,7 +210,7 @@ func RunApp(c *Client) error {
text = "kibitz " + text
}
c.out <- []byte(text)
c.Out <- []byte(text)
inputField.SetText("")
})
inputField.SetFieldBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
@ -245,14 +245,14 @@ func RunApp(c *Client) error {
form.AddFormItem(passwordField)
connectFunc := func() {
c.username = usernameField.GetText()
c.password = passwordField.GetText()
c.Username = usernameField.GetText()
c.Password = passwordField.GetText()
logIn(c)
}
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if !c.loggedin {
if !c.LoggedIn() {
if event.Key() == tcell.KeyEnter {
formIndex, buttonIndex := form.GetFocusedItemIndex()
if formIndex > 0 || buttonIndex == 0 {
@ -310,6 +310,9 @@ func RunApp(c *Client) error {
statusWriter = &bufferWriter{Buffer: statusBuffer}
gameWriter = &bufferWriter{Buffer: gameBuffer}
fibs.StatusWriter = statusWriter
fibs.GameWriter = gameWriter
userList = cview.NewList()
userList.ShowSecondaryText(false)
userList.SetHighlightFullLine(true)
@ -345,12 +348,14 @@ func RunApp(c *Client) error {
// TODO refactor
buildLayout(c)
defer func() {
if c.username != "" && c.password != "" {
if c.Username != "" && c.Password != "" {
app.SetRoot(uiGrid, true)
app.SetFocus(inputField)
}
}()
go HandleEvents(c, b)
setScreen(c, ScreenGame)
lg("+---------------------------------------------------+")
@ -362,7 +367,7 @@ func RunApp(c *Client) error {
lg("| |")
lg("+---------------------------------------------------+")
if c.username == "" || c.password == "" {
if c.Username == "" || c.Password == "" {
app.SetRoot(f2, true)
app.SetFocus(form)
} else {
@ -373,3 +378,26 @@ func RunApp(c *Client) error {
return app.Run()
}
func HandleEvents(c *fibs.Client, b *GameBoard) {
for e := range c.Event {
switch event := e.(type) {
case *fibs.EventBoardState:
//b.SetState(event.S, event.V)
l("STATE")
b.Update()
app.Draw()
case *fibs.EventMove:
//b.movePiece(event.From, event.To)
l("MOVE")
b.Update()
app.Draw()
case *fibs.EventDraw:
//b.ProcessState()
l("DRAW")
_ = event
b.Update()
app.Draw()
}
}
}

799
board.go

@ -1,761 +1,52 @@
package main
import (
"bytes"
"fmt"
"log"
"sort"
"strconv"
"strings"
"code.rocketnine.space/tslocum/cview"
"code.rocketnine.space/tslocum/fibs"
"github.com/gdamore/tcell/v2"
)
// TODO Add PlayerName, etc
const SpaceUnknown = -1
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
)
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 {
type GameBoard struct {
Board *fibs.Board
*cview.TextView
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
dragFromX, dragFromY int
}
func NewBoard(client *Client) *Board {
b := &Board{
func NewGameBoard(client *fibs.Client) *GameBoard {
b := &GameBoard{
Board: client.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.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
}
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()
b.TextView.SetRegions(true)
b.TextView.SetDynamicColors(true)
b.TextView.SetToggleHighlights(true)
b.TextView.SetHighlightedFunc(b.handleHighlight)
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.Update()
return b
}
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) Update() {
b.Lock()
if app != nil {
defer app.Draw(b)
}
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(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 := "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()
b.TextView.SetBytes(t.Bytes())
}
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) Move(player int, f string, t string) {
from, err := strconv.Atoi(f)
if err != nil {
from = SpaceUnknown
if f == "bar" {
barSpace := b.playerBarSpace()
if b.v[StatePlayerColor] == player {
from = barSpace
} else {
from = 25 - barSpace
}
}
}
to, err := strconv.Atoi(t)
if err != nil {
to = SpaceUnknown
if t == "off" {
to = b.playerHomeSpace()
}
}
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.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 *GameBoard) Update() {
b.TextView.SetBytes(b.Board.Render())
}
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 *GameBoard) resetSelection() {
b.selected[0] = 0
b.selected[1] = 0
}
func (b *Board) handleHighlight(added, removed, remaining []string) {
func (b *GameBoard) handleHighlight(added, removed, remaining []string) {
defer b.Update()
v := b.Board.GetIntState()
if len(added) > 0 && len(remaining) > 0 {
if added[0] == "space-0" || added[0] == "space-25" {
// Deselect
@ -780,7 +71,7 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
to, err := strconv.Atoi(added[0][6:])
if err != nil {
if added[0] == "space-off" {
to = b.playerHomeSpace()
to = b.Board.PlayerHomeSpace()
} else {
return
}
@ -796,17 +87,20 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
}
}
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) {
if (v[fibs.StatePlayerDice1] != v[fibs.StatePlayerDice2] && spaces*b.selected[1] == v[fibs.StatePlayerDice1]+v[fibs.StatePlayerDice2]) ||
(v[fibs.StatePlayerDice1] == v[fibs.StatePlayerDice2] && spaces*b.selected[1] == (v[fibs.StatePlayerDice1]+v[fibs.StatePlayerDice2])*2) {
// Prefer any move that will bar opponent
for i := 0; i < 2; i++ {
dice := b.playerDice[i]
index := calcFrom + (dice * b.v[StateDirection])
dice := v[fibs.StatePlayerDice1]
if i == 2 {
dice = v[fibs.StatePlayerDice2]
}
index := calcFrom + (dice * v[fibs.StateDirection])
if index == to {
continue
}
if (b.v[StateBoardSpace0+index] == -1 && b.v[StatePlayerColor] > 0) ||
(b.v[StateBoardSpace0+index] == 1 && b.v[StatePlayerColor] < 0) {
if (v[fibs.StateBoardSpace0+index] == -1 && v[fibs.StatePlayerColor] > 0) ||
(v[fibs.StateBoardSpace0+index] == 1 && v[fibs.StatePlayerColor] < 0) {
mid = index
break
}
@ -814,13 +108,16 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
if mid == SpaceUnknown {
// Send any valid move
for i := 0; i < 2; i++ {
dice := b.playerDice[i]
index := calcFrom + (dice * b.v[StateDirection])
dice := v[fibs.StatePlayerDice1]
if i == 2 {
dice = v[fibs.StatePlayerDice2]
}
index := calcFrom + (dice * v[fibs.StateDirection])
if index == to {
continue
}
if (b.v[StateBoardSpace0+index] >= 0 && b.v[StatePlayerColor] > 0) ||
(b.v[StateBoardSpace0+index] <= 0 && b.v[StatePlayerColor] < 0) {
if (v[fibs.StateBoardSpace0+index] >= 0 && v[fibs.StatePlayerColor] > 0) ||
(v[fibs.StateBoardSpace0+index] <= 0 && v[fibs.StatePlayerColor] < 0) {
mid = index
break
}
@ -830,10 +127,10 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
for i := 0; i < b.selected[1]; i++ {
if mid < 0 {
b.addPreMove(from, to)
b.Board.AddPreMove(from, to)
} else {
b.addPreMove(from, mid)
b.addPreMove(mid, to)
b.Board.AddPreMove(from, mid)
b.Board.AddPreMove(mid, to)
}
}
@ -843,8 +140,6 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
b.TextView.SetHighlightedFunc(b.handleHighlight)
b.resetSelection()
b.autoSendMoves()
} else if len(added) > 0 {
if added[0] == "space-off" {
b.TextView.SetHighlightedFunc(nil)
@ -853,7 +148,7 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
b.resetSelection()
return
} else if (added[0] == "space-0" || added[0] == "space-25") && b.v[StatePlayerBar] == 0 {
} else if (added[0] == "space-0" || added[0] == "space-25") && v[fibs.StatePlayerBar] == 0 {
b.TextView.SetHighlightedFunc(nil)
b.TextView.Highlight(added[0])
b.TextView.SetHighlightedFunc(b.handleHighlight)
@ -864,12 +159,12 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
index, err := strconv.Atoi(added[0][6:])
if err == nil {
abs := b.v[StateBoardSpace0+index]
abs := v[fibs.StateBoardSpace0+index]
if abs < 0 {
abs *= -1
}
if added[0] == "space-0" || added[0] == "space-25" {
abs = b.v[StatePlayerBar]
abs = v[fibs.StatePlayerBar]
}
if b.selected[1] >= abs && false { // TODO or has premove piece in space
@ -885,12 +180,12 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
} else if len(removed) > 0 {
index, err := strconv.Atoi(removed[0][6:])
if err == nil {
abs := b.v[StateBoardSpace0+index]
abs := v[fibs.StateBoardSpace0+index]
if abs < 0 {
abs *= -1
}
if removed[0] == "space-0" || removed[0] == "space-25" {
abs = b.v[StatePlayerBar]
abs = v[fibs.StatePlayerBar]
}
if b.selected[1] < abs {
@ -906,7 +201,7 @@ func (b *Board) handleHighlight(added, removed, remaining []string) {
}
// 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) {
func (b *GameBoard) 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) {

63
board_test.go

@ -1,63 +0,0 @@
package main
import (
"fmt"
"testing"
)
func TestBoard_GetValidMoves(t *testing.T) {
client := NewClient("", "")
board = NewBoard(client)
b := board
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.Update()
validMoves := board.GetValidMoves(c.from)
if !equalInts(validMoves, c.moves) {
t.Errorf("unexpected valid moves: expected %+v, got %+v\n%s", c.moves, validMoves, b.GetBytes(true))
}
})
}
}
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
}

621
client.go

@ -1,621 +0,0 @@
package main
import (
"bytes"
"fmt"
"log"
"math/rand"
"regexp"
"strconv"
"strings"
"time"
"golang.org/x/text/language"
"golang.org/x/text/message"
"code.rocketnine.space/tslocum/cview"
"github.com/reiver/go-oi"
"github.com/reiver/go-telnet"
)
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
listItem *cview.ListItem
}
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
username string
password string
loggedin bool
motd []byte
rawMode bool
who map[string]*WhoInfo
notified map[string]bool
}
func NewClient(username string, password string) *Client {
return &Client{
in: make(chan []byte, 100),
out: make(chan []byte, 100),
username: username,
password: password,
who: make(map[string]*WhoInfo),
notified: make(map[string]bool),
}
}
// 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
go func() {
time.Sleep(time.Second)
app.QueueUpdateDraw(func() {
statusBuffer.ScrollToEnd()
})
}()
}
}
}
}()
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", board.GetState())
lf("Player color: %d", board.v[StatePlayerColor])
lf("Direction: %d", board.v[StateDirection])
lf("Current turn: %d", board.v[StateTurn])
lf("Moves: %+v", board.moves)
lf("Pre-moves: %+v", 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
}
var listItem *cview.ListItem
if existingInfo, ok := c.who[string(s[whoInfoDataName])]; ok {
listItem = existingInfo.listItem
}
info := &WhoInfo{
username: string(s[whoInfoDataName]),
listItem: listItem,
}
r := s[whoInfoDataRating]
if bytes.ContainsRune(r, '.') {
r = s[whoInfoDataRating][:bytes.IndexByte(s[whoInfoDataRating], '.')]
}
rating, err := strconv.Atoi(string(r))