Feat: works on my machine

This commit is contained in:
2025-10-16 05:07:56 +08:00
commit 5bbab63a2c
37 changed files with 4553 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
game
logs/

14
Makefile Normal file
View File

@@ -0,0 +1,14 @@
.PHONY: all clean
GO_ARGS += CGO_ENABLED=0
SOURCE := $(shell find . -type f -name '*.go')
TARGET := game
all: $(TARGET)
$(TARGET): $(SOURCE)
$(GO_ARGS) go build -o $@
clean:
-rm -f $(TARGET)

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# Introduction to Network Programming Homework
## TODO
- Lobby
- Login count
- Logout and duplicate login
- Connection
- Scan port
- Game
- Game end reason

18
auth.go Normal file
View File

@@ -0,0 +1,18 @@
package main
import (
"gitea.konchin.com/ytshih/inp2025/workflows"
"github.com/spf13/cobra"
)
var authCmd = &cobra.Command{
Use: "auth",
Run: func(cmd *cobra.Command, args []string) {
workflows.AuthServer()
},
}
func init() {
authCmd.Flags().
String("listen-addr", "localhost:8888", "")
}

30
cmd.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var RootCmd = &cobra.Command{
Use: "game",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-"))
viper.BindPFlags(cmd.PersistentFlags())
viper.BindPFlags(cmd.Flags())
},
}
func main() {
RootCmd.Execute()
}
func init() {
cobra.EnableTraverseRunHooks = true
RootCmd.AddCommand(authCmd)
RootCmd.AddCommand(playerCmd)
RootCmd.AddCommand(testCmd)
}

2331
dict Normal file

File diff suppressed because it is too large Load Diff

68
go.mod Normal file
View File

@@ -0,0 +1,68 @@
module gitea.konchin.com/ytshih/inp2025
go 1.25.2
require (
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/go-resty/resty/v2 v2.16.5
github.com/gorilla/websocket v1.5.3
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/uptrace/bun v1.2.15
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15
github.com/uptrace/bun/driver/sqliteshim v1.2.15
github.com/uptrace/bunrouter v1.0.23
github.com/vmihailenco/msgpack/v5 v5.4.1
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.28.0 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.38.0 // indirect
)

173
go.sum Normal file
View File

@@ -0,0 +1,173 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.2.15 h1:Ut68XRBLDgp9qG9QBMa9ELWaZOmzHNdczHQdrOZbEFE=
github.com/uptrace/bun v1.2.15/go.mod h1:Eghz7NonZMiTX/Z6oKYytJ0oaMEJ/eq3kEV4vSqG038=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 h1:7upGMVjFRB1oI78GQw6ruNLblYn5CR+kxqcbbeBBils=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15/go.mod h1:c7YIDaPNS2CU2uI1p7umFuFWkuKbDcPDDvp+DLHZnkI=
github.com/uptrace/bun/driver/sqliteshim v1.2.15 h1:M/rZJSjOPV4OmfTVnDPtL+wJmdMTqDUn8cuk5ycfABA=
github.com/uptrace/bun/driver/sqliteshim v1.2.15/go.mod h1:YqwxFyvM992XOCpGJtXyKPkgkb+aZpIIMzGbpaw1hIk=
github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU=
github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

3
go.work Normal file
View File

@@ -0,0 +1,3 @@
go 1.25.2
use .

22
go.work.sum Normal file
View File

@@ -0,0 +1,22 @@
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

11
handlers/auth/handlers.go Normal file
View File

@@ -0,0 +1,11 @@
package auth
import "github.com/uptrace/bun"
type Handlers struct {
db *bun.DB
}
func NewHandlers(db *bun.DB) *Handlers {
return &Handlers{db: db}
}

View File

@@ -0,0 +1,59 @@
package auth
import (
"net/http"
"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"
)
func (self *Handlers) PostLogin(
w http.ResponseWriter,
req bunrouter.Request,
) error {
ctx := req.Context()
user, ok := ctx.Value(types.UserKey).(models.User)
if !ok {
return middlewares.HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "user not login",
}
}
res, err := self.db.NewUpdate().
Model((*models.User)(nil)).
Set("login_count = login_count + ?", 1).
Set("is_logged = ?", true).
Where("is_logged = ?", false).
Where("username = ?", user.Username).
Exec(ctx)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to update login count",
OriginError: err,
}
}
if cnt, err := res.RowsAffected(); err != nil || cnt == 0 {
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to get affected row count",
OriginError: err,
}
}
// debug
return utils.Success(w)
if cnt == 0 {
return middlewares.HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "already logged in",
}
}
}
return utils.Success(w)
}

View File

@@ -0,0 +1,39 @@
package auth
import (
"net/http"
"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"
)
func (self *Handlers) PostLogout(
w http.ResponseWriter,
req bunrouter.Request,
) error {
ctx := req.Context()
user, ok := ctx.Value(types.UserKey).(models.User)
if !ok {
return middlewares.HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "user not login",
}
}
_, err := self.db.NewUpdate().
Set("is_logged = ?", false).
Where("username = ?", user.Username).
Exec(ctx)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to update logged in status",
OriginError: err,
}
}
return utils.Success(w)
}

View File

@@ -0,0 +1,66 @@
package auth
import (
"encoding/json"
"io"
"net/http"
"gitea.konchin.com/ytshih/inp2025/middlewares"
"gitea.konchin.com/ytshih/inp2025/models"
"gitea.konchin.com/ytshih/inp2025/utils"
"github.com/uptrace/bunrouter"
)
func (self *Handlers) PostRegister(
w http.ResponseWriter,
req bunrouter.Request,
) error {
ctx := req.Context()
b, err := io.ReadAll(req.Body)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to read body payload",
OriginError: err,
}
}
var user models.User
if err := json.Unmarshal(b, &user); err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to unmarshal json into user",
OriginError: err,
}
}
res, err := self.db.NewInsert().
Model(&user).
On("CONFLICT (username) DO NOTHING").
Exec(ctx)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to insert user",
OriginError: err,
}
}
if cnt, err := res.RowsAffected(); err != nil || cnt == 0 {
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to get affected row count",
OriginError: err,
}
}
if cnt == 0 {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "username already exist",
}
}
}
return utils.Success(w)
}

View File

@@ -0,0 +1,55 @@
package wordle
import (
"net/http"
"gitea.konchin.com/ytshih/inp2025/middlewares"
"gitea.konchin.com/ytshih/inp2025/types"
"gitea.konchin.com/ytshih/inp2025/utils"
"github.com/gorilla/websocket"
"github.com/uptrace/bunrouter"
"github.com/vmihailenco/msgpack/v5"
)
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{
StatusCode: http.StatusInternalServerError,
Message: "failed to upgrade websocket",
OriginError: err,
}
}
defer c.Close()
username, _, ok := req.BasicAuth()
if !ok {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "username not exist",
}
}
dataCh := make(chan types.WordleState)
self.opCh <- &OperationSubs{
Username: username,
SubsCh: &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)
}

View File

@@ -0,0 +1,98 @@
package wordle
import (
"fmt"
"net/http"
"gitea.konchin.com/ytshih/inp2025/types"
"github.com/gorilla/websocket"
)
type Operation interface {
Run(self *Handlers) error
}
type OperationSubs struct {
Username string
SubsCh *chan types.WordleState
}
func (op *OperationSubs) Run(self *Handlers) error {
_, ok := self.state.History[op.Username]
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"`
}
func (op *OperationGuess) Run(self *Handlers) error {
self.state.CurrentGuess[op.Username] = op.Guess
if len(self.state.CurrentGuess) < len(self.state.History) {
return nil
}
for user, guess := range self.state.CurrentGuess {
if guess == self.ans {
self.state.GameEnd = true
}
guesses := self.state.History[user]
guesses[self.state.Round] = types.NewGuess(guess, self.ans)
self.state.History[user] = guesses
}
self.state.CurrentGuess = make(map[types.UsernameType]string)
self.state.Round++
if self.state.Round >= types.GUESS_COUNT_LIMIT {
self.state.GameEnd = true
}
return nil
}
type Handlers struct {
ans string
state types.WordleState
upgrader websocket.Upgrader
subs []*chan types.WordleState
opCh chan Operation
}
func NewHandlers(ans string) *Handlers {
ret := &Handlers{
ans: ans,
state: types.NewWordleState(),
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
},
opCh: make(chan Operation),
}
go ret.GameFlow()
return ret
}
func (self *Handlers) GameFlow() {
for op := range self.opCh {
if err := op.Run(self); err != nil {
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
}
}
}

View File

@@ -0,0 +1,54 @@
package wordle
import (
"io"
"net/http"
"gitea.konchin.com/ytshih/inp2025/middlewares"
"gitea.konchin.com/ytshih/inp2025/utils"
"github.com/uptrace/bunrouter"
"github.com/vmihailenco/msgpack/v5"
)
type PostGuessInput struct {
Guess string `msgpack:"guess"`
}
func (self *Handlers) PostGuess(
w http.ResponseWriter,
req bunrouter.Request,
) error {
// fmt.Fprintf(os.Stderr, "POST /api/guess\n")
b, err := io.ReadAll(req.Body)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to read body payload",
OriginError: err,
}
}
var input PostGuessInput
if err := msgpack.Unmarshal(b, &input); err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to unmarshal from msgpack",
OriginError: err,
}
}
username, _, ok := req.BasicAuth()
if !ok {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "username not exist",
}
}
self.opCh <- &OperationGuess{
Username: username,
Guess: input.Guess,
}
return utils.Success(w)
}

60
middlewares/auth.go Normal file
View File

@@ -0,0 +1,60 @@
package middlewares
import (
"context"
"database/sql"
"errors"
"net/http"
"gitea.konchin.com/ytshih/inp2025/models"
"gitea.konchin.com/ytshih/inp2025/types"
"github.com/uptrace/bunrouter"
)
func (self *Handlers) Auth(
next bunrouter.HandlerFunc,
) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
ctx := req.Context()
username, password, ok := req.BasicAuth()
if !ok {
return HTTPError{
StatusCode: http.StatusNotFound,
Message: "username not exist",
}
}
dbUser := models.User{Username: username}
err := self.db.NewSelect().
Model(&dbUser).
WherePK().
Scan(ctx)
if errors.Is(err, sql.ErrNoRows) {
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "username not exist",
OriginError: err,
}
}
if err != nil {
return HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to select user from db",
OriginError: err,
}
}
if password != dbUser.Password {
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "password incorrect",
OriginError: err,
}
}
return next(w, req.WithContext(context.WithValue(
ctx, types.UserKey, models.User{
Username: username,
Password: password,
})))
}
}

View File

@@ -0,0 +1,63 @@
package middlewares
import (
"fmt"
"net/http"
"os"
"github.com/uptrace/bunrouter"
"go.uber.org/zap"
)
type HTTPError struct {
StatusCode int `json:"code"`
Message string `json:"message"`
OriginError error `json:"-"`
}
func (e HTTPError) Error() string {
return e.Message
}
func NewHTTPError(err error) HTTPError {
return HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "Internal server error with unknown reason",
OriginError: err,
}
}
func (self *Handlers) ErrorHandler(
next bunrouter.HandlerFunc,
) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
err := next(w, req)
var httpErr HTTPError
switch err := err.(type) {
case nil:
return nil
case HTTPError:
httpErr = err
default:
fmt.Fprintf(os.Stderr, "unhandled error, %v\n", err)
zap.L().Error("unhandled error",
zap.Error(err))
httpErr = NewHTTPError(err)
}
if httpErr.OriginError == nil {
zap.L().Warn(httpErr.Message)
} else {
zap.L().Warn(httpErr.Message,
zap.Error(httpErr.OriginError))
}
w.WriteHeader(httpErr.StatusCode)
_ = bunrouter.JSON(w, httpErr)
return err
}
}

11
middlewares/handlers.go Normal file
View File

@@ -0,0 +1,11 @@
package middlewares
import "github.com/uptrace/bun"
type Handlers struct {
db *bun.DB
}
func NewHandlers(db *bun.DB) *Handlers {
return &Handlers{db: db}
}

13
models/user.go Normal file
View File

@@ -0,0 +1,13 @@
package models
import "github.com/uptrace/bun"
type User struct {
bun.BaseModel `bun:"table:user" json:"-"`
Username string `bun:"username,pk" json:"username"`
Password string `bun:"password" json:"password"`
IsLogged bool `bun:"is_logged" json:"isLogged"`
LoginCount int `bun:"login_count" json:"loginCount"`
}

37
player.go Normal file
View File

@@ -0,0 +1,37 @@
package main
import (
"gitea.konchin.com/ytshih/inp2025/stages"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var playerCmd = &cobra.Command{
Use: "player",
Run: func(cmd *cobra.Command, args []string) {
queue := []*tea.Program{}
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 {
panic(err)
}
}
},
}
func init() {
playerCmd.Flags().
Int("udp-listen-port", 18787, "")
playerCmd.Flags().
StringSlice("udp-endpoints", []string{"localhost:18787"}, "")
playerCmd.Flags().
String("auth-endpoint", "http://localhost:8888", "")
}

23
stages/base.go Normal file
View File

@@ -0,0 +1,23 @@
package stages
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/go-resty/resty/v2"
)
type BaseModel struct {
queue *[]*tea.Program
client *resty.Client
}
func NewBaseModel(
queue *[]*tea.Program,
endpoint string,
) *BaseModel {
return &BaseModel{
queue: queue,
client: resty.New().
SetBaseURL(endpoint).
SetDisableWarn(true),
}
}

241
stages/landing.go Normal file
View File

@@ -0,0 +1,241 @@
package stages
import (
"fmt"
"net/http"
"strings"
"gitea.konchin.com/ytshih/inp2025/models"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
type landingOperationType int
const (
landingOperationChoose landingOperationType = iota
landingOperationLoginCred
landingOperationRegistCred
)
type focusTargetType int
const (
focusTargetUsername focusTargetType = iota
focusTargetPassword
)
type LandingModel struct {
*BaseModel
op landingOperationType
focus focusTargetType
username textinput.Model
password textinput.Model
info string
err error
}
func NewLandingModel(base *BaseModel) *LandingModel {
username := textinput.New()
username.Placeholder = "Username"
username.CharLimit = 32
username.Width = 32
username.Blur()
password := textinput.New()
password.Placeholder = "Password"
password.CharLimit = 32
password.Width = 32
password.Blur()
password.EchoMode = textinput.EchoPassword
password.EchoCharacter = '•'
return &LandingModel{
BaseModel: base,
op: landingOperationChoose,
username: username,
password: password,
}
}
func (m *LandingModel) reset() {
m.op = landingOperationChoose
m.username.Blur()
m.username.Reset()
m.password.Blur()
m.password.Reset()
}
type postLoginMsg struct{}
func (m *LandingModel) postLogin() tea.Cmd {
return func() tea.Msg {
resp, err := m.BaseModel.client.R().
SetBasicAuth(m.username.Value(), m.password.Value()).
Post("/auth/login")
if err == nil {
switch resp.StatusCode() {
case http.StatusOK:
m.BaseModel.client.SetBasicAuth(
m.username.Value(), m.password.Value())
m.info = "login success.\n"
m.err = nil
case http.StatusUnauthorized:
m.err = fmt.Errorf("user not exist or password incorrect, %s",
string(resp.Body()))
default:
m.err = fmt.Errorf("unknown server error, %s",
string(resp.Body()))
}
} else {
m.err = fmt.Errorf("failed to post login, %w", err)
}
return postLoginMsg{}
}
}
type postRegisterMsg struct{}
func (m *LandingModel) postRegister() tea.Cmd {
return func() tea.Msg {
resp, err := m.BaseModel.client.R().
SetBody(models.User{
Username: m.username.Value(),
Password: m.password.Value(),
}).
Post("/auth/register")
if err == nil {
switch resp.StatusCode() {
case http.StatusOK:
m.info = "register success.\n"
m.err = nil
case http.StatusBadRequest:
m.err = fmt.Errorf("username already exist, %s",
string(resp.Body()))
default:
m.err = fmt.Errorf("unknown server error, %s",
string(resp.Body()))
}
} else {
m.err = fmt.Errorf("failed to post register, %s",
string(resp.Body()))
}
return postRegisterMsg{}
}
}
func (m *LandingModel) Init() tea.Cmd {
return tea.ClearScreen
}
func (m *LandingModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
case "L", "l":
if m.op == landingOperationChoose {
m.op = landingOperationLoginCred
m.focus = focusTargetUsername
cmds = append(cmds, m.username.Focus())
return m, tea.Batch(cmds...)
}
case "R", "r":
if m.op == landingOperationChoose {
m.op = landingOperationRegistCred
m.focus = focusTargetUsername
cmds = append(cmds, m.username.Focus())
return m, tea.Batch(cmds...)
}
case "shift+tab", "up":
if m.op == landingOperationLoginCred ||
m.op == landingOperationRegistCred {
switch m.focus {
case focusTargetUsername:
m.username.Blur()
m.focus = focusTargetPassword
cmds = append(cmds, m.password.Focus())
case focusTargetPassword:
m.password.Blur()
m.focus = focusTargetUsername
cmds = append(cmds, m.username.Focus())
}
}
case "tab", "down", "enter":
if m.op == landingOperationLoginCred ||
m.op == landingOperationRegistCred {
switch m.focus {
case focusTargetUsername:
m.username.Blur()
m.focus = focusTargetPassword
cmds = append(cmds, m.password.Focus())
case focusTargetPassword:
if msg.String() == "enter" {
switch m.op {
case landingOperationLoginCred:
cmds = append(cmds, m.postLogin())
case landingOperationRegistCred:
cmds = append(cmds, m.postRegister())
}
} else {
m.password.Blur()
m.focus = focusTargetUsername
cmds = append(cmds, m.username.Focus())
}
}
}
}
case postLoginMsg:
if m.err == nil {
*m.queue = append(*m.queue,
tea.NewProgram(NewLobbyModel(m.BaseModel)))
return m, tea.Quit
} else {
m.reset()
}
case postRegisterMsg:
m.reset()
}
var cmd tea.Cmd
m.username, cmd = m.username.Update(msg)
cmds = append(cmds, cmd)
m.password, cmd = m.password.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m *LandingModel) View() string {
var b strings.Builder
switch m.op {
case landingOperationChoose:
fmt.Fprintf(&b, "Choose Operation\n(L)ogin / (R)egister\n")
case landingOperationLoginCred:
fmt.Fprintf(&b, "Login: \n")
b.WriteString(m.username.View() + "\n")
b.WriteString(m.password.View() + "\n")
case landingOperationRegistCred:
fmt.Fprintf(&b, "Register: \n")
b.WriteString(m.username.View() + "\n")
b.WriteString(m.password.View() + "\n")
}
if m.info != "" {
b.WriteString(m.info + "\n")
}
if m.err != nil {
b.WriteString("----------\n")
b.WriteString(m.err.Error() + "\n")
}
return b.String()
}

283
stages/lobby.go Normal file
View File

@@ -0,0 +1,283 @@
package stages
import (
"fmt"
"net"
"strings"
"time"
"gitea.konchin.com/ytshih/inp2025/types"
"gitea.konchin.com/ytshih/inp2025/utils"
"gitea.konchin.com/ytshih/inp2025/workflows"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/viper"
"github.com/vmihailenco/msgpack/v5"
)
const (
REFRESH_TIME = time.Second
)
type lobbyOperationType int
const (
lobbyOperationChoose lobbyOperationType = iota
lobbyOperationServerWaiting
lobbyOperationServerChoose
lobbyOperationClientScannning
)
type LobbyModel struct {
*BaseModel
op lobbyOperationType
shutdown types.ShutdownFunc
local string
remote string
// server
remoteUser string
listener net.Listener
// client
cursor int
endpoints []string
info string
err error
}
func NewLobbyModel(base *BaseModel) *LobbyModel {
return &LobbyModel{
BaseModel: base,
op: lobbyOperationChoose,
shutdown: func() {},
}
}
type serverListenMsg struct{}
func (m *LobbyModel) serverListen() tea.Cmd {
return func() tea.Msg {
dataCh := make(chan string)
m.local, m.shutdown, m.err = utils.ListenUDPData(
viper.GetInt("udp-listen-port"), dataCh)
if m.err != nil {
m.err = fmt.Errorf("failed to listen, %w", m.err)
return serverListenMsg{}
}
req := <-dataCh
m.shutdown()
m.shutdown = func() {}
var joinRequest types.JoinRequest
if err := msgpack.Unmarshal([]byte(req), &joinRequest); err != nil {
m.err = fmt.Errorf("failed to unmarshal msgpack, %w", err)
return serverListenMsg{}
}
m.remote = joinRequest.Endpoint
m.remoteUser = joinRequest.Username
return serverListenMsg{}
}
}
type serverSendReplyMsg struct{}
func (m *LobbyModel) serverSendReply(response bool) tea.Cmd {
return func() tea.Msg {
if response {
// Start Wordle Server listener
m.listener, m.err = net.Listen("tcp4", ":0")
if m.err != nil {
m.err = fmt.Errorf("failed to listen on anonymous port, %w", m.err)
return serverSendReplyMsg{}
}
local := fmt.Sprintf("%s:%d",
m.listener.Addr().(*net.TCPAddr).IP.String(),
m.listener.Addr().(*net.TCPAddr).Port)
m.err = utils.SendPayload(local, m.remote,
types.JoinResponse{Endpoint: local})
// Store wordle server endpoint
m.remote = local
} else {
m.err = utils.SendPayload("", m.remote,
types.JoinResponse{Endpoint: ""})
}
return serverSendReplyMsg{}
}
}
type clientScanMsg time.Time
func (m *LobbyModel) clientScan() tea.Cmd {
return tea.Tick(REFRESH_TIME, func(t time.Time) tea.Msg {
m.endpoints = []string{}
for _, endpoint := range viper.GetStringSlice("udp-endpoints") {
if err := utils.Ping(endpoint); err == nil {
m.endpoints = append(m.endpoints, endpoint)
}
}
return clientScanMsg(t)
})
}
type clientJoinMsg struct{}
func (m *LobbyModel) clientJoin() tea.Cmd {
return func() tea.Msg {
dataCh := make(chan string)
m.local, m.shutdown, m.err = utils.ListenUDPData(0, dataCh)
if m.err != nil {
m.err = fmt.Errorf("failed to listen udp data, %w", m.err)
return clientJoinMsg{}
}
m.err = utils.SendPayload(m.local, m.remote, types.JoinRequest{
Endpoint: m.local,
Username: m.client.UserInfo.Username,
})
if m.err != nil {
m.err = fmt.Errorf("failed to send invitation, %w", m.err)
return clientJoinMsg{}
}
data := <-dataCh
m.shutdown()
m.shutdown = func() {}
var joinResponse types.JoinResponse
if err := msgpack.Unmarshal([]byte(data), &joinResponse); err != nil {
m.err = fmt.Errorf("failed to unmarshal msgpack, %w", err)
return clientJoinMsg{}
}
m.remote = joinResponse.Endpoint
return clientJoinMsg{}
}
}
func (m *LobbyModel) Init() tea.Cmd {
return tea.ClearScreen
}
func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
}
}
switch m.op {
case lobbyOperationChoose:
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "S", "s":
m.op = lobbyOperationServerWaiting
cmds = append(cmds, m.serverListen())
case "C", "c":
m.op = lobbyOperationClientScannning
cmds = append(cmds, m.clientScan())
}
}
case lobbyOperationServerWaiting:
switch msg.(type) {
case serverListenMsg:
m.op = lobbyOperationServerChoose
}
case lobbyOperationServerChoose:
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "Y", "y", "enter":
cmds = append(cmds, m.serverSendReply(true))
case "N", "n":
m.op = lobbyOperationServerWaiting
cmds = append(cmds, m.serverSendReply(false))
}
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)))
return m, tea.Quit
} else {
m.op = lobbyOperationServerWaiting
}
}
case lobbyOperationClientScannning:
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "shift+tab", "up":
if n := len(m.endpoints); n > 0 {
m.cursor = (m.cursor - 1 + n) % n
}
case "tab", "down":
if n := len(m.endpoints); n > 0 {
m.cursor = (m.cursor + 1) % n
}
case "enter":
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() {})))
return m, tea.Quit
}
}
}
return m, tea.Batch(cmds...)
}
func (m *LobbyModel) View() string {
var b strings.Builder
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",
m.remoteUser)
case lobbyOperationClientScannning:
b.WriteString("Scanning server...\nChoose one to join\n")
for i, endpoint := range m.endpoints {
if i == m.cursor {
fmt.Fprintf(&b, "(•) %s\n", endpoint)
} else {
fmt.Fprintf(&b, "( ) %s\n", endpoint)
}
}
}
if m.info != "" {
b.WriteString(m.info + "\n")
}
if m.err != nil {
b.WriteString("----------\n")
b.WriteString(m.err.Error() + "\n")
}
return b.String()
}

1
stages/udp.go Normal file
View File

@@ -0,0 +1 @@
package stages

179
stages/wordle.go Normal file
View File

@@ -0,0 +1,179 @@
package stages
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"gitea.konchin.com/ytshih/inp2025/handlers/wordle"
"gitea.konchin.com/ytshih/inp2025/types"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/gorilla/websocket"
"github.com/vmihailenco/msgpack/v5"
)
const (
WEBSOCKET_RETRY int = 5
WEBSOCKET_BACKOFF time.Duration = 100 * time.Millisecond
)
type WordleClientModel struct {
*BaseModel
conn *websocket.Conn
state types.WordleState
shutdown types.ShutdownFunc
input textinput.Model
err error
}
func NewWordleClientModel(
base *BaseModel,
shutdown types.ShutdownFunc,
) *WordleClientModel {
input := textinput.New()
input.Focus()
input.CharLimit = types.GUESS_WORD_LENGTH
input.Width = types.GUESS_WORD_LENGTH
return &WordleClientModel{
BaseModel: base,
input: input,
shutdown: shutdown,
}
}
func (m *WordleClientModel) getState() tea.Cmd {
return func() tea.Msg {
if m.conn == nil {
u, err := url.Parse(m.client.BaseURL)
if err != nil {
m.err = fmt.Errorf("failed to parse BaseURL, %w", err)
}
u.Scheme = "ws"
u.Path = "/api/state"
for try := 0; try < WEBSOCKET_RETRY; try++ {
req, _ := http.NewRequest("GET",
/*placeholder*/ "http://localhost", nil)
req.SetBasicAuth(
m.client.UserInfo.Username,
m.client.UserInfo.Password)
m.conn, _, err = websocket.DefaultDialer.Dial(
u.String(), req.Header)
if err == nil {
break
}
time.Sleep(WEBSOCKET_BACKOFF)
}
if err != nil {
m.err = fmt.Errorf("failed to dial, %w", err)
}
}
_, b, err := m.conn.ReadMessage()
if err == nil {
var state types.WordleState
if err := msgpack.Unmarshal(b, &state); err != nil {
m.err = fmt.Errorf("failed to unmarshal state, %w", err)
}
return state
} else {
m.err = fmt.Errorf("failed to read message, %w", err)
return nil
}
}
}
func (m *WordleClientModel) postGuess(guess string) tea.Cmd {
return func() tea.Msg {
b, err := msgpack.Marshal(wordle.OperationGuess{
Username: m.client.UserInfo.Username,
Guess: guess,
})
if err != nil {
m.err = fmt.Errorf("failed to post guess, %w", err)
}
_, err = m.client.R().
SetBody(b).
Post("/api/guess")
if err != nil {
m.err = fmt.Errorf("failed to post guess, %w", err)
}
return nil
}
}
func (m *WordleClientModel) Init() tea.Cmd {
return tea.Sequence(tea.ClearScreen,
tea.Batch(m.getState(), textinput.Blink))
}
func (m *WordleClientModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
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)))
return m, tea.Quit
}
if len(m.input.Value()) == types.GUESS_WORD_LENGTH {
input := strings.ToUpper(m.input.Value())
m.input.Reset()
cmds = append(cmds, m.postGuess(input))
}
}
case types.WordleState:
m.state = msg
if m.state.GameEnd {
m.input.Blur()
}
if _, ok := m.state.CurrentGuess[m.client.UserInfo.Username]; ok {
m.input.Blur()
} else {
cmds = append(cmds, m.input.Focus())
}
cmds = append(cmds, m.getState())
}
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m *WordleClientModel) View() string {
var b strings.Builder
fmt.Fprintf(&b, "Login as '%s'\n", m.client.UserInfo.Username)
fmt.Fprintf(&b, "remote addr: %s\n", m.client.BaseURL)
b.WriteString(m.state.View() + "\n")
b.WriteString(types.NewWordBank(m.state).View() + "\n")
if m.state.GameEnd {
fmt.Fprintf(&b, "Game End. Press 'Enter' to lobby.\n")
} else {
if guess, ok := m.state.CurrentGuess[m.client.UserInfo.Username]; ok {
fmt.Fprintf(&b, "Current guess for the round: %s\n", guess)
} else {
fmt.Fprintf(&b, "guess: %s\n", m.input.View())
}
if m.err != nil {
fmt.Fprintf(&b, "error: %+v\n", m.err)
}
}
return b.String()
}

38
test.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"gitea.konchin.com/ytshih/inp2025/utils"
"github.com/spf13/cobra"
)
var testCmd = &cobra.Command{
Use: "test",
}
var testServerCmd = &cobra.Command{
Use: "server",
Run: func(cmd *cobra.Command, args []string) {
dataCh := make(chan string)
_, shutdown, err := utils.ListenUDPData(18787, dataCh)
if err != nil {
panic(err)
}
defer shutdown()
<-dataCh
},
}
var testClientCmd = &cobra.Command{
Use: "client",
Run: func(cmd *cobra.Command, args []string) {
err := utils.Ping("localhost:18787")
if err != nil {
panic(err)
}
},
}
func init() {
testCmd.AddCommand(testServerCmd)
testCmd.AddCommand(testClientCmd)
}

7
types/auth.go Normal file
View File

@@ -0,0 +1,7 @@
package types
type UserType struct{}
var (
UserKey = UserType{}
)

5
types/types.go Normal file
View File

@@ -0,0 +1,5 @@
package types
type ShutdownFunc func()
type UsernameType = string

10
types/udp.go Normal file
View File

@@ -0,0 +1,10 @@
package types
type JoinRequest struct {
Endpoint string
Username UsernameType
}
type JoinResponse struct {
Endpoint string
}

209
types/wordle.go Normal file
View File

@@ -0,0 +1,209 @@
package types
import (
"fmt"
"sort"
"strings"
"github.com/charmbracelet/lipgloss"
)
const (
GUESS_COUNT_LIMIT int = 6
GUESS_WORD_LENGTH int = 5
ALPHABET_COUNT int = 26
)
type GuessStateType int
const (
GuessStateNotGuessed GuessStateType = iota
GuessStateWrongChar
GuessStateWrongPos
GuessStateCorrect
)
type GuessChar struct {
Char rune
State GuessStateType
}
func (self GuessChar) View() string {
style := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF"))
switch self.State {
case GuessStateWrongChar:
style = style.Background(lipgloss.Color("#3a3a3c"))
case GuessStateWrongPos:
style = style.Background(lipgloss.Color("#b59f3b"))
case GuessStateCorrect:
style = style.Background(lipgloss.Color("#538d4e"))
}
return style.Render(string(self.Char))
}
type Guess struct {
Data [GUESS_WORD_LENGTH]GuessChar
}
type guessLeftType struct {
Rune rune
Idx int
}
func NewGuess(guess, ans string) Guess {
var ret Guess
var ansLeft []rune
var guessLeft []guessLeftType
for i := range ret.Data {
ret.Data[i] = GuessChar{
Char: rune(guess[i]),
State: GuessStateWrongChar,
}
if guess[i] == ans[i] {
ret.Data[i].State = GuessStateCorrect
} else {
ansLeft = append(ansLeft, rune(ans[i]))
guessLeft = append(guessLeft, guessLeftType{
Rune: rune(guess[i]),
Idx: i,
})
}
}
sort.Slice(ansLeft, func(i, j int) bool {
return int32(ansLeft[i]) < int32(ansLeft[j])
})
sort.Slice(guessLeft, func(i, j int) bool {
return int32(guessLeft[i].Rune) < int32(guessLeft[j].Rune)
})
i, j := 0, 0
for i < len(ansLeft) && j < len(guessLeft) {
if int32(ansLeft[i]) == int32(guessLeft[j].Rune) {
ret.Data[guessLeft[j].Idx].State = GuessStateWrongPos
i++
j++
} else {
if int32(ansLeft[i]) < int32(guessLeft[j].Rune) {
i++
} else {
j++
}
}
}
return ret
}
func (self Guess) View() string {
var b strings.Builder
for _, ch := range self.Data {
b.WriteString(ch.View())
}
return b.String()
}
type WordleState struct {
History map[UsernameType][GUESS_COUNT_LIMIT]Guess
CurrentGuess map[UsernameType]string
Round int
GameEnd bool
}
func NewWordleState() WordleState {
return WordleState{
History: make(map[UsernameType][GUESS_COUNT_LIMIT]Guess),
CurrentGuess: make(map[UsernameType]string),
Round: 0,
GameEnd: false,
}
}
func (self *WordleState) View() string {
var b strings.Builder
col := []string{}
for user := range self.History {
col = append(col, user)
}
sort.Slice(col, func(i, j int) bool {
return col[i] < col[j]
})
fmt.Fprintf(&b, " ")
for _, user := range col {
fmt.Fprintf(&b, "%5s ", user)
}
b.WriteRune('\n')
table := make([][]string, GUESS_COUNT_LIMIT)
for i := range table {
table[i] = make([]string, len(col))
}
for j, user := range col {
for i, guess := range self.History[user] {
table[i][j] = guess.View()
}
}
for i, row := range table {
fmt.Fprintf(&b, "%1d ", i+1)
for _, e := range row {
b.WriteString(e + " ")
}
b.WriteRune('\n')
}
return b.String()
}
type WordBank struct {
Data [ALPHABET_COUNT]GuessStateType
}
func NewWordBank(state WordleState) WordBank {
// FIXME: breaks when enter ^A-Z
var ret WordBank
for _, guesses := range state.History {
for i := 0; i < state.Round; i++ {
for _, ch := range guesses[i].Data {
ret.Data[int32(ch.Char)-'A'] = max(
ret.Data[int32(ch.Char)-'A'],
ch.State)
}
}
}
return ret
}
func (self WordBank) View() string {
var b strings.Builder
for i, state := range self.Data {
style := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF"))
switch state {
case GuessStateNotGuessed:
style = style.Background(lipgloss.Color("#2c3032"))
case GuessStateWrongChar:
style = style.Background(lipgloss.Color("#3a3a3c"))
case GuessStateWrongPos:
style = style.Background(lipgloss.Color("#b59f3b"))
case GuessStateCorrect:
style = style.Background(lipgloss.Color("#538d4e"))
}
b.WriteString(style.Render(string('A' + i)))
if i%10 == 9 {
b.WriteRune('\n')
}
}
return b.String()
}

13
utils/initDB.go Normal file
View File

@@ -0,0 +1,13 @@
package utils
import (
"context"
"gitea.konchin.com/ytshih/inp2025/models"
"github.com/uptrace/bun"
)
func InitDB(ctx context.Context, db *bun.DB) error {
return db.ResetModel(ctx,
(*models.User)(nil))
}

14
utils/success.go Normal file
View File

@@ -0,0 +1,14 @@
package utils
import (
"io"
"net/http"
)
func Success(w http.ResponseWriter) error {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
_, err := io.WriteString(w,
`{"code":200, "message": "success"}`+"\n")
return err
}

143
utils/udp.go Normal file
View File

@@ -0,0 +1,143 @@
package utils
import (
"fmt"
"net"
"gitea.konchin.com/ytshih/inp2025/types"
"github.com/vmihailenco/msgpack/v5"
)
const (
BUFFER_SIZE int = 1024
MAGIC_NUMBER int = 114514
)
type UDPReqType int
const (
UDPReqTypeData UDPReqType = iota
UDPReqTypePingRequest
UDPReqTypePingReply
)
type UDPPayload struct {
MagicNumber int `msgpack:"magicNumber"`
Endpoint string `msgpack:"endpoint"`
Type UDPReqType `msgpack:"type"`
Data string `msgpack:"data"`
}
func ListenUDPData(
port int,
dataCh chan string,
) (string, types.ShutdownFunc, error) {
return ListenUDP(port, dataCh, nil)
}
func Ping(endpoint string) error {
pingCh := make(chan string)
local, shutdown, err := ListenUDP(0, nil, pingCh)
if err != nil {
return err
}
defer shutdown()
err = SendRawPayload(endpoint, UDPPayload{
MagicNumber: MAGIC_NUMBER,
Endpoint: local,
Type: UDPReqTypePingRequest,
})
if err != nil {
return err
}
<-pingCh
return nil
}
func ListenUDP(
port int,
dataCh chan string,
pingCh chan string,
) (string, types.ShutdownFunc, error) {
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port})
if err != nil {
return "", nil, fmt.Errorf("failed to listen udp, %w", err)
}
local := conn.LocalAddr().String()
go func() {
for {
buffer := make([]byte, BUFFER_SIZE)
n, _, err := conn.ReadFromUDP(buffer)
if err != nil {
continue
}
var payload UDPPayload
err = msgpack.Unmarshal(buffer[:n], &payload)
if err == nil && payload.MagicNumber == MAGIC_NUMBER {
switch payload.Type {
case UDPReqTypeData:
if dataCh != nil {
dataCh <- payload.Data
}
case UDPReqTypePingRequest:
SendRawPayload(payload.Endpoint, UDPPayload{
MagicNumber: MAGIC_NUMBER,
Endpoint: local,
Type: UDPReqTypePingReply,
})
case UDPReqTypePingReply:
if pingCh != nil {
pingCh <- payload.Endpoint
}
}
}
}
}()
return local, func() { conn.Close() }, nil
}
func SendPayload(
local, remote string,
data any,
) error {
sdata, err := msgpack.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal data, %w", err)
}
return SendRawPayload(remote, UDPPayload{
MagicNumber: MAGIC_NUMBER,
Endpoint: local,
Type: UDPReqTypeData,
Data: string(sdata),
})
}
func SendRawPayload(
endpoint string,
payload UDPPayload,
) error {
conn, err := net.Dial("udp", endpoint)
if err != nil {
return fmt.Errorf("failed to dial endpoint, %w", err)
}
defer conn.Close()
b, err := msgpack.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload, %w", err)
}
_, err = conn.Write(b)
if err != nil {
return fmt.Errorf("failed to send payload, %w", err)
}
return nil
}

63
workflows/authServer.go Normal file
View File

@@ -0,0 +1,63 @@
package workflows
import (
"context"
"database/sql"
"fmt"
"net/http"
"gitea.konchin.com/ytshih/inp2025/handlers/auth"
"gitea.konchin.com/ytshih/inp2025/middlewares"
"gitea.konchin.com/ytshih/inp2025/utils"
"github.com/spf13/viper"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim"
"github.com/uptrace/bunrouter"
"go.uber.org/zap"
)
func AuthServer() {
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
if err != nil {
panic(fmt.Errorf("failed to init sqlite, %w", err))
}
db := bun.NewDB(sqldb, sqlitedialect.New())
if err := utils.InitDB(context.Background(), db); err != nil {
panic(fmt.Errorf("failed to init db schema, %w", err))
}
logger := zap.NewExample()
defer logger.Sync()
undo := zap.ReplaceGlobals(logger)
defer undo()
authHandlers := auth.NewHandlers(db)
middlewareHandlers := middlewares.NewHandlers(db)
router := bunrouter.New()
authGroup := router.NewGroup("/auth").
Use(middlewareHandlers.ErrorHandler)
authGroup.POST("/register",
authHandlers.PostRegister)
authGroup.POST("/login",
middlewareHandlers.Auth(authHandlers.PostLogin))
authGroup.POST("/logout",
middlewareHandlers.Auth(authHandlers.PostLogout))
server := &http.Server{
Addr: viper.GetString("listen-addr"),
Handler: http.Handler(router),
}
defer func() {
sqldb.Close()
if err := server.Shutdown(context.Background()); err != nil {
panic(fmt.Errorf("failed to shutdown wordle server, %w", err))
}
}()
fmt.Printf("server up\n")
fmt.Println(server.ListenAndServe())
}

86
workflows/wordleServer.go Normal file
View File

@@ -0,0 +1,86 @@
package workflows
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"os"
"gitea.konchin.com/ytshih/inp2025/handlers/wordle"
"gitea.konchin.com/ytshih/inp2025/middlewares"
"gitea.konchin.com/ytshih/inp2025/types"
"github.com/uptrace/bunrouter"
"go.uber.org/zap"
)
const (
DICT_FILE string = "./dict"
)
func generateWordleAns() (string, error) {
file, err := os.Open(DICT_FILE)
if err != nil {
return "", fmt.Errorf("failed to open dictionary, %w", err)
}
dict := []string{}
for {
var w string
_, err := fmt.Fscanf(file, "%s", &w)
if err != nil {
break
}
dict = append(dict, w)
}
return dict[rand.Intn(len(dict))], nil
}
func WordleServer(listener net.Listener) types.ShutdownFunc {
ans, err := generateWordleAns()
if err != nil {
panic(fmt.Errorf("failed to generate answer, %w", err))
}
logger, _ := zap.Config{
Level: zap.NewAtomicLevelAt(zap.InfoLevel),
Encoding: "json",
OutputPaths: []string{"logs/wordle-stdout.log"},
ErrorOutputPaths: []string{"logs/wordle-stderr.log"},
EncoderConfig: zap.NewProductionEncoderConfig(),
}.Build()
undo := zap.ReplaceGlobals(logger)
wordleHandlers := wordle.NewHandlers(ans)
middlewareHandlers := middlewares.NewHandlers(nil)
router := bunrouter.New()
apiGroup := router.NewGroup("/api").
Use(middlewareHandlers.ErrorHandler)
apiGroup.GET("/state",
wordleHandlers.GetState)
apiGroup.POST("/guess",
wordleHandlers.PostGuess)
server := &http.Server{
Handler: http.Handler(router),
}
go func() {
fmt.Printf("server up\n")
if err := server.Serve(listener); err != http.ErrServerClosed {
panic(fmt.Errorf("wordle server failed, %w", err))
}
}()
return func() {
logger.Sync()
undo()
if err := server.Shutdown(context.Background()); err != nil {
panic(fmt.Errorf("failed to shutdown wordle server, %w", err))
}
}
}