diff --git a/Makefile b/Makefile index 0dbd2dc..619981e 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ SWAG ?= ~/go/bin/swag TARGET := $(shell find . -type f -name '*.go') +DEST := server/docs all: swagger docker install @@ -17,7 +18,7 @@ game: $(TARGET) swagger: $(SWAG) fmt - $(SWAG) init -o backend/docs -g cmd/serve/backend.go -pdl 1 + $(SWAG) init -o $(DEST) -g cmd/serve/backend.go -pdl 1 clean: -rm -f game diff --git a/backend/handlers/api/getLobbyRooms.go b/backend/handlers/api/getLobbyRooms.go deleted file mode 100644 index fa9f912..0000000 --- a/backend/handlers/api/getLobbyRooms.go +++ /dev/null @@ -1,29 +0,0 @@ -package api - -import ( - "net/http" - - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" - "github.com/uptrace/bunrouter" -) - -// GetLobbyRooms -// -// @Router /api/lobby/rooms [get] -func (self *Handlers) GetLobbyRooms( - w http.ResponseWriter, - req bunrouter.Request, -) error { - ctx := req.Context() - - rooms, err := self.db.GetRooms(ctx) - if err != nil { - return middlewares.HTTPError{ - StatusCode: http.StatusInternalServerError, - Message: "failed to get room", - OriginError: err, - } - } - - return bunrouter.JSON(w, rooms) -} diff --git a/cmd/play/root.go b/cmd/play/root.go index 3e06188..26b5cfc 100644 --- a/cmd/play/root.go +++ b/cmd/play/root.go @@ -21,8 +21,8 @@ var RootCmd = &cobra.Command{ SetDisableWarn(true) queue := []*tea.Program{} - queue = append(queue, - tea.NewProgram(plays.NewLanding(plays.NewBase(client)))) + queue = append(queue, tea.NewProgram( + plays.NewLanding(plays.NewBase(client)))) for len(queue) > 0 { program := queue[0] @@ -41,4 +41,5 @@ var RootCmd = &cobra.Command{ } func init() { + RootCmd.AddCommand(clientCmd) } diff --git a/cmd/serve/backend.go b/cmd/serve/backend.go index 0ee1d24..cd45f7c 100644 --- a/cmd/serve/backend.go +++ b/cmd/serve/backend.go @@ -16,20 +16,20 @@ import ( "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.uber.org/zap" - _ "gitea.konchin.com/ytshih/inp2025/game/backend/docs" - "gitea.konchin.com/ytshih/inp2025/game/backend/handlers/api" - "gitea.konchin.com/ytshih/inp2025/game/backend/handlers/auth" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "gitea.konchin.com/ytshih/inp2025/game/implements" + "gitea.konchin.com/ytshih/inp2025/game/server/backend/api" + "gitea.konchin.com/ytshih/inp2025/game/server/backend/auth" + _ "gitea.konchin.com/ytshih/inp2025/game/server/docs" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" "gitea.konchin.com/ytshih/inp2025/game/tracing" "gitea.konchin.com/ytshih/inp2025/game/utils" ) -// @title Intro. to Network Programming Game -// @version 0.0.1-alpha -// @license.name 0BSD -// @securityDefinitions.basic BasicAuth -// @BasePath / +// @title Intro. to Network Programming Game +// @version 0.0.1-alpha +// @license.name 0BSD +// @securityDefinitions.basic BasicAuth +// @BasePath / var backendCmd = &cobra.Command{ Use: "backend", Short: "Game backend", diff --git a/docker-compose.yaml b/docker-compose.yaml index 619921f..4eacbb3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,7 +3,7 @@ services: backend: build: context: . - env_file: ./backend/.env + env_file: ./server/.env ports: - 8081:8080 restart: unless-stopped diff --git a/go.mod b/go.mod index 79ed547..84563b4 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect diff --git a/go.sum b/go.sum index 50fca37..1795271 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/implements/bunDatabase.go b/implements/bunDatabase.go index 4132e50..22e6c54 100644 --- a/implements/bunDatabase.go +++ b/implements/bunDatabase.go @@ -38,16 +38,6 @@ func (self *BunDatabase) GetUserStatuses( return ret, err } -func (self *BunDatabase) GetRooms( - ctx context.Context, -) ([]models.Room, error) { - var ret []models.Room - err := self.db.NewSelect(). - Model(&ret). - Scan(ctx) - return ret, err -} - func (self *BunDatabase) InsertUser( ctx context.Context, user models.User, @@ -81,16 +71,6 @@ func (self *BunDatabase) InsertUserStatus( return err } -func (self *BunDatabase) InsertRoom( - ctx context.Context, - room models.Room, -) error { - _, err := self.db.NewInsert(). - Model(&room). - Exec(ctx) - return err -} - func (self *BunDatabase) DeleteUserStatus( ctx context.Context, username string, @@ -101,14 +81,3 @@ func (self *BunDatabase) DeleteUserStatus( Exec(ctx) return err } - -func (self *BunDatabase) DeleteRoom( - ctx context.Context, - roomId int, -) error { - _, err := self.db.NewDelete(). - Model(&models.Room{Id: roomId}). - WherePK(). - Exec(ctx) - return err -} diff --git a/interfaces/database.go b/interfaces/database.go index 5576fc1..93178c5 100644 --- a/interfaces/database.go +++ b/interfaces/database.go @@ -16,10 +16,6 @@ type Database interface { ctx context.Context, ) ([]models.UserStatus, error) - GetRooms( - ctx context.Context, - ) ([]models.Room, error) - InsertUser( ctx context.Context, user models.User, @@ -30,18 +26,8 @@ type Database interface { userStatus models.UserStatus, ) error - InsertRoom( - ctx context.Context, - room models.Room, - ) error - DeleteUserStatus( ctx context.Context, username string, ) error - - DeleteRoom( - ctx context.Context, - roomId int, - ) error } diff --git a/models/room.go b/models/room.go deleted file mode 100644 index b96683f..0000000 --- a/models/room.go +++ /dev/null @@ -1,47 +0,0 @@ -package models - -import ( - "fmt" - - "github.com/uptrace/bun" -) - -type RoomType int - -const ( - RoomTypeWordle RoomType = iota -) - -var ( - RoomTypeStr = []string{"Wordle"} -) - -type RoomStatus int - -const ( - RoomStatusWaiting RoomStatus = iota - RoomStatusPlaying - RoomStatusEnded -) - -var ( - RoomStatusStr = []string{"Waiting", "Playing", "Ended"} -) - -type Room struct { - bun.BaseModel `bun:"table:room"` - - Id int `bun:"id,pk,autoincrement"` - Creater string `bun:"creater"` - Type RoomType `bun:"type"` - Status RoomStatus `bun:"status"` -} - -func (self Room) View() string { - return fmt.Sprintf("%2d. %10s %10s [%s]", - self.Id, - self.Creater, - RoomTypeStr[self.Type], - RoomStatusStr[self.Status], - ) -} diff --git a/plays/game.go b/plays/game.go new file mode 100644 index 0000000..96e4dfe --- /dev/null +++ b/plays/game.go @@ -0,0 +1,77 @@ +package plays + +import ( + "strings" + + "gitea.konchin.com/ytshih/inp2025/game/types" + tea "github.com/charmbracelet/bubbletea" +) + +type Game struct { + *Base + state types.WordleState + input string +} + +func NewGame(base *Base) *Game { + m := Game{ + Base: base, + state: types.NewWordleState(), + input: "", + } + + return &m +} + +func (m *Game) Init() tea.Cmd { + return tea.Batch(tea.ClearScreen) +} + +func (m *Game) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if _, cmd := m.Base.Update(msg); cmd != nil { + return m, cmd + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyBackspace: + if len(m.input) > 0 { + m.input = m.input[:len(m.input)-1] + } + case tea.KeyEnter: + if len(m.input) == types.MaxLength { + return m, tea.Quit + } + case tea.KeyRunes: + if len(m.input) < types.MaxLength { + m.input = m.input + string(msg.Runes) + } + } + } + + return m, nil +} + +func (m *Game) View() string { + var b strings.Builder + + for _, s := range m.history { + b.WriteString(s.View()) + b.WriteRune('\n') + } + + b.WriteString(m.input) + return b.String() +} + +func (m *Game) Next(queue *[]*tea.Program) error { + if len(m.input) == types.MaxLength { + resp, err := m.Base.client.R(). + Post("/api/guess") + + *queue = append(*queue, + tea.NewProgram(NewGame(m.Base))) + } + return nil +} diff --git a/plays/lobby.go b/plays/lobby.go index 5a9367e..edfcf68 100644 --- a/plays/lobby.go +++ b/plays/lobby.go @@ -25,7 +25,7 @@ type Lobby struct { updateCh chan struct{} users []models.UserStatus - rooms []models.Room + rooms []types.Room } func NewLobby(base *Base) *Lobby { @@ -58,7 +58,8 @@ func updateLobbyInfo(m *Lobby) error { } m.users = users - var rooms []models.Room + var rooms []types.Room + // TODO: scan rooms _, err = m.Base.client.R(). SetResult(&rooms). ForceContentType("application/json"). diff --git a/backend/handlers/api/getLobbyUsers.go b/server/backend/api/getLobbyUsers.go similarity index 89% rename from backend/handlers/api/getLobbyUsers.go rename to server/backend/api/getLobbyUsers.go index 7824076..e75c1a7 100644 --- a/backend/handlers/api/getLobbyUsers.go +++ b/server/backend/api/getLobbyUsers.go @@ -3,7 +3,7 @@ package api import ( "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" "github.com/uptrace/bunrouter" ) diff --git a/backend/handlers/api/getUser.go b/server/backend/api/getUser.go similarity index 94% rename from backend/handlers/api/getUser.go rename to server/backend/api/getUser.go index 5dfa98d..1e8bd99 100644 --- a/backend/handlers/api/getUser.go +++ b/server/backend/api/getUser.go @@ -4,7 +4,7 @@ import ( "database/sql" "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" "github.com/uptrace/bunrouter" ) diff --git a/backend/handlers/api/handlers.go b/server/backend/api/handlers.go similarity index 100% rename from backend/handlers/api/handlers.go rename to server/backend/api/handlers.go diff --git a/backend/handlers/auth/getLogin.go b/server/backend/auth/getLogin.go similarity index 92% rename from backend/handlers/auth/getLogin.go rename to server/backend/auth/getLogin.go index 425db1c..65d146f 100644 --- a/backend/handlers/auth/getLogin.go +++ b/server/backend/auth/getLogin.go @@ -3,8 +3,8 @@ package auth import ( "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "gitea.konchin.com/ytshih/inp2025/game/models" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" "gitea.konchin.com/ytshih/inp2025/game/types" "gitea.konchin.com/ytshih/inp2025/game/utils" "github.com/uptrace/bunrouter" diff --git a/backend/handlers/auth/handlers.go b/server/backend/auth/handlers.go similarity index 100% rename from backend/handlers/auth/handlers.go rename to server/backend/auth/handlers.go diff --git a/backend/handlers/auth/postLogout.go b/server/backend/auth/postLogout.go similarity index 93% rename from backend/handlers/auth/postLogout.go rename to server/backend/auth/postLogout.go index ed0d3e0..42f22ca 100644 --- a/backend/handlers/auth/postLogout.go +++ b/server/backend/auth/postLogout.go @@ -3,8 +3,8 @@ package auth import ( "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "gitea.konchin.com/ytshih/inp2025/game/models" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" "gitea.konchin.com/ytshih/inp2025/game/types" "gitea.konchin.com/ytshih/inp2025/game/utils" "github.com/uptrace/bunrouter" diff --git a/backend/handlers/auth/postRegister.go b/server/backend/auth/postRegister.go similarity index 95% rename from backend/handlers/auth/postRegister.go rename to server/backend/auth/postRegister.go index 7206fca..b83cd6a 100644 --- a/backend/handlers/auth/postRegister.go +++ b/server/backend/auth/postRegister.go @@ -5,8 +5,8 @@ import ( "io" "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "gitea.konchin.com/ytshih/inp2025/game/models" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" "gitea.konchin.com/ytshih/inp2025/game/types" "gitea.konchin.com/ytshih/inp2025/game/utils" "github.com/uptrace/bunrouter" diff --git a/backend/docs/docs.go b/server/docs/docs.go similarity index 96% rename from backend/docs/docs.go rename to server/docs/docs.go index f546d4f..d85611e 100644 --- a/backend/docs/docs.go +++ b/server/docs/docs.go @@ -18,11 +18,6 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/lobby/rooms": { - "get": { - "responses": {} - } - }, "/api/lobby/users": { "get": { "responses": {} diff --git a/backend/docs/swagger.json b/server/docs/swagger.json similarity index 95% rename from backend/docs/swagger.json rename to server/docs/swagger.json index 11669d1..5f85f63 100644 --- a/backend/docs/swagger.json +++ b/server/docs/swagger.json @@ -10,11 +10,6 @@ }, "basePath": "/", "paths": { - "/api/lobby/rooms": { - "get": { - "responses": {} - } - }, "/api/lobby/users": { "get": { "responses": {} diff --git a/backend/docs/swagger.yaml b/server/docs/swagger.yaml similarity index 95% rename from backend/docs/swagger.yaml rename to server/docs/swagger.yaml index e7815b6..6af4478 100644 --- a/backend/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -14,9 +14,6 @@ info: title: Intro. to Network Programming Game version: 0.0.1-alpha paths: - /api/lobby/rooms: - get: - responses: {} /api/lobby/users: get: responses: {} diff --git a/backend/middlewares/accessLog.go b/server/middlewares/accessLog.go similarity index 100% rename from backend/middlewares/accessLog.go rename to server/middlewares/accessLog.go diff --git a/backend/middlewares/auth.go b/server/middlewares/auth.go similarity index 100% rename from backend/middlewares/auth.go rename to server/middlewares/auth.go diff --git a/backend/middlewares/bunrouterOtel.go b/server/middlewares/bunrouterOtel.go similarity index 100% rename from backend/middlewares/bunrouterOtel.go rename to server/middlewares/bunrouterOtel.go diff --git a/backend/middlewares/errorHandler.go b/server/middlewares/errorHandler.go similarity index 100% rename from backend/middlewares/errorHandler.go rename to server/middlewares/errorHandler.go diff --git a/backend/middlewares/handler.go b/server/middlewares/handler.go similarity index 100% rename from backend/middlewares/handler.go rename to server/middlewares/handler.go diff --git a/server/wordle/handlers.go b/server/wordle/handlers.go new file mode 100644 index 0000000..29121d7 --- /dev/null +++ b/server/wordle/handlers.go @@ -0,0 +1,29 @@ +package wordle + +import "gitea.konchin.com/ytshih/inp2025/game/types" + +type Operation struct { +} + +type Handlers struct { + state WordleState + opCh chan Operation + subs []chan types.WordleState +} + +func NewHandlers() *Handlers { + ret := &Handlers{subs: nil} + go ret.GameFlow() + return ret +} + +func (self *Handlers) GameFlow() { + for { + select { + case op := <-self.opCh: + if opCh.Type { + + } + } + } +} diff --git a/server/wordle/wsGetState.go b/server/wordle/wsGetState.go new file mode 100644 index 0000000..2cf6ed7 --- /dev/null +++ b/server/wordle/wsGetState.go @@ -0,0 +1,66 @@ +package wordle + +import ( + "net/http" + + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" + "gitea.konchin.com/ytshih/inp2025/game/types" + "gitea.konchin.com/ytshih/inp2025/game/utils" + "github.com/gorilla/websocket" + "github.com/uptrace/bunrouter" + "github.com/vmihailenco/msgpack/v5" +) + +func gameFlow( + dataCh chan types.WordleState, + retCh chan error, +) { + // TODO +} + +func (self *handlers) WSGetState( + w http.ResponseWriter, + req bunrouter.Request, +) error { + ctx := req.Context() + + c, err := upgrader.Upgrade(w, req, nil) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to upgrade websocket", + OriginError: err, + } + } + defer c.Close() + + dataCh := make(chan types.WordleState) + retCh := make(chan error) + + go gameFlow(dataCh, retCh) + for { + select { + case err := <-retCh: + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "wordle server error", + OriginError: err, + } + } + break + case data := <-dataCh: + b, err := msgpack.Marshal(data) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to marshal data into msgpack", + OriginError: err, + } + } + c.WriteMessage(websocket.BinaryMessage, b) + } + } + + return utils.Success(w) +} diff --git a/types/game.go b/types/game.go new file mode 100644 index 0000000..c9c63f4 --- /dev/null +++ b/types/game.go @@ -0,0 +1,27 @@ +package types + +import "fmt" + +type RoomStatus int + +const ( + RoomStatusWaiting RoomStatus = iota + RoomStatusPlaying + RoomStatusEnded +) + +var ( + RoomStatusStr = []string{"Waiting", "Playing", "Ended"} +) + +type Room struct { + Creater string + Status RoomStatus +} + +func (self Room) View() string { + return fmt.Sprintf("%10s [%s]", + self.Creater, + RoomStatusStr[self.Status], + ) +} diff --git a/types/wordle.go b/types/wordle.go new file mode 100644 index 0000000..1cfd36d --- /dev/null +++ b/types/wordle.go @@ -0,0 +1,81 @@ +package types + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +type CharStatus int + +const ( + CharStatusA CharStatus = iota + CharStatusB + CharStatusC +) + +type Char struct { + Char rune `msgpack:"char"` + Status CharStatus `msgpack:"status"` +} + +func NewChar() Char { + return Char{ + Char: 0x0, + Status: CharStatusC, + } +} + +func (self Char) View() string { + style := lipgloss.NewStyle() + + switch self.Status { + case CharStatusA: + style = style.Background(lipgloss.Color("#00ff00")) + case CharStatusB: + style = style.Background(lipgloss.Color("#ffff00")) + case CharStatusC: + style = style.Background(lipgloss.Color("#808080")) + } + + return style.Render(string(self.Char)) +} + +type Word struct { + Chars []Char `msgpack:"chars"` +} + +func NewWord() Word { + return Word{ + Chars: nil, + } +} + +func (self Word) View() string { + var b strings.Builder + + for _, char := range self.Chars { + b.WriteString(char.View()) + b.WriteRune('\n') + } + + return b.String() +} + +const ( + MaxLength = 5 +) + +type UserState struct { + History []Word `msgpack:"history"` +} + +type WordleState struct { + Users []UserState `msgpack:"users"` +} + +func NewWordleState() WordleState { + return WordleState{ + Users: nil, + } +} diff --git a/utils/initDB.go b/utils/initDB.go index 3bf9609..a0199fc 100644 --- a/utils/initDB.go +++ b/utils/initDB.go @@ -11,6 +11,5 @@ func InitDB(ctx context.Context, db *bun.DB) error { return db.ResetModel(ctx, (*models.User)(nil), (*models.UserStatus)(nil), - (*models.Room)(nil), ) } diff --git a/workflows/wordleServer.go b/workflows/wordleServer.go new file mode 100644 index 0000000..a5e8698 --- /dev/null +++ b/workflows/wordleServer.go @@ -0,0 +1,32 @@ +package workflows + +import ( + "context" + "log" + "net/http" + + "gitea.konchin.com/ytshih/inp2025/game/implements" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" + "gitea.konchin.com/ytshih/inp2025/game/server/wordle" + "github.com/spf13/viper" + "github.com/uptrace/bunrouter" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +func WordleServer(ctx context.Context) { + handlers := wordle.NewHandlers() + middlewareHandlers := middlewares.NewHandlers( + implements.NewBunDatabase(nil)) + + router := bunrouter.New() + + apiGroup := router.NewGroup("/api"). + Use(middlewareHandlers.ErrorHandler) + apiGroup.GET("/state", + handlers.WSGetState) + apiGroup.POST("/guess", + handlers.WSPostGuess) + + log.Println(http.ListenAndServe(":"+viper.GetString("port"), + otelhttp.NewHandler(router, ""))) +}