diff --git a/.gitignore b/.gitignore index 711f784..52a77d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ game +inp2025 logs/ diff --git a/Makefile b/Makefile index 21daff4..1af6c12 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean +.PHONY: all clean play GO_ARGS += CGO_ENABLED=0 @@ -12,3 +12,6 @@ $(TARGET): $(SOURCE) clean: -rm -f $(TARGET) + +play: + go run . player diff --git a/README.md b/README.md index b0ae54d..edcf42a 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,5 @@ ## TODO -- Lobby - - Login count - - Logout and duplicate login - Connection - Scan port -- Game - - Game end reason diff --git a/handlers/auth/postLogin.go b/handlers/auth/postLogin.go index 6211200..de1b293 100644 --- a/handlers/auth/postLogin.go +++ b/handlers/auth/postLogin.go @@ -6,11 +6,14 @@ import ( "gitea.konchin.com/ytshih/inp2025/middlewares" "gitea.konchin.com/ytshih/inp2025/models" "gitea.konchin.com/ytshih/inp2025/types" - "gitea.konchin.com/ytshih/inp2025/utils" "github.com/uptrace/bunrouter" ) +type PostLoginOutput struct { + LoginCount int `msgpack:"loginCount"` +} + func (self *Handlers) PostLogin( w http.ResponseWriter, req bunrouter.Request, @@ -25,11 +28,12 @@ func (self *Handlers) PostLogin( } res, err := self.db.NewUpdate(). - Model((*models.User)(nil)). + Model(&user). Set("login_count = login_count + ?", 1). Set("is_logged = ?", true). Where("is_logged = ?", false). Where("username = ?", user.Username). + Returning("*"). Exec(ctx) if err != nil { return middlewares.HTTPError{ @@ -46,8 +50,6 @@ func (self *Handlers) PostLogin( OriginError: err, } } - // debug - return utils.Success(w) if cnt == 0 { return middlewares.HTTPError{ StatusCode: http.StatusUnauthorized, @@ -55,5 +57,7 @@ func (self *Handlers) PostLogin( } } } - return utils.Success(w) + return bunrouter.JSON(w, PostLoginOutput{ + LoginCount: user.LoginCount, + }) } diff --git a/handlers/auth/postLogout.go b/handlers/auth/postLogout.go index 1137c00..3e49ec0 100644 --- a/handlers/auth/postLogout.go +++ b/handlers/auth/postLogout.go @@ -24,6 +24,7 @@ func (self *Handlers) PostLogout( } _, err := self.db.NewUpdate(). + Model((*models.User)(nil)). Set("is_logged = ?", false). Where("username = ?", user.Username). Exec(ctx) diff --git a/handlers/auth/postRegister.go b/handlers/auth/postRegister.go index 898b3ae..f771f44 100644 --- a/handlers/auth/postRegister.go +++ b/handlers/auth/postRegister.go @@ -11,6 +11,11 @@ import ( "github.com/uptrace/bunrouter" ) +type PostRegisterInput struct { + Username string `json:"username"` + Password string `json:"password"` +} + func (self *Handlers) PostRegister( w http.ResponseWriter, req bunrouter.Request, @@ -26,15 +31,20 @@ func (self *Handlers) PostRegister( } } - var user models.User - if err := json.Unmarshal(b, &user); err != nil { + var input PostRegisterInput + if err := json.Unmarshal(b, &input); err != nil { return middlewares.HTTPError{ StatusCode: http.StatusBadRequest, - Message: "failed to unmarshal json into user", + Message: "failed to unmarshal json", OriginError: err, } } + user := models.User{ + Username: input.Username, + Password: input.Password, + LoginCount: 0, + } res, err := self.db.NewInsert(). Model(&user). On("CONFLICT (username) DO NOTHING"). diff --git a/handlers/wordle/getState.go b/handlers/wordle/getState.go index 1ea5b75..3f100b7 100644 --- a/handlers/wordle/getState.go +++ b/handlers/wordle/getState.go @@ -15,7 +15,6 @@ func (self *Handlers) GetState( w http.ResponseWriter, req bunrouter.Request, ) error { - // fmt.Fprintf(os.Stderr, "GET /api/state\n") c, err := self.upgrader.Upgrade(w, req.Request, nil) if err != nil { return middlewares.HTTPError{ diff --git a/handlers/wordle/handlers.go b/handlers/wordle/handlers.go index 8c1952a..2b6d630 100644 --- a/handlers/wordle/handlers.go +++ b/handlers/wordle/handlers.go @@ -22,15 +22,14 @@ func (op *OperationSubs) Run(self *Handlers) error { if !ok { 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) return nil } type OperationGuess struct { - Username string `msgpack:"username"` - Guess string `msgpack:"guess"` + Username string + Guess string } func (op *OperationGuess) Run(self *Handlers) error { @@ -57,6 +56,13 @@ func (op *OperationGuess) Run(self *Handlers) error { return nil } +type OperationEnd struct{} + +func (op *OperationEnd) Run(self *Handlers) error { + self.state.GameEnd = true + return nil +} + type Handlers struct { ans string state types.WordleState @@ -90,7 +96,6 @@ func (self *Handlers) GameFlow() { panic(fmt.Errorf("game flow operation failed, %w", err)) } - // fmt.Fprintf(os.Stderr, "[DEBUG] %+v\n", self.state.History) for _, ch := range self.subs { *ch <- self.state } diff --git a/handlers/wordle/postEnd.go b/handlers/wordle/postEnd.go new file mode 100644 index 0000000..5aa8b75 --- /dev/null +++ b/handlers/wordle/postEnd.go @@ -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) +} diff --git a/player.go b/player.go index ef5bf62..c12952e 100644 --- a/player.go +++ b/player.go @@ -2,6 +2,7 @@ package main import ( "gitea.konchin.com/ytshih/inp2025/stages" + "gitea.konchin.com/ytshih/inp2025/types" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -10,17 +11,16 @@ import ( var playerCmd = &cobra.Command{ Use: "player", 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")) - queue = append(queue, - tea.NewProgram(stages.NewLandingModel(base))) - - for len(queue) > 0 { - program := queue[0] - queue = queue[1:] - _, err := program.Run() - if err != nil { + for base.Size() > 0 { + p := base.Pop() + if err := p.Run(); err != nil { panic(err) } } diff --git a/stages/base.go b/stages/base.go index 59a5199..7c88d81 100644 --- a/stages/base.go +++ b/stages/base.go @@ -1,23 +1,52 @@ package stages import ( - tea "github.com/charmbracelet/bubbletea" + "gitea.konchin.com/ytshih/inp2025/types" "github.com/go-resty/resty/v2" ) type BaseModel struct { - queue *[]*tea.Program + stack []types.Program client *resty.Client + + // after login + loginCount int } func NewBaseModel( - queue *[]*tea.Program, endpoint string, ) *BaseModel { return &BaseModel{ - queue: queue, + stack: []types.Program{}, client: resty.New(). SetBaseURL(endpoint). 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 +} diff --git a/stages/landing.go b/stages/landing.go index f2fc6e8..4b3092a 100644 --- a/stages/landing.go +++ b/stages/landing.go @@ -5,9 +5,11 @@ import ( "net/http" "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" tea "github.com/charmbracelet/bubbletea" + "github.com/spf13/viper" ) type landingOperationType int @@ -73,14 +75,18 @@ type postLoginMsg struct{} func (m *LandingModel) postLogin() tea.Cmd { 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()). Post("/auth/login") if err == nil { switch resp.StatusCode() { case http.StatusOK: - m.BaseModel.client.SetBasicAuth( + m.client.SetBasicAuth( m.username.Value(), m.password.Value()) + m.loginCount = res.LoginCount m.info = "login success.\n" m.err = nil case http.StatusUnauthorized: @@ -101,8 +107,8 @@ type postRegisterMsg struct{} func (m *LandingModel) postRegister() tea.Cmd { return func() tea.Msg { - resp, err := m.BaseModel.client.R(). - SetBody(models.User{ + resp, err := m.client.R(). + SetBody(auth.PostRegisterInput{ Username: m.username.Value(), Password: m.password.Value(), }). @@ -193,8 +199,22 @@ func (m *LandingModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case postLoginMsg: if m.err == nil { - *m.queue = append(*m.queue, - tea.NewProgram(NewLobbyModel(m.BaseModel))) + m.Push(types.Program{ + 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 } else { m.reset() diff --git a/stages/lobby.go b/stages/lobby.go index 08adaff..1304ada 100644 --- a/stages/lobby.go +++ b/stages/lobby.go @@ -172,6 +172,7 @@ func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": + m.shutdown() return m, tea.Quit } } @@ -207,10 +208,25 @@ func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case serverSendReplyMsg: if m.err == nil { m.shutdown() + m.client.SetBaseURL("http://" + m.remote) shutdown := workflows.WordleServer(m.listener) - *m.queue = append(*m.queue, - tea.NewProgram(NewWordleClientModel(m.BaseModel, shutdown))) + m.Push(types.Program{ + 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 } else { m.op = lobbyOperationServerWaiting @@ -229,17 +245,29 @@ func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cursor = (m.cursor + 1) % n } case "enter": - m.remote = m.endpoints[m.cursor] - cmds = append(cmds, m.clientJoin()) + if n := len(m.endpoints); n > 0 && m.cursor < n { + m.remote = m.endpoints[m.cursor] + cmds = append(cmds, m.clientJoin()) + } } case clientScanMsg: cmds = append(cmds, m.clientScan()) case clientJoinMsg: if m.err == nil && m.remote != "" { m.shutdown() + m.client.SetBaseURL("http://" + m.remote) - *m.BaseModel.queue = append(*m.BaseModel.queue, - tea.NewProgram(NewWordleClientModel(m.BaseModel, func() {}))) + 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 } } @@ -251,13 +279,16 @@ func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *LobbyModel) View() string { var b strings.Builder + fmt.Fprintf(&b, "Login as '%s' for '%d' times\n", + m.client.UserInfo.Username, m.loginCount) + switch m.op { case lobbyOperationChoose: b.WriteString("Choose Role\n(S)erver / (C)lient\n") case lobbyOperationServerWaiting: b.WriteString("Wait for user to join...\n") 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) case lobbyOperationClientScannning: b.WriteString("Scanning server...\nChoose one to join\n") diff --git a/stages/wordle.go b/stages/wordle.go index 02dff2a..a9e21e8 100644 --- a/stages/wordle.go +++ b/stages/wordle.go @@ -22,9 +22,8 @@ const ( type WordleClientModel struct { *BaseModel - conn *websocket.Conn - state types.WordleState - shutdown types.ShutdownFunc + conn *websocket.Conn + state types.WordleState input textinput.Model err error @@ -32,7 +31,6 @@ type WordleClientModel struct { func NewWordleClientModel( base *BaseModel, - shutdown types.ShutdownFunc, ) *WordleClientModel { input := textinput.New() input.Focus() @@ -41,7 +39,6 @@ func NewWordleClientModel( return &WordleClientModel{ BaseModel: base, input: input, - shutdown: shutdown, } } @@ -118,14 +115,15 @@ func (m *WordleClientModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": m.conn.Close() - m.shutdown() return m, tea.Quit case "enter": if m.state.GameEnd { m.conn.Close() - m.shutdown() - *m.queue = append(*m.queue, - tea.NewProgram(NewLobbyModel(m.BaseModel))) + p := tea.NewProgram(NewLobbyModel(m.BaseModel)) + m.Push(types.Program{ + Run: func() error { _, err := p.Run(); return err }, + Stage: types.StageLobby, + }) return m, tea.Quit } if len(m.input.Value()) == types.GUESS_WORD_LENGTH { diff --git a/types/types.go b/types/types.go index 6097cbe..a4223f4 100644 --- a/types/types.go +++ b/types/types.go @@ -2,4 +2,19 @@ package types 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 diff --git a/workflows/wordleServer.go b/workflows/wordleServer.go index fa3152a..7a38be4 100644 --- a/workflows/wordleServer.go +++ b/workflows/wordleServer.go @@ -64,6 +64,8 @@ func WordleServer(listener net.Listener) types.ShutdownFunc { wordleHandlers.GetState) apiGroup.POST("/guess", wordleHandlers.PostGuess) + apiGroup.POST("/end", + wordleHandlers.PostEnd) server := &http.Server{ Handler: http.Handler(router),