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.

404 lines
9.5 KiB

4 months ago
package main
import (
"sort"
"strings"
4 months ago
"time"
4 months ago
"code.rocketnine.space/tslocum/cview"
"code.rocketnine.space/tslocum/fibs"
4 months ago
"github.com/gdamore/tcell/v2"
)
const (
ScreenLobby = iota
ScreenGame
)
4 months ago
var (
app *cview.Application
uiGrid *cview.Grid
userList *cview.List
userListGrid *cview.Grid
board *GameBoard
actionBuffer *cview.TextView
gameBuffer *cview.TextView
statusBuffer *cview.TextView
4 months ago
inputField *cview.InputField
4 months ago
clock *cview.TextView
form *cview.Form
4 months ago
statusWriter *bufferWriter
gameWriter *bufferWriter
chatMode int
viewScreen int
screenX int
)
const (
ChatModeShout = iota
ChatModeKibitz
4 months ago
)
const initialState = "bgammon: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"
4 months ago
type bufferWriter struct {
Buffer *cview.TextView
4 months ago
}
func (w *bufferWriter) Write(p []byte) (int, error) {
n, err := w.Buffer.Write(p)
app.Draw(w.Buffer)
4 months ago
return n, err
}
4 months ago
func updateChatType() {
if chatMode == ChatModeShout {
inputField.SetPlaceholder(" Shout")
} else {
inputField.SetPlaceholder(" Kibitz")
}
4 months ago
}
func updateClock() {
var displayedTime string
var newTime string
t := time.NewTicker(time.Second)
for range t.C {
newTime = time.Now().Format("15:04")
if newTime != displayedTime {
app.QueueUpdateDraw(func() {
clock.SetText(newTime + string(cview.BoxDrawingsLightVertical))
})
displayedTime = newTime
}
}
}
func logIn(c *fibs.Client) {
app.SetRoot(uiGrid, true)
app.SetFocus(inputField)
go c.Connect()
}
func setScreen(c *fibs.Client, screen int) {
viewScreen = screen
if viewScreen == ScreenLobby {
updateUserList(c)
board.SetVisible(false)
userListGrid.SetVisible(true)
} else {
userListGrid.SetVisible(false)
board.SetVisible(true)
}
buildLayout(c)
}
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].Watching == "") != (infos[j].Watching == "") {
return infos[i].Watching == ""
}
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
}
return strings.ToLower(infos[i].Username) < strings.ToLower(infos[j].Username)
})
userList.Clear()
for _, whoInfo := range infos {
//userList.AddItem(whoInfo.listItem)
// TODO add list item
_ = whoInfo
}
}
func buildLayout(c *fibs.Client) {
uiGrid.Clear()
var currentScreen cview.Primitive
if viewScreen == ScreenLobby {
currentScreen = userListGrid
} else {
currentScreen = board
}
// Single column on smaller screens
if screenX < 156 {
uiGrid.SetRows(16, 1, -1, 1, -1, 1)
uiGrid.AddItem(currentScreen, 0, 0, 1, 1, 0, 0, false)
uiGrid.AddItem(cview.NewBox(), 1, 0, 1, 1, 0, 0, false)
uiGrid.AddItem(gameBuffer, 2, 0, 1, 1, 0, 0, false)
uiGrid.AddItem(cview.NewBox(), 3, 0, 1, 1, 0, 0, false)
uiGrid.AddItem(statusBuffer, 4, 0, 1, 1, 0, 0, false)
uiGrid.AddItem(inputField, 5, 0, 1, 1, 0, 0, true)
return
}
uiGrid.SetRows(16, 1, -1, 1)
uiGrid.SetColumns(-1, 1, -1)
uiGrid.AddItem(currentScreen, 0, 0, 1, 3, 0, 0, false)
uiGrid.AddItem(cview.NewBox(), 1, 0, 1, 3, 0, 0, false)
uiGrid.AddItem(gameBuffer, 2, 0, 1, 1, 0, 0, false)
uiGrid.AddItem(cview.NewBox(), 2, 1, 1, 1, 0, 0, false)
uiGrid.AddItem(statusBuffer, 2, 2, 1, 1, 0, 0, false)
uiGrid.AddItem(inputField, 3, 0, 1, 3, 0, 0, true)
}
func RunApp(c *fibs.Client, b *GameBoard) error {
4 months ago
app = cview.NewApplication()
app.EnableMouse(true)
var focusFunc func(p cview.Primitive) bool
focusFunc = func(p cview.Primitive) bool {
if !c.LoggedIn() {
return true
}
4 months ago
if p != inputField {
app.SetFocus(inputField)
return false
}
4 months ago
return true
}
app.SetBeforeFocusFunc(focusFunc)
4 months ago
actionBuffer = cview.NewTextView()
gameBuffer = cview.NewTextView()
gameBuffer.SetVerticalAlign(cview.AlignBottom)
gameBuffer.SetScrollable(true)
gameBuffer.SetScrollBarVisibility(cview.ScrollBarAlways)
statusBuffer = cview.NewTextView()
statusBuffer.SetVerticalAlign(cview.AlignBottom)
statusBuffer.SetScrollable(true)
statusBuffer.SetScrollBarVisibility(cview.ScrollBarAlways)
4 months ago
clock = cview.NewTextView()
clock.SetText(time.Now().Format("15:04") + string(cview.BoxDrawingsLightVertical))
// TODO what happens when the clock is clicked?
4 months ago
inputField = cview.NewInputField()
inputField.SetDoneFunc(func(key tcell.Key) {
text := inputField.GetText()
if len(text) == 0 {
return
}
if text[0] == '/' {
text = text[1:]
} else if chatMode == ChatModeShout {
text = "shout " + text
} else if chatMode == ChatModeKibitz {
text = "kibitz " + text
}
c.Out <- []byte(text)
4 months ago
inputField.SetText("")
})
inputField.SetFieldBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
inputField.SetFieldBackgroundColorFocused(cview.Styles.PrimitiveBackgroundColor)
4 months ago
updateChatType()
f3 := cview.NewFlex()
f3.AddItem(board, 0, 1, false)
f3.AddItem(gameBuffer, 0, 1, false)
f4 := cview.NewFlex()
f4.AddItem(clock, 7, 1, false)
f4.AddItem(inputField, 0, 1, false)
4 months ago
4 months ago
f := cview.NewFlex()
f.SetDirection(cview.FlexRow)
f.AddItem(f3, 14, 1, true)
f.AddItem(actionBuffer, 3, 1, false)
4 months ago
f.AddItem(statusBuffer, 0, 1, false)
f.AddItem(f4, 1, 1, false)
4 months ago
form = cview.NewForm()
4 months ago
usernameField := cview.NewInputField()
usernameField.SetLabel("Username")
usernameField.SetFieldWidth(16)
passwordField := cview.NewInputField()
passwordField.SetLabel("Password")
passwordField.SetFieldWidth(16)
passwordField.SetMaskCharacter('*')
form.AddFormItem(usernameField)
form.AddFormItem(passwordField)
connectFunc := func() {
c.Username = usernameField.GetText()
c.Password = passwordField.GetText()
4 months ago
logIn(c)
}
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if !c.LoggedIn() {
if event.Key() == tcell.KeyEnter {
formIndex, buttonIndex := form.GetFocusedItemIndex()
if formIndex > 0 || buttonIndex == 0 {
connectFunc()
return nil
}
}
return event
}
if viewScreen == ScreenLobby {
switch event.Key() {
case tcell.KeyUp:
// Transform user list
}
}
if event.Key() == tcell.KeyESC {
newScreen := ScreenLobby
if viewScreen == ScreenLobby {
newScreen = ScreenGame
}
setScreen(c, newScreen)
return nil
}
if event.Key() == tcell.KeyTab {
if chatMode == ChatModeShout {
chatMode = ChatModeKibitz
} else {
chatMode = ChatModeShout
}
updateChatType()
return nil
}
return event
4 months ago
})
form.AddButton("Connect", connectFunc)
4 months ago
form.SetPadding(1, 0, 2, 0)
logInHeader := cview.NewTextView()
logInHeader.SetDynamicColors(true)
logInHeader.SetText("\n [" + cview.ColorHex(cview.Styles.SecondaryTextColor) + "] Connect to FIBS[-]")
logInFooter := cview.NewTextView()
logInFooter.SetText(" For information on how to play backgammon, visit\n https://bkgm.com/rules.html\n\n For information on how to register, visit\n http://www.fibs.com/help.html#register\n\n For information on bgammon (this FIBS client), visit\n https://code.rocketnine.space/tslocum/bgammon")
f2 := cview.NewFlex() // Login flex
f2.SetDirection(cview.FlexRow)
f2.AddItem(logInHeader, 2, 1, true)
f2.AddItem(form, 8, 1, true)
f2.AddItem(logInFooter, 0, 1, true)
4 months ago
statusWriter = &bufferWriter{Buffer: statusBuffer}
gameWriter = &bufferWriter{Buffer: gameBuffer}
fibs.StatusWriter = statusWriter
fibs.GameWriter = gameWriter
userList = cview.NewList()
userList.ShowSecondaryText(false)
userList.SetHighlightFullLine(true)
userList.AddContextItem("Invite", 'i', func(index int) {
})
userList.AddContextItem("Watch", 'w', func(index int) {
})
userList.AddContextItem("Message", 'm', func(index int) {
})
userList.SetSelectedFunc(func(i int, item *cview.ListItem) {
lf("%d", i)
})
userListHeader := cview.NewTextView()
userListHeader.SetText("[yellow::b]Player Rating Experience Status[-:-:-]")
userListHeader.SetDynamicColors(true)
userListGrid = cview.NewGrid()
userListGrid.SetRows(1, -1)
userListGrid.AddItem(userListHeader, 0, 0, 1, 1, 0, 0, false)
userListGrid.AddItem(userList, 1, 0, 1, 1, 0, 0, true)
uiGrid = cview.NewGrid()
app.SetAfterResizeFunc(func(width int, height int) {
screenX = width
buildLayout(c)
})
// TODO refactor
buildLayout(c)
defer func() {
if c.Username != "" && c.Password != "" {
app.SetRoot(uiGrid, true)
app.SetFocus(inputField)
}
}()
go HandleEvents(c, b)
setScreen(c, ScreenGame)
lg("+---------------------------------------------------+")
lg("| |")
lg("| Welcome to bgammon. |")
lg("| This program is free software. |")
lg("| https://code.rocketnine.space/tslocum/bgammon |")
lg("| |")
lg("| |")
lg("+---------------------------------------------------+")
if c.Username == "" || c.Password == "" {
4 months ago
app.SetRoot(f2, true)
app.SetFocus(form)
} else {
logIn(c)
4 months ago
}
4 months ago
4 months ago
go updateClock()
4 months ago
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()
}
}
}