Draft: big refactor

This commit is contained in:
2025-09-16 16:03:27 +08:00
parent f527230f1e
commit 08c23bb29b
33 changed files with 339 additions and 155 deletions

View File

@@ -3,6 +3,7 @@
SWAG ?= ~/go/bin/swag SWAG ?= ~/go/bin/swag
TARGET := $(shell find . -type f -name '*.go') TARGET := $(shell find . -type f -name '*.go')
DEST := server/docs
all: swagger docker install all: swagger docker install
@@ -17,7 +18,7 @@ game: $(TARGET)
swagger: swagger:
$(SWAG) fmt $(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: clean:
-rm -f game -rm -f game

View File

@@ -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)
}

View File

@@ -21,8 +21,8 @@ var RootCmd = &cobra.Command{
SetDisableWarn(true) SetDisableWarn(true)
queue := []*tea.Program{} queue := []*tea.Program{}
queue = append(queue, queue = append(queue, tea.NewProgram(
tea.NewProgram(plays.NewLanding(plays.NewBase(client)))) plays.NewLanding(plays.NewBase(client))))
for len(queue) > 0 { for len(queue) > 0 {
program := queue[0] program := queue[0]
@@ -41,4 +41,5 @@ var RootCmd = &cobra.Command{
} }
func init() { func init() {
RootCmd.AddCommand(clientCmd)
} }

View File

@@ -16,11 +16,11 @@ import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.uber.org/zap" "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/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/tracing"
"gitea.konchin.com/ytshih/inp2025/game/utils" "gitea.konchin.com/ytshih/inp2025/game/utils"
) )

View File

@@ -3,7 +3,7 @@ services:
backend: backend:
build: build:
context: . context: .
env_file: ./backend/.env env_file: ./server/.env
ports: ports:
- 8081:8080 - 8081:8080
restart: unless-stopped restart: unless-stopped

1
go.mod
View File

@@ -44,6 +44,7 @@ require (
github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/google/uuid v1.6.0 // 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/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect

2
go.sum
View File

@@ -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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= 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= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=

View File

@@ -38,16 +38,6 @@ func (self *BunDatabase) GetUserStatuses(
return ret, err 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( func (self *BunDatabase) InsertUser(
ctx context.Context, ctx context.Context,
user models.User, user models.User,
@@ -81,16 +71,6 @@ func (self *BunDatabase) InsertUserStatus(
return err 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( func (self *BunDatabase) DeleteUserStatus(
ctx context.Context, ctx context.Context,
username string, username string,
@@ -101,14 +81,3 @@ func (self *BunDatabase) DeleteUserStatus(
Exec(ctx) Exec(ctx)
return err 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
}

View File

@@ -16,10 +16,6 @@ type Database interface {
ctx context.Context, ctx context.Context,
) ([]models.UserStatus, error) ) ([]models.UserStatus, error)
GetRooms(
ctx context.Context,
) ([]models.Room, error)
InsertUser( InsertUser(
ctx context.Context, ctx context.Context,
user models.User, user models.User,
@@ -30,18 +26,8 @@ type Database interface {
userStatus models.UserStatus, userStatus models.UserStatus,
) error ) error
InsertRoom(
ctx context.Context,
room models.Room,
) error
DeleteUserStatus( DeleteUserStatus(
ctx context.Context, ctx context.Context,
username string, username string,
) error ) error
DeleteRoom(
ctx context.Context,
roomId int,
) error
} }

View File

@@ -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],
)
}

77
plays/game.go Normal file
View File

@@ -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
}

View File

@@ -25,7 +25,7 @@ type Lobby struct {
updateCh chan struct{} updateCh chan struct{}
users []models.UserStatus users []models.UserStatus
rooms []models.Room rooms []types.Room
} }
func NewLobby(base *Base) *Lobby { func NewLobby(base *Base) *Lobby {
@@ -58,7 +58,8 @@ func updateLobbyInfo(m *Lobby) error {
} }
m.users = users m.users = users
var rooms []models.Room var rooms []types.Room
// TODO: scan rooms
_, err = m.Base.client.R(). _, err = m.Base.client.R().
SetResult(&rooms). SetResult(&rooms).
ForceContentType("application/json"). ForceContentType("application/json").

View File

@@ -3,7 +3,7 @@ package api
import ( import (
"net/http" "net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"
) )

View File

@@ -4,7 +4,7 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"
) )

View File

@@ -3,8 +3,8 @@ package auth
import ( import (
"net/http" "net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares"
"gitea.konchin.com/ytshih/inp2025/game/models" "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/types"
"gitea.konchin.com/ytshih/inp2025/game/utils" "gitea.konchin.com/ytshih/inp2025/game/utils"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"

View File

@@ -3,8 +3,8 @@ package auth
import ( import (
"net/http" "net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares"
"gitea.konchin.com/ytshih/inp2025/game/models" "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/types"
"gitea.konchin.com/ytshih/inp2025/game/utils" "gitea.konchin.com/ytshih/inp2025/game/utils"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"

View File

@@ -5,8 +5,8 @@ import (
"io" "io"
"net/http" "net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares"
"gitea.konchin.com/ytshih/inp2025/game/models" "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/types"
"gitea.konchin.com/ytshih/inp2025/game/utils" "gitea.konchin.com/ytshih/inp2025/game/utils"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"

View File

@@ -18,11 +18,6 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/api/lobby/rooms": {
"get": {
"responses": {}
}
},
"/api/lobby/users": { "/api/lobby/users": {
"get": { "get": {
"responses": {} "responses": {}

View File

@@ -10,11 +10,6 @@
}, },
"basePath": "/", "basePath": "/",
"paths": { "paths": {
"/api/lobby/rooms": {
"get": {
"responses": {}
}
},
"/api/lobby/users": { "/api/lobby/users": {
"get": { "get": {
"responses": {} "responses": {}

View File

@@ -14,9 +14,6 @@ info:
title: Intro. to Network Programming Game title: Intro. to Network Programming Game
version: 0.0.1-alpha version: 0.0.1-alpha
paths: paths:
/api/lobby/rooms:
get:
responses: {}
/api/lobby/users: /api/lobby/users:
get: get:
responses: {} responses: {}

48
server/wordle/handlers.go Normal file
View File

@@ -0,0 +1,48 @@
package wordle
import "gitea.konchin.com/ytshih/inp2025/game/types"
type OperationType int
const (
OperationTypeGuess OperationType = iota
)
type Operation struct {
Type OperationType
Username string
Guess string
}
type Handlers struct {
state WordleState
ans string
opCh chan Operation
subs []*chan types.WordleState
}
func NewHandlers() *Handlers {
ret := &Handlers{subs: nil}
go ret.GameFlow()
return ret
}
func (self *Handlers) GameFlow() {
for op := range self.opCh {
switch op.Type {
case OperationTypeGuess:
self.state.CurrentGuess++
if self.state.CurrentGuess >= len(self.Users) {
for i := range self.Users {
self.Users[i].History = append(self.Users[i].History,
types.NewWord(input, ans))
}
}
for _, sub := range self.subs {
}
}
}
}

View File

@@ -0,0 +1,46 @@
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 (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)
handlers.subs = append(handlers.subs, &dataCh)
for data := range 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)
}

27
types/game.go Normal file
View File

@@ -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],
)
}

83
types/wordle.go Normal file
View File

@@ -0,0 +1,83 @@
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"`
Input string
}
type WordleState struct {
Users map[string]UserState `msgpack:"users"`
CurrentGuess int
}
func NewWordleState() WordleState {
return WordleState{
Users: nil,
}
}

View File

@@ -11,6 +11,5 @@ func InitDB(ctx context.Context, db *bun.DB) error {
return db.ResetModel(ctx, return db.ResetModel(ctx,
(*models.User)(nil), (*models.User)(nil),
(*models.UserStatus)(nil), (*models.UserStatus)(nil),
(*models.Room)(nil),
) )
} }

32
workflows/wordleServer.go Normal file
View File

@@ -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, "")))
}