Fix: various bug fix

This commit is contained in:
2025-10-16 07:12:01 +08:00
parent 5bbab63a2c
commit 0dea850cfa
16 changed files with 185 additions and 56 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
game game
inp2025
logs/ logs/

View File

@@ -1,4 +1,4 @@
.PHONY: all clean .PHONY: all clean play
GO_ARGS += CGO_ENABLED=0 GO_ARGS += CGO_ENABLED=0
@@ -12,3 +12,6 @@ $(TARGET): $(SOURCE)
clean: clean:
-rm -f $(TARGET) -rm -f $(TARGET)
play:
go run . player

View File

@@ -2,10 +2,5 @@
## TODO ## TODO
- Lobby
- Login count
- Logout and duplicate login
- Connection - Connection
- Scan port - Scan port
- Game
- Game end reason

View File

@@ -6,11 +6,14 @@ import (
"gitea.konchin.com/ytshih/inp2025/middlewares" "gitea.konchin.com/ytshih/inp2025/middlewares"
"gitea.konchin.com/ytshih/inp2025/models" "gitea.konchin.com/ytshih/inp2025/models"
"gitea.konchin.com/ytshih/inp2025/types" "gitea.konchin.com/ytshih/inp2025/types"
"gitea.konchin.com/ytshih/inp2025/utils"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"
) )
type PostLoginOutput struct {
LoginCount int `msgpack:"loginCount"`
}
func (self *Handlers) PostLogin( func (self *Handlers) PostLogin(
w http.ResponseWriter, w http.ResponseWriter,
req bunrouter.Request, req bunrouter.Request,
@@ -25,11 +28,12 @@ func (self *Handlers) PostLogin(
} }
res, err := self.db.NewUpdate(). res, err := self.db.NewUpdate().
Model((*models.User)(nil)). Model(&user).
Set("login_count = login_count + ?", 1). Set("login_count = login_count + ?", 1).
Set("is_logged = ?", true). Set("is_logged = ?", true).
Where("is_logged = ?", false). Where("is_logged = ?", false).
Where("username = ?", user.Username). Where("username = ?", user.Username).
Returning("*").
Exec(ctx) Exec(ctx)
if err != nil { if err != nil {
return middlewares.HTTPError{ return middlewares.HTTPError{
@@ -46,8 +50,6 @@ func (self *Handlers) PostLogin(
OriginError: err, OriginError: err,
} }
} }
// debug
return utils.Success(w)
if cnt == 0 { if cnt == 0 {
return middlewares.HTTPError{ return middlewares.HTTPError{
StatusCode: http.StatusUnauthorized, StatusCode: http.StatusUnauthorized,
@@ -55,5 +57,7 @@ func (self *Handlers) PostLogin(
} }
} }
} }
return utils.Success(w) return bunrouter.JSON(w, PostLoginOutput{
LoginCount: user.LoginCount,
})
} }

View File

@@ -24,6 +24,7 @@ func (self *Handlers) PostLogout(
} }
_, err := self.db.NewUpdate(). _, err := self.db.NewUpdate().
Model((*models.User)(nil)).
Set("is_logged = ?", false). Set("is_logged = ?", false).
Where("username = ?", user.Username). Where("username = ?", user.Username).
Exec(ctx) Exec(ctx)

View File

@@ -11,6 +11,11 @@ import (
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"
) )
type PostRegisterInput struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (self *Handlers) PostRegister( func (self *Handlers) PostRegister(
w http.ResponseWriter, w http.ResponseWriter,
req bunrouter.Request, req bunrouter.Request,
@@ -26,15 +31,20 @@ func (self *Handlers) PostRegister(
} }
} }
var user models.User var input PostRegisterInput
if err := json.Unmarshal(b, &user); err != nil { if err := json.Unmarshal(b, &input); err != nil {
return middlewares.HTTPError{ return middlewares.HTTPError{
StatusCode: http.StatusBadRequest, StatusCode: http.StatusBadRequest,
Message: "failed to unmarshal json into user", Message: "failed to unmarshal json",
OriginError: err, OriginError: err,
} }
} }
user := models.User{
Username: input.Username,
Password: input.Password,
LoginCount: 0,
}
res, err := self.db.NewInsert(). res, err := self.db.NewInsert().
Model(&user). Model(&user).
On("CONFLICT (username) DO NOTHING"). On("CONFLICT (username) DO NOTHING").

View File

@@ -15,7 +15,6 @@ func (self *Handlers) GetState(
w http.ResponseWriter, w http.ResponseWriter,
req bunrouter.Request, req bunrouter.Request,
) error { ) error {
// fmt.Fprintf(os.Stderr, "GET /api/state\n")
c, err := self.upgrader.Upgrade(w, req.Request, nil) c, err := self.upgrader.Upgrade(w, req.Request, nil)
if err != nil { if err != nil {
return middlewares.HTTPError{ return middlewares.HTTPError{

View File

@@ -22,15 +22,14 @@ func (op *OperationSubs) Run(self *Handlers) error {
if !ok { if !ok {
self.state.History[op.Username] = [types.GUESS_COUNT_LIMIT]types.Guess{} self.state.History[op.Username] = [types.GUESS_COUNT_LIMIT]types.Guess{}
} }
// fmt.Fprintf(os.Stderr, "[DEBUG] %+v\n", self.state.History)
self.subs = append(self.subs, op.SubsCh) self.subs = append(self.subs, op.SubsCh)
return nil return nil
} }
type OperationGuess struct { type OperationGuess struct {
Username string `msgpack:"username"` Username string
Guess string `msgpack:"guess"` Guess string
} }
func (op *OperationGuess) Run(self *Handlers) error { func (op *OperationGuess) Run(self *Handlers) error {
@@ -57,6 +56,13 @@ func (op *OperationGuess) Run(self *Handlers) error {
return nil return nil
} }
type OperationEnd struct{}
func (op *OperationEnd) Run(self *Handlers) error {
self.state.GameEnd = true
return nil
}
type Handlers struct { type Handlers struct {
ans string ans string
state types.WordleState state types.WordleState
@@ -90,7 +96,6 @@ func (self *Handlers) GameFlow() {
panic(fmt.Errorf("game flow operation failed, %w", err)) panic(fmt.Errorf("game flow operation failed, %w", err))
} }
// fmt.Fprintf(os.Stderr, "[DEBUG] %+v\n", self.state.History)
for _, ch := range self.subs { for _, ch := range self.subs {
*ch <- self.state *ch <- self.state
} }

View File

@@ -0,0 +1,16 @@
package wordle
import (
"net/http"
"gitea.konchin.com/ytshih/inp2025/utils"
"github.com/uptrace/bunrouter"
)
func (self *Handlers) PostEnd(
w http.ResponseWriter,
req bunrouter.Request,
) error {
self.opCh <- &OperationEnd{}
return utils.Success(w)
}

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"gitea.konchin.com/ytshih/inp2025/stages" "gitea.konchin.com/ytshih/inp2025/stages"
"gitea.konchin.com/ytshih/inp2025/types"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -10,17 +11,16 @@ import (
var playerCmd = &cobra.Command{ var playerCmd = &cobra.Command{
Use: "player", Use: "player",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
queue := []*tea.Program{} base := stages.NewBaseModel(viper.GetString("auth-endpoint"))
p := tea.NewProgram(stages.NewLandingModel(base))
base.Push(types.Program{
Run: func() error { _, err := p.Run(); return err },
Stage: types.StageLanding,
})
base := stages.NewBaseModel(&queue, viper.GetString("auth-endpoint")) for base.Size() > 0 {
queue = append(queue, p := base.Pop()
tea.NewProgram(stages.NewLandingModel(base))) if err := p.Run(); err != nil {
for len(queue) > 0 {
program := queue[0]
queue = queue[1:]
_, err := program.Run()
if err != nil {
panic(err) panic(err)
} }
} }

View File

@@ -1,23 +1,52 @@
package stages package stages
import ( import (
tea "github.com/charmbracelet/bubbletea" "gitea.konchin.com/ytshih/inp2025/types"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
) )
type BaseModel struct { type BaseModel struct {
queue *[]*tea.Program stack []types.Program
client *resty.Client client *resty.Client
// after login
loginCount int
} }
func NewBaseModel( func NewBaseModel(
queue *[]*tea.Program,
endpoint string, endpoint string,
) *BaseModel { ) *BaseModel {
return &BaseModel{ return &BaseModel{
queue: queue, stack: []types.Program{},
client: resty.New(). client: resty.New().
SetBaseURL(endpoint). SetBaseURL(endpoint).
SetDisableWarn(true), SetDisableWarn(true),
} }
} }
func (m *BaseModel) Size() int {
return len(m.stack)
}
func (m *BaseModel) Pop() types.Program {
stack := m.stack[:]
var ret types.Program
ret, stack = stack[len(stack)-1], stack[:len(stack)-1]
m.stack = stack
return ret
}
func (m *BaseModel) Push(program types.Program) error {
stack := m.stack[:]
for len(stack) > 0 && stack[len(stack)-1].Stage > program.Stage {
var p types.Program
p, stack = stack[len(stack)-1], stack[:len(stack)-1]
if err := p.Run(); err != nil {
return err
}
}
stack = append(stack, program)
m.stack = stack
return nil
}

View File

@@ -5,9 +5,11 @@ import (
"net/http" "net/http"
"strings" "strings"
"gitea.konchin.com/ytshih/inp2025/models" "gitea.konchin.com/ytshih/inp2025/handlers/auth"
"gitea.konchin.com/ytshih/inp2025/types"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/viper"
) )
type landingOperationType int type landingOperationType int
@@ -73,14 +75,18 @@ type postLoginMsg struct{}
func (m *LandingModel) postLogin() tea.Cmd { func (m *LandingModel) postLogin() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
resp, err := m.BaseModel.client.R(). var res auth.PostLoginOutput
resp, err := m.client.R().
SetResult(&res).
ForceContentType("application/json").
SetBasicAuth(m.username.Value(), m.password.Value()). SetBasicAuth(m.username.Value(), m.password.Value()).
Post("/auth/login") Post("/auth/login")
if err == nil { if err == nil {
switch resp.StatusCode() { switch resp.StatusCode() {
case http.StatusOK: case http.StatusOK:
m.BaseModel.client.SetBasicAuth( m.client.SetBasicAuth(
m.username.Value(), m.password.Value()) m.username.Value(), m.password.Value())
m.loginCount = res.LoginCount
m.info = "login success.\n" m.info = "login success.\n"
m.err = nil m.err = nil
case http.StatusUnauthorized: case http.StatusUnauthorized:
@@ -101,8 +107,8 @@ type postRegisterMsg struct{}
func (m *LandingModel) postRegister() tea.Cmd { func (m *LandingModel) postRegister() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
resp, err := m.BaseModel.client.R(). resp, err := m.client.R().
SetBody(models.User{ SetBody(auth.PostRegisterInput{
Username: m.username.Value(), Username: m.username.Value(),
Password: m.password.Value(), Password: m.password.Value(),
}). }).
@@ -193,8 +199,22 @@ func (m *LandingModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
case postLoginMsg: case postLoginMsg:
if m.err == nil { if m.err == nil {
*m.queue = append(*m.queue, m.Push(types.Program{
tea.NewProgram(NewLobbyModel(m.BaseModel))) Run: func() error {
m.client.SetBaseURL(viper.GetString("auth-endpoint"))
_, err := m.client.R().
Post("/auth/logout")
return err
},
Stage: types.StageLanding,
})
program := tea.NewProgram(NewLobbyModel(m.BaseModel))
m.Push(types.Program{
Run: func() error { _, err := program.Run(); return err },
Stage: types.StageLobby,
})
return m, tea.Quit return m, tea.Quit
} else { } else {
m.reset() m.reset()

View File

@@ -172,6 +172,7 @@ func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c": case "ctrl+c":
m.shutdown()
return m, tea.Quit return m, tea.Quit
} }
} }
@@ -207,10 +208,25 @@ func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case serverSendReplyMsg: case serverSendReplyMsg:
if m.err == nil { if m.err == nil {
m.shutdown() m.shutdown()
m.client.SetBaseURL("http://" + m.remote) m.client.SetBaseURL("http://" + m.remote)
shutdown := workflows.WordleServer(m.listener) shutdown := workflows.WordleServer(m.listener)
*m.queue = append(*m.queue, m.Push(types.Program{
tea.NewProgram(NewWordleClientModel(m.BaseModel, shutdown))) Run: func() error { shutdown(); return nil },
Stage: types.StageGaming,
})
m.Push(types.Program{
Run: func() error { m.client.R().Post("/api/end"); return nil },
Stage: types.StageGaming,
})
p := tea.NewProgram(NewWordleClientModel(m.BaseModel))
m.Push(types.Program{
Run: func() error { _, err := p.Run(); return err },
Stage: types.StageGaming,
})
return m, tea.Quit return m, tea.Quit
} else { } else {
m.op = lobbyOperationServerWaiting m.op = lobbyOperationServerWaiting
@@ -229,17 +245,29 @@ func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cursor = (m.cursor + 1) % n m.cursor = (m.cursor + 1) % n
} }
case "enter": case "enter":
m.remote = m.endpoints[m.cursor] if n := len(m.endpoints); n > 0 && m.cursor < n {
cmds = append(cmds, m.clientJoin()) m.remote = m.endpoints[m.cursor]
cmds = append(cmds, m.clientJoin())
}
} }
case clientScanMsg: case clientScanMsg:
cmds = append(cmds, m.clientScan()) cmds = append(cmds, m.clientScan())
case clientJoinMsg: case clientJoinMsg:
if m.err == nil && m.remote != "" { if m.err == nil && m.remote != "" {
m.shutdown() m.shutdown()
m.client.SetBaseURL("http://" + m.remote) m.client.SetBaseURL("http://" + m.remote)
*m.BaseModel.queue = append(*m.BaseModel.queue, m.Push(types.Program{
tea.NewProgram(NewWordleClientModel(m.BaseModel, func() {}))) Run: func() error { m.client.R().Post("/api/end"); return nil },
Stage: types.StageGaming,
})
p := tea.NewProgram(NewWordleClientModel(m.BaseModel))
m.Push(types.Program{
Run: func() error { _, err := p.Run(); return err },
Stage: types.StageGaming,
})
return m, tea.Quit return m, tea.Quit
} }
} }
@@ -251,13 +279,16 @@ func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *LobbyModel) View() string { func (m *LobbyModel) View() string {
var b strings.Builder var b strings.Builder
fmt.Fprintf(&b, "Login as '%s' for '%d' times\n",
m.client.UserInfo.Username, m.loginCount)
switch m.op { switch m.op {
case lobbyOperationChoose: case lobbyOperationChoose:
b.WriteString("Choose Role\n(S)erver / (C)lient\n") b.WriteString("Choose Role\n(S)erver / (C)lient\n")
case lobbyOperationServerWaiting: case lobbyOperationServerWaiting:
b.WriteString("Wait for user to join...\n") b.WriteString("Wait for user to join...\n")
case lobbyOperationServerChoose: case lobbyOperationServerChoose:
fmt.Fprintf(&b, "Receive Join Request by '%s'\nAccept (Y)/N\n", fmt.Fprintf(&b, "Receive Join Request by '%s'\nAccept (Y) / (n)\n",
m.remoteUser) m.remoteUser)
case lobbyOperationClientScannning: case lobbyOperationClientScannning:
b.WriteString("Scanning server...\nChoose one to join\n") b.WriteString("Scanning server...\nChoose one to join\n")

View File

@@ -22,9 +22,8 @@ const (
type WordleClientModel struct { type WordleClientModel struct {
*BaseModel *BaseModel
conn *websocket.Conn conn *websocket.Conn
state types.WordleState state types.WordleState
shutdown types.ShutdownFunc
input textinput.Model input textinput.Model
err error err error
@@ -32,7 +31,6 @@ type WordleClientModel struct {
func NewWordleClientModel( func NewWordleClientModel(
base *BaseModel, base *BaseModel,
shutdown types.ShutdownFunc,
) *WordleClientModel { ) *WordleClientModel {
input := textinput.New() input := textinput.New()
input.Focus() input.Focus()
@@ -41,7 +39,6 @@ func NewWordleClientModel(
return &WordleClientModel{ return &WordleClientModel{
BaseModel: base, BaseModel: base,
input: input, input: input,
shutdown: shutdown,
} }
} }
@@ -118,14 +115,15 @@ func (m *WordleClientModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "ctrl+c": case "ctrl+c":
m.conn.Close() m.conn.Close()
m.shutdown()
return m, tea.Quit return m, tea.Quit
case "enter": case "enter":
if m.state.GameEnd { if m.state.GameEnd {
m.conn.Close() m.conn.Close()
m.shutdown() p := tea.NewProgram(NewLobbyModel(m.BaseModel))
*m.queue = append(*m.queue, m.Push(types.Program{
tea.NewProgram(NewLobbyModel(m.BaseModel))) Run: func() error { _, err := p.Run(); return err },
Stage: types.StageLobby,
})
return m, tea.Quit return m, tea.Quit
} }
if len(m.input.Value()) == types.GUESS_WORD_LENGTH { if len(m.input.Value()) == types.GUESS_WORD_LENGTH {

View File

@@ -2,4 +2,19 @@ package types
type ShutdownFunc func() type ShutdownFunc func()
type RunFunc func() error
type StageType int
const (
StageLanding StageType = iota
StageLobby
StageGaming
)
type Program struct {
Run RunFunc
Stage StageType
}
type UsernameType = string type UsernameType = string

View File

@@ -64,6 +64,8 @@ func WordleServer(listener net.Listener) types.ShutdownFunc {
wordleHandlers.GetState) wordleHandlers.GetState)
apiGroup.POST("/guess", apiGroup.POST("/guess",
wordleHandlers.PostGuess) wordleHandlers.PostGuess)
apiGroup.POST("/end",
wordleHandlers.PostEnd)
server := &http.Server{ server := &http.Server{
Handler: http.Handler(router), Handler: http.Handler(router),