Use internal on-screen keyboard

This commit is contained in:
Trevor Slocum 2024-06-25 11:57:42 -07:00
parent d5f1cd773a
commit 88bcaf480a
8 changed files with 159 additions and 172 deletions

View file

@ -1,3 +1,6 @@
1.3.5:
- Use internal on-screen keyboard
1.3.4:
- Add rematch button
- Fix resetting password

View file

@ -118,8 +118,8 @@ type board struct {
menuGrid *etk.Grid
changePasswordOld *etk.Input
changePasswordNew *etk.Input
changePasswordOld *Input
changePasswordNew *Input
changePasswordGrid *etk.Grid
highlightCheckbox *etk.Checkbox
@ -308,10 +308,10 @@ func NewBoard() *board {
}
oldLabel.SetVertical(etk.AlignCenter)
b.changePasswordOld = etk.NewInput("", func(text string) (handled bool) {
b.changePasswordOld = &Input{etk.NewInput("", func(text string) (handled bool) {
b.selectChangePassword()
return false
})
})}
b.changePasswordOld.SetBackground(frameColor)
centerInput(b.changePasswordOld)
@ -324,10 +324,10 @@ func NewBoard() *board {
}
newLabel.SetVertical(etk.AlignCenter)
b.changePasswordNew = etk.NewInput("", func(text string) (handled bool) {
b.changePasswordNew = &Input{etk.NewInput("", func(text string) (handled bool) {
b.selectChangePassword()
return false
})
})}
b.changePasswordNew.SetBackground(frameColor)
centerInput(b.changePasswordNew)

View file

@ -25,6 +25,7 @@ import (
"code.rocket9labs.com/tslocum/bgammon-bei-bot/bot"
"code.rocket9labs.com/tslocum/bgammon/pkg/server"
"code.rocket9labs.com/tslocum/etk"
"code.rocket9labs.com/tslocum/etk/kibodo"
"code.rocket9labs.com/tslocum/tabula"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
@ -40,7 +41,7 @@ import (
)
const (
version = "v1.3.4p1"
version = "v1.3.5"
baseButtonHeight = 54
MaxDebug = 2
DefaultServerAddress = "wss://ws.bgammon.org"
@ -95,7 +96,7 @@ const (
minHeight = 240
)
const (
var (
extraSmallFontSize = 14
smallFontSize = 20
mediumFontSize = 24
@ -111,7 +112,7 @@ var (
var (
statusBuffer *etk.Text
gameBuffer *etk.Text
inputBuffer *etk.Input
inputBuffer *Input
statusLogged bool
gameLogged bool
@ -135,6 +136,7 @@ var (
historyContainer *etk.Grid
listGamesContainer *etk.Grid
displayFrame *etk.Frame
connectFrame *etk.Frame
createGameFrame *etk.Frame
joinGameFrame *etk.Frame
@ -403,6 +405,11 @@ func cubeImage(value int8) *ebiten.Image {
}
func setViewBoard(view bool) {
if view != viewBoard {
go hideKeyboard()
inputBuffer.SetText("")
}
var refreshLobby bool
if !view && viewBoard != view {
refreshLobby = true
@ -428,7 +435,7 @@ func setViewBoard(view bool) {
game.lobby.createGamePassword.SetText("")
game.lobby.rebuildButtonsGrid()
etk.SetRoot(game.Board.frame)
game.setRoot(game.Board.frame)
etk.SetFocus(inputBuffer)
game.Board.uiGrid.SetRect(game.Board.uiGrid.Rect())
@ -530,9 +537,8 @@ type Game struct {
lobby *lobby
keyboardHint *etk.Text
keyboardHintVisible bool
keyboardHintDismissed bool
keyboard *etk.Keyboard
keyboardFrame *etk.Frame
volume float64 // Volume range is 0-1.
@ -542,15 +548,15 @@ type Game struct {
cpuProfile *os.File
connectUsername *etk.Input
connectPassword *etk.Input
connectServer *etk.Input
connectUsername *Input
connectPassword *Input
connectServer *Input
registerEmail *etk.Input
registerUsername *etk.Input
registerPassword *etk.Input
registerEmail *Input
registerUsername *Input
registerPassword *Input
resetEmail *etk.Input
resetEmail *Input
resetInfo *etk.Text
resetInProgress bool
@ -609,6 +615,9 @@ func NewGame() *Game {
ebiten.SetWindowClosingHandled(true)
g := &Game{
keyboard: etk.NewKeyboard(),
keyboardFrame: etk.NewFrame(),
runeBuffer: make([]rune, 24),
tutorialFrame: etk.NewFrame(),
@ -618,6 +627,13 @@ func NewGame() *Game {
Mutex: &sync.Mutex{},
}
g.keyboard.SetScheduleFrameFunc(scheduleFrame)
g.keyboard.SetKeys(kibodo.KeysMobileQWERTY)
g.keyboard.SetExtendedKeys(kibodo.KeysMobileSymbols)
if !AutoEnableTouchInput {
g.keyboard.SetVisible(false)
}
g.keyboardFrame.AddChild(g.keyboard)
g.savedUsername, g.savedPassword = loadCredentials()
g.tutorialFrame.SetPositionChildren(true)
game = g
@ -629,20 +645,27 @@ func (g *Game) initialize() {
loadAudioAssets()
loadImageAssets(0)
fnt, err := opentype.Parse(fonts.MPlus1pRegular_ttf)
if err != nil {
log.Fatal(err)
}
etk.Style.TextFont = fnt
etk.Style.TextSize = largeFontSize
if AutoEnableTouchInput {
etk.Bindings.ConfirmRune = 199
etk.Bindings.BackRune = 231
etk.Style.BorderSize /= 2
extraSmallFontSize /= 2
smallFontSize /= 2
mediumFontSize /= 2
mediumLargeFontSize /= 2
largeFontSize /= 2
}
fnt, err := opentype.Parse(fonts.MPlus1pRegular_ttf)
if err != nil {
log.Fatal(err)
}
etk.Style.TextFont = fnt
etk.Style.TextSize = largeFontSize
etk.Style.TextColorLight = triangleA
etk.Style.TextColorDark = triangleA
etk.Style.InputBgColor = color.RGBA{40, 24, 9, 255}
@ -661,7 +684,7 @@ func (g *Game) initialize() {
statusBuffer = etk.NewText("")
gameBuffer = etk.NewText("")
inputBuffer = etk.NewInput("", acceptInput)
inputBuffer = &Input{etk.NewInput("", acceptInput)}
statusBuffer.SetForeground(bufferTextColor)
statusBuffer.SetBackground(bufferBackgroundColor)
@ -678,6 +701,9 @@ func (g *Game) initialize() {
fieldHeight = etk.Scale(32)
}
displayFrame = etk.NewFrame()
displayFrame.SetPositionChildren(true)
g.Board = NewBoard()
g.lobby = NewLobby()
@ -694,10 +720,10 @@ func (g *Game) initialize() {
if connectAddress == "" {
connectAddress = DefaultServerAddress
}
g.connectServer = etk.NewInput(connectAddress, func(text string) (handled bool) {
g.connectServer = &Input{etk.NewInput(connectAddress, func(text string) (handled bool) {
g.selectConnect()
return false
})
})}
{
headerLabel := newCenteredText(gotext.Get("Register"))
@ -706,22 +732,22 @@ func (g *Game) initialize() {
passwordLabel := newCenteredText(gotext.Get("Password"))
serverLabel := newCenteredText(gotext.Get("Server"))
g.registerEmail = etk.NewInput("", func(text string) (handled bool) {
g.registerEmail = &Input{etk.NewInput("", func(text string) (handled bool) {
g.selectConfirmRegister()
return false
})
})}
centerInput(g.registerEmail)
g.registerUsername = etk.NewInput("", func(text string) (handled bool) {
g.registerUsername = &Input{etk.NewInput("", func(text string) (handled bool) {
g.selectConfirmRegister()
return false
})
})}
centerInput(g.registerUsername)
g.registerPassword = etk.NewInput("", func(text string) (handled bool) {
g.registerPassword = &Input{etk.NewInput("", func(text string) (handled bool) {
g.selectConfirmRegister()
return false
})
})}
centerInput(g.registerPassword)
g.registerPassword.SetMask('*')
@ -777,10 +803,10 @@ func (g *Game) initialize() {
emailLabel := newCenteredText(gotext.Get("Email"))
serverLabel := newCenteredText(gotext.Get("Server"))
g.resetEmail = etk.NewInput("", func(text string) (handled bool) {
g.resetEmail = &Input{etk.NewInput("", func(text string) (handled bool) {
g.selectConfirmReset()
return false
})
})}
centerInput(g.resetEmail)
cancelButton := etk.NewButton(gotext.Get("Cancel"), func() error {
@ -842,16 +868,16 @@ func (g *Game) initialize() {
footerLabel.SetHorizontal(etk.AlignEnd)
footerLabel.SetVertical(etk.AlignEnd)
g.connectUsername = etk.NewInput("", func(text string) (handled bool) {
g.connectUsername = &Input{etk.NewInput("", func(text string) (handled bool) {
g.selectConnect()
return false
})
})}
centerInput(g.connectUsername)
g.connectPassword = etk.NewInput("", func(text string) (handled bool) {
g.connectPassword = &Input{etk.NewInput("", func(text string) (handled bool) {
g.selectConnect()
return false
})
})}
centerInput(g.connectPassword)
g.connectPassword.SetMask('*')
@ -917,22 +943,22 @@ func (g *Game) initialize() {
passwordLabel := newCenteredText(gotext.Get("Password"))
variantLabel := newCenteredText(gotext.Get("Variant"))
g.lobby.createGameName = etk.NewInput("", func(text string) (handled bool) {
g.lobby.createGameName = &Input{etk.NewInput("", func(text string) (handled bool) {
g.lobby.confirmCreateGame()
return false
})
})}
centerInput(g.lobby.createGameName)
g.lobby.createGamePoints = etk.NewInput("", func(text string) (handled bool) {
g.lobby.createGamePoints = &Input{etk.NewInput("", func(text string) (handled bool) {
g.lobby.confirmCreateGame()
return false
})
})}
centerInput(g.lobby.createGamePoints)
g.lobby.createGamePassword = etk.NewInput("", func(text string) (handled bool) {
g.lobby.createGamePassword = &Input{etk.NewInput("", func(text string) (handled bool) {
g.lobby.confirmCreateGame()
return false
})
})}
centerInput(g.lobby.createGamePassword)
g.lobby.createGamePassword.SetMask('*')
@ -1022,10 +1048,10 @@ func (g *Game) initialize() {
passwordLabel := newCenteredText(gotext.Get("Password"))
g.lobby.joinGamePassword = etk.NewInput("", func(text string) (handled bool) {
g.lobby.joinGamePassword = &Input{etk.NewInput("", func(text string) (handled bool) {
g.lobby.confirmJoinGame()
return false
})
})}
centerInput(g.lobby.joinGamePassword)
g.lobby.joinGamePassword.SetMask('*')
@ -1074,10 +1100,10 @@ func (g *Game) initialize() {
opponentLabel.SetFont(etk.Style.TextFont, etk.Scale(mediumFontSize))
}
g.lobby.historyUsername = etk.NewInput("", func(text string) (handled bool) {
g.lobby.historyUsername = &Input{etk.NewInput("", func(text string) (handled bool) {
g.selectHistorySearch()
return false
})
})}
centerInput(g.lobby.historyUsername)
g.lobby.historyUsername.SetScrollBarVisible(false)
@ -1211,12 +1237,6 @@ func (g *Game) initialize() {
listGamesFrame.AddChild(g.tutorialFrame)
}
if AutoEnableTouchInput {
g.keyboardHint = etk.NewText(gotext.Get("Press back to toggle the keyboard.") + "\n\n" + gotext.Get("Long press back to exit the application.") + "\n\n" + gotext.Get("Tap to dismiss this pop-up."))
g.keyboardHint.SetHorizontal(etk.AlignCenter)
g.keyboardHint.SetVertical(etk.AlignCenter)
}
statusBuffer.SetScrollBarColors(etk.Style.ScrollAreaColor, etk.Style.ScrollHandleColor)
gameBuffer.SetScrollBarColors(etk.Style.ScrollAreaColor, etk.Style.ScrollHandleColor)
inputBuffer.SetScrollBarColors(etk.Style.ScrollAreaColor, etk.Style.ScrollHandleColor)
@ -1249,11 +1269,12 @@ func (g *Game) initialize() {
go g.handleAutoRefresh()
go g.handleUpdateTimeLabels()
etk.SetRoot(displayFrame)
scheduleFrame()
}
func (g *Game) playOffline() {
hideKeyboard()
go hideKeyboard()
if g.loggedIn {
return
}
@ -1330,7 +1351,8 @@ func (g *Game) setRoot(w etk.Widget) {
if w != g.Board.frame {
g.rootWidget = w
}
etk.SetRoot(w)
displayFrame.Clear()
displayFrame.AddChild(w, g.keyboardFrame)
}
func (g *Game) setBufferRects() {
@ -2231,7 +2253,7 @@ func (g *Game) selectCancel() error {
}
func (g *Game) selectConfirmRegister() error {
hideKeyboard()
go hideKeyboard()
g.Email = g.registerEmail.Text()
g.Username = g.registerUsername.Text()
g.Password = g.registerPassword.Text()
@ -2244,7 +2266,7 @@ func (g *Game) selectConfirmRegister() error {
}
func (g *Game) selectConfirmReset() error {
hideKeyboard()
go hideKeyboard()
if g.resetInProgress {
return nil
}
@ -2266,7 +2288,7 @@ func (g *Game) selectConfirmReset() error {
}
func (g *Game) selectConnect() error {
hideKeyboard()
go hideKeyboard()
g.Username = g.connectUsername.Text()
g.Password = g.connectPassword.Text()
if ShowServerSettings {
@ -2277,7 +2299,7 @@ func (g *Game) selectConnect() error {
}
func (g *Game) searchMatches(username string) {
hideKeyboard()
go hideKeyboard()
loadingText := newCenteredText(gotext.Get("Loading..."))
if AutoEnableTouchInput {
loadingText.SetFont(etk.Style.TextFont, etk.Scale(mediumFontSize))
@ -2290,7 +2312,7 @@ func (g *Game) searchMatches(username string) {
}
func (g *Game) selectHistory() error {
hideKeyboard()
go hideKeyboard()
g.lobby.showHistory = true
g.setRoot(historyFrame)
g.lobby.historyUsername.SetText(g.Client.Username)
@ -2301,7 +2323,7 @@ func (g *Game) selectHistory() error {
}
func (g *Game) selectHistorySearch() error {
hideKeyboard()
go hideKeyboard()
username := g.lobby.historyUsername.Text()
if strings.TrimSpace(username) == "" {
return nil
@ -2311,7 +2333,7 @@ func (g *Game) selectHistorySearch() error {
}
func (g *Game) selectHistoryPrevious() error {
hideKeyboard()
go hideKeyboard()
if g.lobby.historyUsername.Text() == "" || g.lobby.historyPage == 1 {
return nil
}
@ -2320,7 +2342,7 @@ func (g *Game) selectHistoryPrevious() error {
}
func (g *Game) selectHistoryNext() error {
hideKeyboard()
go hideKeyboard()
if g.lobby.historyUsername.Text() == "" || g.lobby.historyPage == g.lobby.historyPages {
return nil
}
@ -2464,28 +2486,6 @@ func (g *Game) handleInput(keys []ebiten.Key) error {
return nil
}
func (g *Game) handleTouch(p image.Point) bool {
if g.keyboardHintDismissed || (p.X == 0 && p.Y == 0) {
return false
}
if g.keyboardHintVisible && p.X >= 0 && p.X < g.screenW && p.Y >= 0 && p.Y < g.screenH/3 {
g.keyboardHintVisible = false
g.keyboardHintDismissed = true
return true
}
w := etk.At(p)
if w == nil {
return false
}
switch w.(type) {
case *etk.Input:
showKeyboard()
}
return false
}
// Update is called by Ebitengine only when user input occurs, or a frame is
// explicitly scheduled.
func (g *Game) Update() error {
@ -2552,16 +2552,8 @@ func (g *Game) Update() error {
}
// Handle touch input.
g.touchIDs = inpututil.AppendJustPressedTouchIDs(g.touchIDs[:0])
for _, id := range g.touchIDs {
tx, ty := ebiten.TouchPosition(id)
if tx != 0 || ty != 0 {
cx, cy = tx, ty
if g.handleTouch(image.Point{cx, cy}) {
return nil
}
break
}
if len(ebiten.AppendTouchIDs(g.touchIDs[:0])) != 0 {
scheduleFrame()
}
// Handle physical keyboard.
@ -2578,10 +2570,6 @@ func (g *Game) Update() error {
}
}
if len(ebiten.AppendTouchIDs(g.touchIDs[:0])) != 0 {
scheduleFrame()
}
err = etk.Update()
if err != nil {
return err
@ -2619,17 +2607,6 @@ func (g *Game) Update() error {
return nil
}
func (g *Game) drawKeyboardHint(screen *ebiten.Image) {
if !g.keyboardHintVisible {
return
}
r := image.Rect(0, 0, g.screenW, g.screenH/3)
screen.SubImage(r).(*ebiten.Image).Fill(hintColor)
screen.SubImage(image.Rect(0, g.screenH/3, g.screenW, g.screenH/3+4)).(*ebiten.Image).Fill(borderColor)
g.keyboardHint.SetRect(r)
g.keyboardHint.Draw(screen)
}
func (g *Game) Draw(screen *ebiten.Image) {
g.Lock()
defer g.Unlock()
@ -2664,7 +2641,6 @@ func (g *Game) Draw(screen *ebiten.Image) {
if err != nil {
log.Fatal(err)
}
g.drawKeyboardHint(screen)
return
}
@ -2677,8 +2653,6 @@ func (g *Game) Draw(screen *ebiten.Image) {
log.Fatal(err)
}
g.drawKeyboardHint(screen)
if Debug > 0 {
g.drawBuffer.Reset()
@ -2838,6 +2812,8 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
g.Board.updateOpponentLabel()
g.Board.updatePlayerLabel()
g.keyboard.SetRect(image.Rect(0, game.screenH-game.screenH/3, game.screenW, game.screenH))
if g.LoadReplay != nil {
go g.HandleReplay(g.LoadReplay)
g.LoadReplay = nil
@ -2875,7 +2851,7 @@ func acceptInput(text string) (handled bool) {
}
game.Client.Out <- []byte(text)
hideKeyboard()
go hideKeyboard()
return true
}
@ -3053,6 +3029,17 @@ func LoadLocale(forceLanguage *language.Tag) error {
return nil
}
type Input struct {
*etk.Input
}
func (i *Input) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
if clicked {
go showKeyboard()
}
return i.Input.HandleMouse(cursor, pressed, clicked)
}
type ClickableText struct {
*etk.Text
onSelected func()
@ -3071,7 +3058,7 @@ func newCenteredText(text string) *etk.Text {
return t
}
func centerInput(input *etk.Input) {
func centerInput(input *Input) {
input.SetVertical(etk.AlignCenter)
input.SetPadding(etk.Scale(5))
}

View file

@ -121,16 +121,11 @@ func saveReplay(id int, content []byte) error {
}
func showKeyboard() {
if keyboardConn == nil {
game.keyboardHintVisible = true
return
}
keyboardConn.Write([]byte("1\n"))
game.keyboard.SetVisible(true)
scheduleFrame()
}
func hideKeyboard() {
if keyboardConn == nil {
return
}
keyboardConn.Write([]byte("0\n"))
game.keyboard.SetVisible(false)
scheduleFrame()
}

View file

@ -81,7 +81,8 @@ func showKeyboard() {
if virtualKeyboard.IsUndefined() {
return
}
virtualKeyboard.Call("show")
game.keyboard.SetVisible(true)
scheduleFrame()
}
func hideKeyboard() {
@ -89,5 +90,6 @@ func hideKeyboard() {
if virtualKeyboard.IsUndefined() {
return
}
virtualKeyboard.Call("hide")
game.keyboard.SetVisible(false)
scheduleFrame()
}

View file

@ -62,22 +62,22 @@ type lobby struct {
refresh bool
showCreateGame bool
createGameName *etk.Input
createGamePoints *etk.Input
createGamePassword *etk.Input
createGameName *Input
createGamePoints *Input
createGamePassword *Input
createGameAceyCheckbox *etk.Checkbox
createGameTabulaCheckbox *etk.Checkbox
showJoinGame bool
joinGameID int
joinGameLabel *etk.Text
joinGamePassword *etk.Input
joinGamePassword *Input
showHistory bool
historySelected int
historyLastClick time.Time
historyMatches []*bgammon.HistoryMatch
historyUsername *etk.Input
historyUsername *Input
historyList *etk.List
historyPage int
historyPages int
@ -245,7 +245,7 @@ func (l *lobby) getButtons() []string {
}
func (l *lobby) confirmCreateGame() {
hideKeyboard()
go hideKeyboard()
typeAndPassword := "public"
if len(strings.TrimSpace(game.lobby.createGamePassword.Text())) > 0 {
typeAndPassword = fmt.Sprintf("private %s", strings.ReplaceAll(game.lobby.createGamePassword.Text(), " ", "_"))
@ -264,7 +264,7 @@ func (l *lobby) confirmCreateGame() {
}
func (l *lobby) confirmJoinGame() {
hideKeyboard()
go hideKeyboard()
l.c.Out <- []byte(fmt.Sprintf("j %d %s", l.joinGameID, l.joinGamePassword.Text()))
}

24
go.mod
View file

@ -1,18 +1,18 @@
module code.rocket9labs.com/tslocum/boxcars
go 1.17
go 1.19
require (
code.rocket9labs.com/tslocum/bgammon v0.0.0-20240423015129-50b743a46875
code.rocket9labs.com/tslocum/bgammon v0.0.0-20240625060716-8acfc83de66a
code.rocket9labs.com/tslocum/bgammon-bei-bot v0.0.0-20240422204629-93fba875ad23
code.rocket9labs.com/tslocum/etk v0.0.0-20240424172123-500cae845cf1
code.rocket9labs.com/tslocum/etk v0.0.0-20240625183029-399fae94ad9c
code.rocket9labs.com/tslocum/tabula v0.0.0-20240422202348-09cfc96fcfc9
github.com/hajimehoshi/ebiten/v2 v2.7.4
github.com/leonelquinteros/gotext v1.6.0
github.com/hajimehoshi/ebiten/v2 v2.7.5
github.com/leonelquinteros/gotext v1.6.1
github.com/llgcode/draw2d v0.0.0-20240322162412-ee6987bd01dc
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
golang.org/x/image v0.16.0
golang.org/x/text v0.15.0
golang.org/x/image v0.17.0
golang.org/x/text v0.16.0
nhooyr.io/websocket v1.8.11
)
@ -35,10 +35,10 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/jezek/xgb v1.1.1 // indirect
@ -55,8 +55,8 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/vanng822/css v1.0.1 // indirect
github.com/vanng822/go-premailer v1.21.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/sys v0.21.0 // indirect
)

44
go.sum
View file

@ -1,11 +1,11 @@
code.rocket9labs.com/tslocum/bei v0.0.0-20240108012722-6db380cc190b h1:Y0a14Kf/hSYepSmp4ZfDeE4CZZGBGBS97CNjCbKJm0c=
code.rocket9labs.com/tslocum/bei v0.0.0-20240108012722-6db380cc190b/go.mod h1:tS60/VNAJphKvDBkSLQhKALa15msIAuWWfEKNc4oFZc=
code.rocket9labs.com/tslocum/bgammon v0.0.0-20240423015129-50b743a46875 h1:EpSeUWSKOQC2e7o/sT9GmSE6yqFt99VnK/zhkw6qtoI=
code.rocket9labs.com/tslocum/bgammon v0.0.0-20240423015129-50b743a46875/go.mod h1:5wus4g00koVGEPM++Udr+LVAY2kxeNtGHZVDNt5A7vc=
code.rocket9labs.com/tslocum/bgammon v0.0.0-20240625060716-8acfc83de66a h1:OoI6IGfVhgQ1RZ+XtG1mbuPXjxBvZeAO7svUe2uifZU=
code.rocket9labs.com/tslocum/bgammon v0.0.0-20240625060716-8acfc83de66a/go.mod h1:5oAuNVvHL0McMjg2P0/7e215vdJSEqMDWRSdsDUNCow=
code.rocket9labs.com/tslocum/bgammon-bei-bot v0.0.0-20240422204629-93fba875ad23 h1:Gv98n8QExq/cqWIIeSoGCvvcC/SzT6Gg5UrpiUvxFf0=
code.rocket9labs.com/tslocum/bgammon-bei-bot v0.0.0-20240422204629-93fba875ad23/go.mod h1:fJ8Yj32Gs0v2jBKhWKHB0oWKZuMZ9gGBbjFITanMN8E=
code.rocket9labs.com/tslocum/etk v0.0.0-20240424172123-500cae845cf1 h1:RV7ABOlELpcJespOPAKU2HXow2MadpCraOThEf8eQP4=
code.rocket9labs.com/tslocum/etk v0.0.0-20240424172123-500cae845cf1/go.mod h1:ge3tHt0qwsw0UfLQd0s3JxC1YJui/39vt5ds9Ndj9Hs=
code.rocket9labs.com/tslocum/etk v0.0.0-20240625183029-399fae94ad9c h1:zIyk8BXf46kD6ABfwc91zrFZ8nYwNl7Pa5H7CIG955U=
code.rocket9labs.com/tslocum/etk v0.0.0-20240625183029-399fae94ad9c/go.mod h1:InNHK22sJo9XHAUTFIfueLBP92s8VFj7Ni1zj2MAm8M=
code.rocket9labs.com/tslocum/tabula v0.0.0-20240422202348-09cfc96fcfc9 h1:9wtELMiDrOEsgRPcumaSkdVOBGxr1/KAcUii/rmpwmU=
code.rocket9labs.com/tslocum/tabula v0.0.0-20240422202348-09cfc96fcfc9/go.mod h1:WEJXESKXqrMFLAArikQ79lpRibNeeE1C0VruxXYMF5M=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@ -56,18 +56,18 @@ github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/hajimehoshi/ebiten/v2 v2.7.5 h1:jN6FnhCd9NGYCsm5GtrweuikrlyVGCSUpH5YgL+7UKA=
github.com/hajimehoshi/ebiten/v2 v2.7.5/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
@ -85,8 +85,8 @@ github.com/jlouis/glicko2 v1.0.0/go.mod h1:5dzlxjhVPPLk+wiUwwF2oVyDwsNXMgnw7WrLR
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leonelquinteros/gotext v1.6.0 h1:IYL2+dKsaYYvqGAOafaC7mpAGBhMrD/vKjHUGyp8V64=
github.com/leonelquinteros/gotext v1.6.0/go.mod h1:qQRISjoonXYFdRGrTG1LARQ38Gpibad0IPeB4hpvyyM=
github.com/leonelquinteros/gotext v1.6.1 h1:PuTN8YUqHvfPZxW+fPXp7o0Fc2zN9L2wXBZrqT5MO5A=
github.com/leonelquinteros/gotext v1.6.1/go.mod h1:qQRISjoonXYFdRGrTG1LARQ38Gpibad0IPeB4hpvyyM=
github.com/llgcode/draw2d v0.0.0-20240322162412-ee6987bd01dc h1:lorg2FaIDdlahOHekjnQjItP2oCtkVlYc0QNekubCLk=
github.com/llgcode/draw2d v0.0.0-20240322162412-ee6987bd01dc/go.mod h1:muweRyJCZ1mZSMiCgYbAicfnwZFoeHpNr6A6QBu+rBg=
github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e h1:ZAvbj5hI/G/EbAYAcj4yCXUNiFKefEhH0qfImDDD0/8=
@ -137,10 +137,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco=
golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -153,8 +153,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -173,8 +173,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -190,8 +190,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=