Cribbage.World terminal-based client
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.

328 lines
7.3 KiB

package main
import (
"flag"
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/gdamore/tcell/v2"
"gitlab.com/tslocum/cbind"
"gitlab.com/tslocum/cview"
"gitlab.com/tslocum/joker"
cribbage "gitlab.com/tslocum/joker-cribbage"
)
const (
defaultStatusText = "Press Enter to chat, 1-6 to throw a card, Space to continue"
logFormat = "2006-01-02 15:04:05"
)
var (
debug bool
app *cview.Application
inputConfig *cbind.Configuration
statusText *statusTextView
gameText *cview.TextView
inputField *cview.InputField
statusBuf *cview.TextView
statusGrid *cview.Grid
throwGrid *cview.Grid
starterWidget *cardWidget
mainHandStack *cardStackWidget
mainCribStack *cardStackWidget
throwStack *cardStackWidget
oppCribStack *cardStackWidget
currentGameState = &gameState{}
writeBuffer = make(chan string)
exit = make(chan bool)
inputFocused bool
wroteFirstLogMessage bool
logMutex sync.Mutex
)
func setupThrowGrid() {
if throwGrid == nil {
throwGrid = cview.NewGrid()
}
throwGrid.Clear()
if currentGameState.Phase == PhasePeg {
throwGrid.SetColumns(-1)
throwGrid.AddItem(throwStack, 0, 0, 1, 1, 0, 0, false)
} else {
throwGrid.SetColumns((cardBufferX * 5) + cardWidth)
throwGrid.AddItem(throwStack, 0, 0, 1, 1, 0, 0, false)
throwGrid.AddItem(oppCribStack, 0, 1, 1, 1, 0, 0, false)
}
}
// Starts at 1
func selectCardIndex(cardNumber int) {
if cardNumber <= 0 || cardNumber > len(mainHandStack.Cards) {
return
}
var selected int
if !mainHandStack.Cards[cardNumber-1].selected {
for _, card := range mainHandStack.Cards {
if card.selected {
selected++
}
}
if selected >= mainHandStack.GetMaxSelection() {
return
}
}
mainHandStack.Cards[cardNumber-1].Select()
}
func throwCard(card joker.Card) {
if currentGameState.Phase == PhasePick {
var selected int
for i := range mainHandStack.Cards {
if mainHandStack.Cards[i].selected {
selected++
}
}
if len(mainHandStack.Cards) == 6 {
if selected == 2 {
// TODO Race condition
for _, throwCard := range mainHandStack.Cards {
if throwCard.selected {
writeBuffer <- fmt.Sprintf("throw %s", throwCard.Identifier())
}
}
}
} else {
writeBuffer <- fmt.Sprintf("throw %s", card.Identifier())
}
} else if currentGameState.Phase == PhasePeg && currentGameState.Turn == currentGameState.Player {
if cribbage.Sum(currentGameState.ThrowPile.Cards())+cribbage.Value(card) > 31 {
setStatusText("Can not throw card: illegal move")
} else {
writeBuffer <- fmt.Sprintf("throw %s", card.Identifier())
}
}
}
func doAction() {
var cards joker.Cards
for _, card := range mainHandStack.Cards {
if card.selected {
cards = append(cards, card.Card)
}
}
if len(cards) > 0 {
for i := range cards {
throwCard(cards[i])
}
return
}
writeBuffer <- "continue"
}
func setStatusText(status string) {
app.QueueUpdateDraw(func() {
statusText.SetText(" " + status)
})
}
func updateGameText() {
playerScore := currentGameState.Score1
opponentScore := currentGameState.Score2
if currentGameState.Player == 2 {
playerScore = currentGameState.Score2
opponentScore = currentGameState.Score1
}
playerScorePrinted := fmt.Sprintf("%d", playerScore)
opponentScorePrinted := fmt.Sprintf("%d", opponentScore)
newGameText := "\n\n\n" + playerScorePrinted + strings.Repeat(" ", ((cardWidth-len(playerScorePrinted))-len(opponentScorePrinted))-2) + opponentScorePrinted
newGameText += "\n\nYou" + strings.Repeat(" ", cardWidth-8) + "Opp"
gameText.SetText(newGameText)
}
func toggleFocus() {
if inputFocused && inputField.GetText() != "" {
writeBuffer <- "msg " + inputField.GetText()
statusMessage(fmt.Sprintf("<%s> %s", "You", inputField.GetText()))
}
inputFocused = !inputFocused
inputField.SetText("")
focusUpdated()
}
func focusUpdated() {
if inputFocused {
app.SetFocus(inputField)
} else {
app.SetFocus(nil)
}
}
func statusMessage(message string) {
logMutex.Lock()
defer logMutex.Unlock()
var prefix string
if !wroteFirstLogMessage {
wroteFirstLogMessage = true
} else {
prefix = "\n"
}
if len(message) > 0 && message[0:1] != "<" {
message = "* " + message
}
statusBuf.Write([]byte(prefix + time.Now().Format(logFormat) + " " + message))
app.Draw()
}
func main() {
cview.Styles.PrimaryTextColor = tcell.ColorDefault
cview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
var connectAddress string
flag.BoolVar(&debug, "debug", false, "print debug information")
flag.StringVar(&connectAddress, "server", "wss://play.cribbage.world/crib", "Server address")
flag.Parse()
app = cview.NewApplication().EnableMouse(true)
inputConfig = cbind.NewConfiguration()
inputConfig.SetKey(tcell.ModNone, tcell.KeyEnter, func(ev *tcell.EventKey) *tcell.EventKey {
toggleFocus()
return nil
})
inputConfig.SetRune(tcell.ModNone, ' ', func(ev *tcell.EventKey) *tcell.EventKey {
if inputFocused {
return ev
}
doAction()
return nil
})
for i := 1; i <= 6; i++ {
i := i // Capture
inputConfig.SetRune(tcell.ModNone, '0'+rune(i), func(ev *tcell.EventKey) *tcell.EventKey {
if inputFocused {
return ev
}
selectCardIndex(i)
app.Draw()
return nil
})
}
app.SetInputCapture(inputConfig.Capture)
starterWidget = newCardWidget(joker.Card{}, nil)
mainHandStack = newCardStackWidget()
mainCribStack = newCardStackWidget()
throwStack = newCardStackWidget()
oppCribStack = newCardStackWidget()
mainHandStack.EverSelectable = true
mainCribStack.EverSelectable = true
mainHandStack.SetMaxSelection(2)
mainCribStack.SetMaxSelection(-1)
throwStack.SetMaxSelection(-1)
oppCribStack.SetMaxSelection(-1)
g := cview.NewGrid().
SetColumns(cardWidth, 1, (cardBufferX*5)+cardWidth, 2, -1).
SetRows(cardHeight, 1, 1, cardHeight+1, 2, -1)
gameText = cview.NewTextView()
gameText.SetTextAlign(cview.AlignCenter)
statusGrid = cview.NewGrid().
SetColumns(1, -1, 1).
SetRows(1, 1, -1)
statusGrid.AddItem(cview.NewTextView(), 0, 0, 3, 1, 0, 0, false)
statusGrid.AddItem(cview.NewTextView(), 0, 1, 1, 2, 0, 0, false)
statusGrid.AddItem(cview.NewTextView(), 1, 0, 1, 1, 0, 0, false)
statusGrid.AddItem(gameText, 2, 1, 1, 1, 0, 0, false)
statusText = newStatusTextView()
inputField = cview.NewInputField().
SetText("").
SetLabel("> ").
SetFieldWidth(0).
SetFieldBackgroundColor(tcell.ColorDefault).
SetFieldTextColor(tcell.ColorDefault)
statusBuf = cview.NewTextView().SetWrap(true).SetWordWrap(true)
setupThrowGrid()
g.AddItem(starterWidget, 0, 0, 1, 1, 0, 0, false)
g.AddItem(cview.NewTextView(), 0, 1, 1, 1, 0, 0, false)
g.AddItem(throwGrid, 0, 2, 1, 3, 0, 0, false)
g.AddItem(cview.NewTextView(), 1, 0, 1, 5, 0, 0, false)
g.AddItem(statusText, 2, 0, 1, 5, 0, 0, false)
g.AddItem(statusGrid, 3, 0, 1, 1, 0, 0, false)
g.AddItem(cview.NewTextView(), 3, 1, 1, 1, 0, 0, false)
g.AddItem(mainHandStack, 3, 2, 1, 1, 0, 0, false)
g.AddItem(cview.NewTextView(), 3, 3, 1, 1, 0, 0, false)
g.AddItem(mainCribStack, 3, 4, 1, 1, 0, 0, false)
g.AddItem(inputField, 4, 0, 1, 5, 0, 0, false)
g.AddItem(statusBuf, 5, 0, 1, 5, 0, 0, false)
app.SetRoot(g, true)
app.SetBeforeFocusFunc(func(p cview.Primitive) bool {
return p == nil || p == inputField
})
focusUpdated()
statusMessage(defaultStatusText)
go connect(connectAddress)
go func() {
if err := app.Run(); err != nil {
log.Fatalf("failed to run application: %s", err)
}
exit <- true
}()
<-exit
}