diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ca3f339 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +; https://editorconfig.org/ + +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[{Makefile,go.mod,go.sum,*.go,.gitmodules}] +indent_style = tab +indent_size = 4 + +[*.md] +indent_size = 4 +trim_trailing_whitespace = false + +[Dockerfile] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d234cfe --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Treat all files in the Go repo as binary, with no git magic updating +# line endings. This produces predictable results in different environments. +# +# Windows users contributing to Go will need to use a modern version +# of git and editors capable of LF line endings. +# +# Windows .bat files are known to have multiple bugs when run with LF +# endings, and so they are checked in with CRLF endings, with a test +# in test/winbatch.go to catch problems. (See golang.org/issue/37791.) +# +# We'll prevent accidental CRLF line endings from entering the repo +# via the git-codereview gofmt checks and tests. +# +# See golang.org/issue/9281. + +* -text +*.go eol=lf diff=golang +*.ts diff=typescript +*.tsx diff=typescript +*.json diff=json +*.sql diff=sql +*.md text diff=markdown diff --git a/.github/workflows/test-go-unit.yml b/.github/workflows/test-go-unit.yml index c2e8666..5035750 100644 --- a/.github/workflows/test-go-unit.yml +++ b/.github/workflows/test-go-unit.yml @@ -11,7 +11,7 @@ on: - .github/workflows/test-go-unit.yml env: - GO_VERSION: 1.21 + GO_VERSION: 1.22 jobs: build: @@ -30,6 +30,13 @@ jobs: working-directory: ./Backend run: cp env .env + - name: Install dependencies + working-directory: ./Backend + run: | + go mod tidy + go mod download + go mod vendor + - name: Build working-directory: ./Backend run: go build -v ./... @@ -61,7 +68,7 @@ jobs: services: mysql: - image: mysql:8.1.0 + image: mysql:8.1 env: MYSQL_ROOT_PASSWORD: ${{ secrets.DB_ROOT_PASSWORD }} MYSQL_DATABASE: test @@ -69,7 +76,7 @@ jobs: MYSQL_PASSWORD: ${{ secrets.DB_PASSWORD }} ports: - "3306:3306" - + runs-on: ubuntu-22.04 steps: - name: Checkout code @@ -83,9 +90,18 @@ jobs: working-directory: ./Backend run: cp env .env + - name: Wait for MySQL + run: | + wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh + chmod +x wait-for-it.sh + ./wait-for-it.sh 127.0.0.1:3306 --timeout=60 + - name: Migration working-directory: ./Backend - run: go run ./cmd/migrate/migrate.go + run: | + go run ./cmd/migrate/migrate.go + go run ./cmd/migrate/game_card_seeder.go + - name: 🎯 Acceptance test working-directory: ./Backend run: go test ./... -v -count=1 -coverprofile=coverage.out @@ -94,4 +110,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: acceptance-test-coverage-report - path: ./Backend/coverage.out \ No newline at end of file + path: ./Backend/coverage.out diff --git a/.gitignore b/.gitignore index 2195e91..67cdc41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ # Backend +Backend/cmd/app/docs/swagger.* Backend/.env /.idea/* +vendor/ diff --git a/Backend/cmd/app/docs/docs.go b/Backend/cmd/app/docs/docs.go new file mode 100644 index 0000000..1a72e82 --- /dev/null +++ b/Backend/cmd/app/docs/docs.go @@ -0,0 +1,372 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/games": { + "post": { + "description": "Start a new game", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "games" + ], + "summary": "Start a new game", + "parameters": [ + { + "description": "Players", + "name": "players", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateGameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.CreateGameResponse" + } + } + } + } + }, + "/api/v1/games/{gameId}/events": { + "get": { + "description": "Get game events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "games" + ], + "summary": "Get game events", + "parameters": [ + { + "type": "integer", + "description": "Game ID", + "name": "gameId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.GameSSERequest" + } + } + } + } + }, + "/api/v1/heartbeat": { + "get": { + "description": "Check if the server is alive", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "heartbeat" + ], + "summary": "Check if the server is alive", + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/player/{id}/player-cards/": { + "get": { + "description": "GetPlayerCardsByPlayerId", + "produces": [ + "application/json" + ], + "tags": [ + "player_cards" + ], + "summary": "GetPlayerCards", + "parameters": [ + { + "type": "integer", + "description": "Player ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.PlayerCardsResponse" + } + } + } + } + }, + "/api/v1/player/{playerId}/transmit-intelligence": { + "post": { + "description": "Transmit an intelligence card", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "players" + ], + "summary": "Transmit intelligence", + "parameters": [ + { + "type": "integer", + "description": "Player ID", + "name": "playerId", + "in": "path", + "required": true + }, + { + "description": "Card ID", + "name": "card_id", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PlayCardRequest" + } + }, + { + "description": "Intelligence Type", + "name": "intelligence_type", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PlayCardRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.PlayCardResponse" + } + } + } + } + }, + "/api/v1/players/{playerId}/accept": { + "post": { + "description": "Decide accept card or not", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "players" + ], + "summary": "Accept Card", + "parameters": [ + { + "type": "integer", + "description": "Player ID", + "name": "playerId", + "in": "path", + "required": true + }, + { + "description": "Accept", + "name": "accept", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AcceptCardRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.PlayCardResponse" + } + } + } + } + }, + "/api/v1/players/{playerId}/player-cards": { + "post": { + "description": "Play a card", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "players" + ], + "summary": "Play a card", + "parameters": [ + { + "type": "integer", + "description": "Player ID", + "name": "playerId", + "in": "path", + "required": true + }, + { + "description": "Card ID", + "name": "card_id", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PlayCardRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.PlayCardResponse" + } + } + } + } + } + }, + "definitions": { + "http.GameSSERequest": { + "type": "object", + "properties": { + "game_id": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "request.AcceptCardRequest": { + "type": "object", + "properties": { + "accept": { + "type": "boolean" + } + } + }, + "request.CreateGameRequest": { + "type": "object", + "properties": { + "players": { + "type": "array", + "items": { + "$ref": "#/definitions/request.PlayerInfo" + } + } + } + }, + "request.CreateGameResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "request.PlayCardRequest": { + "type": "object", + "properties": { + "card_id": { + "type": "integer" + }, + "intelligence_type": { + "type": "integer" + } + } + }, + "request.PlayCardResponse": { + "type": "object", + "properties": { + "result": { + "type": "boolean" + } + } + }, + "request.PlayerCardsResponse": { + "type": "object", + "properties": { + "player_cards": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "request.PlayerInfo": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "127.0.0.1:8080", + BasePath: "", + Schemes: []string{}, + Title: "The Message API", + Description: "This is an online version of the \"The Message\" board game backend API", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/Backend/cmd/app/main.go b/Backend/cmd/app/main.go index 88bc5f3..715110f 100644 --- a/Backend/cmd/app/main.go +++ b/Backend/cmd/app/main.go @@ -1,23 +1,95 @@ package main import ( + _ "github.com/Game-as-a-Service/The-Message/cmd/app/docs" "github.com/Game-as-a-Service/The-Message/config" - "github.com/gin-gonic/gin" - - http "github.com/Game-as-a-Service/The-Message/service/delivery/http/v1" + "github.com/Game-as-a-Service/The-Message/service/delivery/http/v1" mysqlRepo "github.com/Game-as-a-Service/The-Message/service/repository/mysql" + "github.com/Game-as-a-Service/The-Message/service/service" + "github.com/gin-gonic/gin" _ "github.com/joho/godotenv/autoload" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" ) +// @title The Message API +// @description This is an online version of the "The Message" board game backend API +// @host 127.0.0.1:8080 func main() { - db := config.InitDB() + db := config.NewDatabase() engine := gin.Default() + sse := http.NewSSEServer() gameRepo := mysqlRepo.NewGameRepository(db) playerRepo := mysqlRepo.NewPlayerRepository(db) + cardRepo := mysqlRepo.NewCardRepository(db) + deckRepo := mysqlRepo.NewDeckRepository(db) + playerCardRepo := mysqlRepo.NewPlayerCardRepository(db) + gameProgressRepo := mysqlRepo.NewGameProgressRepository(db) + + cardService := service.NewCardService(&service.CardServiceOptions{ + CardRepo: cardRepo, + PlayerRepo: playerRepo, + PlayerCardRepo: playerCardRepo, + GameRepo: gameRepo, + }) + + deckService := service.NewDeckService(&service.DeckServiceOptions{ + DeckRepo: deckRepo, + CardService: cardService, + }) + + playerService := service.NewPlayerService(&service.PlayerServiceOptions{ + PlayerRepo: playerRepo, + PlayerCardRepo: playerCardRepo, + GameRepo: gameRepo, + GameProgressRepo: gameProgressRepo, + }) + + gameService := service.NewGameService( + &service.GameServiceOptions{ + GameRepo: gameRepo, + PlayerService: playerService, + CardService: cardService, + DeckService: deckService, + }, + ) + playerService.GameServ = &gameService + + http.RegisterGameHandler( + &http.GameHandlerOptions{ + Engine: engine, + Service: gameService, + SSE: sse, + }, + ) + + // Register the heartbeat handler + http.RegisterHeartbeatHandler( + &http.HeartbeatHandler{ + Engine: engine, + }) + + http.RegisterCardHandler( + &http.CardHandlerOptions{ + Engine: engine, + Service: cardService, + }, + ) - http.NewGameHandler(engine, gameRepo, playerRepo) + http.RegisterPlayerHandler( + &http.PlayerHandlerOptions{ + Engine: engine, + Service: playerService, + GameService: gameService, + SSE: sse, + }, + ) + engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - engine.Run(":8080") + err := engine.Run(":8080") + if err != nil { + return + } } diff --git a/Backend/cmd/migrate/fresh.go b/Backend/cmd/migrate/fresh.go deleted file mode 100644 index d6ee041..0000000 --- a/Backend/cmd/migrate/fresh.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build migrate - -package main - -import ( - "database/sql" - "fmt" - "github.com/Game-as-a-Service/The-Message/config" - _ "github.com/joho/godotenv/autoload" - "gorm.io/gorm" -) - -func main() { - db := config.InitDB() - tableNames := GetTableNames(db) - TruncateTables(db, tableNames) -} - -func GetTableNames(db *gorm.DB) []string { - var tableNames []string - var rows *sql.Rows - var err error - - rows, err = db.Raw("SHOW TABLES").Rows() - if err != nil { - fmt.Println("Error fetching table names:", err) - return tableNames - } - defer rows.Close() - - for rows.Next() { - var tableName string - rows.Scan(&tableName) - tableNames = append(tableNames, tableName) - } - - return tableNames -} - -func TruncateTables(db *gorm.DB, tableNames []string) { - for _, tableName := range tableNames { - db.Exec(fmt.Sprintf("SET FOREIGN_KEY_CHECKS = 0")) - db.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)) - db.Exec(fmt.Sprintf("SET FOREIGN_KEY_CHECKS = 1")) - } - fmt.Println("All specified tables truncated successfully.") -} diff --git a/Backend/cmd/migrate/game_card_seeder.go b/Backend/cmd/migrate/game_card_seeder.go new file mode 100644 index 0000000..9677b20 --- /dev/null +++ b/Backend/cmd/migrate/game_card_seeder.go @@ -0,0 +1,14 @@ +//go:build migrate + +package main + +import ( + "github.com/Game-as-a-Service/The-Message/config" + "github.com/Game-as-a-Service/The-Message/database/seeders" + _ "github.com/joho/godotenv/autoload" +) + +func main() { + db := config.NewDatabase() + seeders.Run(db) +} diff --git a/Backend/cmd/migrate/migrate.go b/Backend/cmd/migrate/migrate.go index 05d4a31..a182bef 100644 --- a/Backend/cmd/migrate/migrate.go +++ b/Backend/cmd/migrate/migrate.go @@ -3,35 +3,31 @@ package main import ( + "fmt" + "net/url" + "github.com/Game-as-a-Service/The-Message/config" - "github.com/Game-as-a-Service/The-Message/service/repository" - _ "github.com/joho/godotenv/autoload" - "gorm.io/gorm" ) func main() { - db := config.InitDB() - MigrationMysql(db) -} + sourceURL := config.GetSourceURL() -func MigrationMysql(db *gorm.DB) { - err := db.AutoMigrate(&repository.Game{}) - if err != nil { - return - } - err = db.AutoMigrate(&repository.Player{}) - if err != nil { - return - } -} + dsn := config.BaseDSN() + val := url.Values{} + val.Add("multiStatements", "true") + dsn = fmt.Sprintf("%s?%s", dsn, val.Encode()) -func Migration(db *gorm.DB) { - err := db.AutoMigrate(&repository.Game{}) + m, err := config.NewMigration(dsn, sourceURL) if err != nil { - return + panic(err) } - err = db.AutoMigrate(&repository.Player{}) + + err = m.Up() if err != nil { - return + if err.Error() == "no change" { + fmt.Println("no change") + return + } + panic(err) } } diff --git a/Backend/cmd/migrate/refresh.go b/Backend/cmd/migrate/refresh.go new file mode 100644 index 0000000..7c015ea --- /dev/null +++ b/Backend/cmd/migrate/refresh.go @@ -0,0 +1,14 @@ +//go:build migrate + +package main + +import ( + "github.com/Game-as-a-Service/The-Message/config" + "github.com/Game-as-a-Service/The-Message/database/seeders" +) + +func main() { + config.RunRefresh() + db := config.NewDatabase() + seeders.Run(db) +} diff --git a/Backend/cmd/migrate/rollback.go b/Backend/cmd/migrate/rollback.go new file mode 100644 index 0000000..76b187c --- /dev/null +++ b/Backend/cmd/migrate/rollback.go @@ -0,0 +1,32 @@ +//go:build migrate + +package main + +import ( + "fmt" + "net/url" + + "github.com/Game-as-a-Service/The-Message/config" +) + +func main() { + sourceURL := config.GetSourceURL() + + dsn := config.BaseDSN() + val := url.Values{} + val.Add("multiStatements", "true") + dsn = fmt.Sprintf("%s?%s", dsn, val.Encode()) + + m, err := config.NewMigration(dsn, sourceURL) + if err != nil { + panic(err) + } + + err = m.Down() + if err != nil { + if err.Error() == "no change" { + } else { + panic(err) + } + } +} diff --git a/Backend/config/config.go b/Backend/config/config.go index b663f62..d912156 100644 --- a/Backend/config/config.go +++ b/Backend/config/config.go @@ -1,3 +1 @@ package config - - diff --git a/Backend/config/database.go b/Backend/config/database.go index cc15ab2..8ec30c9 100644 --- a/Backend/config/database.go +++ b/Backend/config/database.go @@ -2,28 +2,43 @@ package config import ( "fmt" - "gorm.io/driver/mysql" - "gorm.io/gorm" "log" + "net/url" "os" + + "gorm.io/driver/mysql" + "gorm.io/gorm" ) -func InitDB() *gorm.DB { - dbHost := os.Getenv("DB_HOST") - dbDatabase := os.Getenv("DB_DATABASE") - dbUser := os.Getenv("DB_USER") - dbPwd := os.Getenv("DB_PASSWORD") - dbPort := os.Getenv("DB_PORT") +func NewDatabase() *gorm.DB { + dsn := DefaultDSN() - DSN := GetDSN(dbUser, dbPwd, dbHost, dbPort, dbDatabase) - db, err := gorm.Open(mysql.Open(DSN), &gorm.Config{}) + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatalf("Cannot connect to database: %v", err) - return nil } + return db } -func GetDSN(user string, password string, host string, port string, database string) string { - return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, password, host, port, database) +func DefaultDSN() string { + dsn := BaseDSN() + + val := url.Values{} + val.Add("parseTime", "true") + val.Add("loc", "Local") + + dsn = fmt.Sprintf("%s?%s", dsn, val.Encode()) + return dsn +} + +func BaseDSN() string { + username := os.Getenv("DB_USER") + password := os.Getenv("DB_PASSWORD") + host := os.Getenv("DB_HOST") + port := os.Getenv("DB_PORT") + database := os.Getenv("DB_DATABASE") + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", username, password, host, port, database) + return dsn } diff --git a/Backend/config/migration.go b/Backend/config/migration.go new file mode 100644 index 0000000..c9a0f4f --- /dev/null +++ b/Backend/config/migration.go @@ -0,0 +1,80 @@ +package config + +import ( + "database/sql" + "fmt" + "net/url" + "os" + "strings" + + _ "github.com/go-sql-driver/mysql" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/mysql" + _ "github.com/golang-migrate/migrate/v4/source/file" + _ "github.com/joho/godotenv/autoload" +) + +func NewMigration(dsn string, sourceURL string) (*migrate.Migrate, error) { + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, err + } + defer db.Close() + + if err = db.Ping(); err != nil { + return nil, err + } + + driver, err := mysql.WithInstance(db, &mysql.Config{}) + if err != nil { + return nil, err + } + + m, err := migrate.NewWithDatabaseInstance( + sourceURL, + "mysql", + driver, + ) + if err != nil { + return nil, err + } + + return m, nil +} + +func RunRefresh() { + sourceURL := GetSourceURL() + + dsn := BaseDSN() + val := url.Values{} + val.Add("multiStatements", "true") + dsn = fmt.Sprintf("%s?%s", dsn, val.Encode()) + + m, err := NewMigration(dsn, sourceURL) + if err != nil { + panic(err) + } + + err = m.Down() + if err != nil { + if err.Error() == "no change" { + } else { + panic(err) + } + } + + err = m.Up() + if err != nil { + panic(err) + } +} + +func GetSourceURL() string { + dir, _ := os.Getwd() + dir = strings.SplitAfter(dir, "Backend")[0] + dir = strings.ReplaceAll(dir, "\\", "/") + + sourceURL := "file://" + dir + "/database/migrations" + + return sourceURL +} diff --git a/Backend/config/test_database.go b/Backend/config/test_database.go index f01de5c..82bc76b 100644 --- a/Backend/config/test_database.go +++ b/Backend/config/test_database.go @@ -1,10 +1,12 @@ package config import ( - "github.com/joho/godotenv" - "gorm.io/gorm" + "fmt" "log" "os" + + "github.com/joho/godotenv" + "gorm.io/gorm" ) func InitTestDB() *gorm.DB { @@ -13,7 +15,7 @@ func InitTestDB() *gorm.DB { return nil } - db := InitDB() + db := NewDatabase() return db } @@ -36,3 +38,14 @@ func getEnvPath() (string, error) { envPath := cwd + "/../../.env" return envPath, err } + +func BaseTestDSN() string { + username := os.Getenv("DB_USER") + password := os.Getenv("DB_PASSWORD") + host := os.Getenv("DB_HOST") + port := os.Getenv("DB_PORT") + database := os.Getenv("DB_DATABASE") + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", username, password, host, port, database) + return dsn +} diff --git a/Backend/coverage.out b/Backend/coverage.out new file mode 100644 index 0000000..5f02b11 --- /dev/null +++ b/Backend/coverage.out @@ -0,0 +1 @@ +mode: set diff --git a/Backend/database/migrations/000001_create_games_table.down.sql b/Backend/database/migrations/000001_create_games_table.down.sql new file mode 100644 index 0000000..1007ef2 --- /dev/null +++ b/Backend/database/migrations/000001_create_games_table.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS games; + +COMMIT; diff --git a/Backend/database/migrations/000001_create_games_table.up.sql b/Backend/database/migrations/000001_create_games_table.up.sql new file mode 100644 index 0000000..7d8ab74 --- /dev/null +++ b/Backend/database/migrations/000001_create_games_table.up.sql @@ -0,0 +1,14 @@ +BEGIN; + +CREATE TABLE games +( + id INT AUTO_INCREMENT PRIMARY KEY, + token LONGTEXT NOT NULL, + status VARCHAR(10) NOT NULL, + current_player_id INT, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + deleted_at DATETIME +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +COMMIT; diff --git a/Backend/database/migrations/000002_create_players_table.down.sql b/Backend/database/migrations/000002_create_players_table.down.sql new file mode 100644 index 0000000..5a8fc29 --- /dev/null +++ b/Backend/database/migrations/000002_create_players_table.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS players; + +COMMIT; diff --git a/Backend/database/migrations/000002_create_players_table.up.sql b/Backend/database/migrations/000002_create_players_table.up.sql new file mode 100644 index 0000000..3566df0 --- /dev/null +++ b/Backend/database/migrations/000002_create_players_table.up.sql @@ -0,0 +1,17 @@ +BEGIN; + +CREATE TABLE players +( + id INT AUTO_INCREMENT PRIMARY KEY, + game_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + order_number INT NOT NULL, + identity_card VARCHAR(255) NOT NULL, + status VARCHAR(10) NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + deleted_at DATETIME, + FOREIGN KEY (game_id) REFERENCES games (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +COMMIT; diff --git a/Backend/database/migrations/000003_create_cards_table.down.sql b/Backend/database/migrations/000003_create_cards_table.down.sql new file mode 100644 index 0000000..6d07f2a --- /dev/null +++ b/Backend/database/migrations/000003_create_cards_table.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS cards; + +COMMIT; diff --git a/Backend/database/migrations/000003_create_cards_table.up.sql b/Backend/database/migrations/000003_create_cards_table.up.sql new file mode 100644 index 0000000..9ce71a1 --- /dev/null +++ b/Backend/database/migrations/000003_create_cards_table.up.sql @@ -0,0 +1,13 @@ +BEGIN; + +CREATE TABLE cards +( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + color VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + deleted_at DATETIME +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +COMMIT; diff --git a/Backend/database/migrations/000004_create_decks_table.down.sql b/Backend/database/migrations/000004_create_decks_table.down.sql new file mode 100644 index 0000000..bd69e7b --- /dev/null +++ b/Backend/database/migrations/000004_create_decks_table.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS decks; + +COMMIT; diff --git a/Backend/database/migrations/000004_create_decks_table.up.sql b/Backend/database/migrations/000004_create_decks_table.up.sql new file mode 100644 index 0000000..3179477 --- /dev/null +++ b/Backend/database/migrations/000004_create_decks_table.up.sql @@ -0,0 +1,13 @@ +BEGIN; + +CREATE TABLE decks +( + id INT AUTO_INCREMENT PRIMARY KEY, + game_id INT NOT NULL, + card_id INT NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + deleted_at DATETIME +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +COMMIT; diff --git a/Backend/database/migrations/000005_create_player_cards_table.down.sql b/Backend/database/migrations/000005_create_player_cards_table.down.sql new file mode 100644 index 0000000..5ab9b1a --- /dev/null +++ b/Backend/database/migrations/000005_create_player_cards_table.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS player_cards; + +COMMIT; diff --git a/Backend/database/migrations/000005_create_player_cards_table.up.sql b/Backend/database/migrations/000005_create_player_cards_table.up.sql new file mode 100644 index 0000000..f5f6ef1 --- /dev/null +++ b/Backend/database/migrations/000005_create_player_cards_table.up.sql @@ -0,0 +1,18 @@ +BEGIN; + +CREATE TABLE player_cards +( + id INT AUTO_INCREMENT PRIMARY KEY, + player_id INT NOT NULL, + game_id INT NOT NULL, + card_id INT NOT NULL, + type VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + deleted_at DATETIME, + FOREIGN KEY (player_id) REFERENCES players (id), + FOREIGN KEY (game_id) REFERENCES games (id), + FOREIGN KEY (card_id) REFERENCES cards (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +COMMIT; diff --git a/Backend/database/migrations/000006_create_game_progresses_table.down.sql b/Backend/database/migrations/000006_create_game_progresses_table.down.sql new file mode 100644 index 0000000..9b0ebbb --- /dev/null +++ b/Backend/database/migrations/000006_create_game_progresses_table.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS game_progresses; + +COMMIT; diff --git a/Backend/database/migrations/000006_create_game_progresses_table.up.sql b/Backend/database/migrations/000006_create_game_progresses_table.up.sql new file mode 100644 index 0000000..5551949 --- /dev/null +++ b/Backend/database/migrations/000006_create_game_progresses_table.up.sql @@ -0,0 +1,19 @@ +BEGIN; + +CREATE TABLE game_progresses +( + id INT AUTO_INCREMENT PRIMARY KEY, + player_id INT NOT NULL, + game_id INT NOT NULL, + card_id INT NOT NULL, + action VARCHAR(255) NOT NULL, + target_player_id INT, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + deleted_at DATETIME, + FOREIGN KEY (player_id) REFERENCES players (id), + FOREIGN KEY (game_id) REFERENCES games (id), + FOREIGN KEY (card_id) REFERENCES cards (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +COMMIT; diff --git a/Backend/database/migrations/000007_add_intelligence_type_to_cards_table.down.sql b/Backend/database/migrations/000007_add_intelligence_type_to_cards_table.down.sql new file mode 100644 index 0000000..56a1574 --- /dev/null +++ b/Backend/database/migrations/000007_add_intelligence_type_to_cards_table.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +ALTER TABLE cards DROP COLUMN intelligence_type; + +COMMIT; diff --git a/Backend/database/migrations/000007_add_intelligence_type_to_cards_table.up.sql b/Backend/database/migrations/000007_add_intelligence_type_to_cards_table.up.sql new file mode 100644 index 0000000..0c9aea3 --- /dev/null +++ b/Backend/database/migrations/000007_add_intelligence_type_to_cards_table.up.sql @@ -0,0 +1,5 @@ +BEGIN; + +ALTER TABLE cards ADD COLUMN intelligence_type TINYINT COMMENT '1 密電, 2 直達, 3 文件' AFTER color; + +COMMIT; diff --git a/Backend/database/seeders/database_seeder.go b/Backend/database/seeders/database_seeder.go new file mode 100644 index 0000000..6ee14a4 --- /dev/null +++ b/Backend/database/seeders/database_seeder.go @@ -0,0 +1,9 @@ +package seeders + +import ( + "gorm.io/gorm" +) + +func Run(db *gorm.DB) { + SeederCards(db) +} diff --git a/Backend/database/seeders/game_card_seeder.go b/Backend/database/seeders/game_card_seeder.go new file mode 100644 index 0000000..d6a1461 --- /dev/null +++ b/Backend/database/seeders/game_card_seeder.go @@ -0,0 +1,36 @@ +package seeders + +import ( + "context" + + "github.com/Game-as-a-Service/The-Message/enums" + "github.com/Game-as-a-Service/The-Message/service/repository" + "github.com/Game-as-a-Service/The-Message/service/repository/mysql" + "gorm.io/gorm" +) + +var actionColors = map[string]map[string]int{ + enums.LockOn: {"紅": 5, "藍": 5, "黑": 4}, + enums.LureAway: {"紅": 2, "藍": 2, "黑": 4}, + enums.Intercept: {"紅": 1, "藍": 1, "黑": 6}, + enums.Diversion: {"紅": 2, "藍": 2, "黑": 2}, + enums.Decipher: {"紅": 3, "藍": 3, "黑": 1}, + enums.Burn: {"紅": 1, "藍": 1, "黑": 4}, + enums.SeeThrough: {"紅": 3, "藍": 3, "黑": 6}, + enums.Probe: {"紅": 6, "藍": 6, "黑": 6}, + enums.BlurOfTruth: {"紅": 1, "藍": 1}, +} + +func SeederCards(db *gorm.DB) { + for actionType, colors := range actionColors { + for color, count := range colors { + for i := 0; i < count; i++ { + mysql.NewCardRepository(db).CreateCard(context.TODO(), &repository.Card{ + Name: actionType, + Color: color, + IntelligenceType: enums.ToIntelligenceType(actionType), + }) + } + } + } +} diff --git a/Backend/enums/card_colors.go b/Backend/enums/card_colors.go new file mode 100644 index 0000000..791d847 --- /dev/null +++ b/Backend/enums/card_colors.go @@ -0,0 +1,7 @@ +package enums + +const ( + Red = "紅" + Blue = "藍" + Black = "黑" +) diff --git a/Backend/enums/game_action.go b/Backend/enums/game_action.go new file mode 100644 index 0000000..9156e71 --- /dev/null +++ b/Backend/enums/game_action.go @@ -0,0 +1,5 @@ +package enums + +const ( + TransmitIntelligence = "傳情報" +) diff --git a/Backend/enums/game_cards.go b/Backend/enums/game_cards.go new file mode 100644 index 0000000..cf9ab15 --- /dev/null +++ b/Backend/enums/game_cards.go @@ -0,0 +1,26 @@ +package enums + +const ( + LockOn = "鎖定" + LureAway = "調虎離山" + Probe = "試探" + Intercept = "截獲" + Decipher = "破譯" + Diversion = "退回" + Burn = "燒毀" + BlurOfTruth = "真偽莫辯" + SeeThrough = "識破" +) + +func ToIntelligenceType(actionType string) int { + switch actionType { + case LockOn, LureAway, Probe, Decipher: + return SecretTelegram + case Intercept, Burn, SeeThrough: + return Direct + case Diversion, BlurOfTruth: + return Document + default: + return 0 + } +} diff --git a/Backend/enums/game_status.go b/Backend/enums/game_status.go new file mode 100644 index 0000000..c8fa67f --- /dev/null +++ b/Backend/enums/game_status.go @@ -0,0 +1,8 @@ +package enums + +const ( + GameStart = "開始遊戲" + ActionCardStage = "功能牌階段" + TransmitIntelligenceStage = "情報牌階段" + GameEnd = "結束遊戲" +) diff --git a/Backend/enums/hand_card_types.go b/Backend/enums/hand_card_types.go new file mode 100644 index 0000000..32b3490 --- /dev/null +++ b/Backend/enums/hand_card_types.go @@ -0,0 +1,6 @@ +package enums + +const ( + Intelligence = "intelligence" + Hand = "hand" +) diff --git a/Backend/enums/indentity.go b/Backend/enums/indentity_cards.go similarity index 74% rename from Backend/enums/indentity.go rename to Backend/enums/indentity_cards.go index 991c87c..db2a3d6 100644 --- a/Backend/enums/indentity.go +++ b/Backend/enums/indentity_cards.go @@ -2,6 +2,6 @@ package enums const ( UndercoverFront = "潛伏戰線" - MilitaryIntel = "軍情" + MilitaryAgency = "軍情處" Bystander = "打醬油" ) diff --git a/Backend/enums/intelligence_types.go b/Backend/enums/intelligence_types.go new file mode 100644 index 0000000..ee90690 --- /dev/null +++ b/Backend/enums/intelligence_types.go @@ -0,0 +1,20 @@ +package enums + +const ( + SecretTelegram = 1 + Direct = 2 + Document = 3 +) + +func ToString(secretTelegramType int) string { + switch secretTelegramType { + case SecretTelegram: + return "密電" + case Direct: + return "直達" + case Document: + return "文件" + default: + return "" + } +} diff --git a/Backend/enums/player_status.go b/Backend/enums/player_status.go new file mode 100644 index 0000000..ae650a9 --- /dev/null +++ b/Backend/enums/player_status.go @@ -0,0 +1,6 @@ +package enums + +const ( + PlayerStatusAlive = "生存" + PlayerStatusDead = "死亡" +) diff --git a/Backend/go.mod b/Backend/go.mod index f1ad291..2d1ac81 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -1,47 +1,61 @@ module github.com/Game-as-a-Service/The-Message -go 1.21.0 +go 1.22 require ( - github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/gin-gonic/gin v1.9.1 - github.com/stretchr/testify v1.8.3 - gorm.io/driver/mysql v1.5.1 - gorm.io/gorm v1.25.4 + github.com/go-faker/faker/v4 v4.3.0 + github.com/go-sql-driver/mysql v1.7.1 + github.com/golang-migrate/migrate/v4 v4.17.0 + github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.8.4 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.3 + gorm.io/driver/mysql v1.5.4 + gorm.io/gorm v1.25.7 ) require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/bytedance/sonic v1.10.2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/spec v0.20.14 // indirect + github.com/go-openapi/swag v0.22.9 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/go-playground/validator/v10 v10.18.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.18.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/Backend/go.sum b/Backend/go.sum index 049652b..f6be5e2 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -1,70 +1,123 @@ -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= +github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M= +github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-faker/faker/v4 v4.3.0 h1:UXOW7kn/Mwd0u6MR30JjUKVzguT20EB/hBOddAAO+DY= +github.com/go-faker/faker/v4 v4.3.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol/dhcpRub4= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= +github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= +github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 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/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 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/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -73,39 +126,75 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= -gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= -gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= -gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/driver/mysql v1.5.4 h1:igQmHfKcbaTVyAIHNhhB888vvxh8EdQ2uSUT0LPcBso= +gorm.io/driver/mysql v1.5.4/go.mod h1:9rYxJph/u9SWkWc9yY4XJ1F/+xO0S/ChOmbk3+Z5Tvs= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/Backend/service/delivery/http/v1/card_handler.go b/Backend/service/delivery/http/v1/card_handler.go new file mode 100644 index 0000000..bb34d69 --- /dev/null +++ b/Backend/service/delivery/http/v1/card_handler.go @@ -0,0 +1,56 @@ +package http + +import ( + "net/http" + "strconv" + + "github.com/Game-as-a-Service/The-Message/service/service" + "github.com/gin-gonic/gin" +) + +type CardHandler struct { + cardService service.CardService +} + +type CardHandlerOptions struct { + Engine *gin.Engine + Service service.CardService +} + +func RegisterCardHandler(opts *CardHandlerOptions) { + handler := &CardHandler{ + cardService: opts.Service, + } + + opts.Engine.GET("/api/v1/player/:playerId/player-cards/", handler.GetPlayerCards) +} + +// GetPlayerCards godoc +// @Summary GetPlayerCards +// @Description GetPlayerCardsByPlayerId +// @Tags player_cards +// @Produce json +// @Param id path int true "Player ID" +// @Success 200 {object} request.PlayerCardsResponse +// @Router /api/v1/player/{id}/player-cards/ [get] +func (p *CardHandler) GetPlayerCards(c *gin.Context) { + playerId, _ := strconv.Atoi(c.Param("playerId")) + playerCards, err := p.cardService.GetPlayerCardsByPlayerId(c, playerId) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + + playerCardsInfo := []map[string]interface{}{} + + for _, card := range playerCards { + dict := map[string]interface{}{ + "id": card.Id, + "name": card.Name, + "color": card.Color, + } + playerCardsInfo = append(playerCardsInfo, dict) + } + + c.JSON(http.StatusOK, gin.H{"player_cards": playerCardsInfo}) +} diff --git a/Backend/service/delivery/http/v1/game_handler.go b/Backend/service/delivery/http/v1/game_handler.go index 04b67ad..91d9cb2 100644 --- a/Backend/service/delivery/http/v1/game_handler.go +++ b/Backend/service/delivery/http/v1/game_handler.go @@ -1,79 +1,107 @@ package http import ( - "crypto/sha256" - "encoding/hex" + "encoding/json" + "io" + "log" "net/http" "strconv" - "github.com/Game-as-a-Service/The-Message/service/repository" - mysqlRepo "github.com/Game-as-a-Service/The-Message/service/repository/mysql" + "github.com/Game-as-a-Service/The-Message/enums" "github.com/Game-as-a-Service/The-Message/service/request" - service "github.com/Game-as-a-Service/The-Message/service/service" + "github.com/Game-as-a-Service/The-Message/service/service" "github.com/gin-gonic/gin" ) type GameHandler struct { - GameRepo repository.GameRepository - PlayerRepo repository.PlayerRepository + gameService service.GameService + SSE *Event } -func NewGameHandler(engine *gin.Engine, gameRepo *mysqlRepo.GameRepository, playerRepo *mysqlRepo.PlayerRepository) *GameHandler { +type GameHandlerOptions struct { + Engine *gin.Engine + Service service.GameService + SSE *Event +} + +func RegisterGameHandler(opts *GameHandlerOptions) { handler := &GameHandler{ - GameRepo: gameRepo, - PlayerRepo: playerRepo, + gameService: opts.Service, + SSE: opts.SSE, } - engine.POST("/api/v1/games", handler.StartGame) - engine.Static("/swagger", "./web/swagger-ui") - return handler + + opts.Engine.POST("/api/v1/games", handler.StartGame) + opts.Engine.GET("/api/v1/games/:gameId/events", HeadersMiddleware(), opts.SSE.serveHTTP(), handler.GameEvent) } -func (g *GameHandler) GetGame(c *gin.Context) { - gameId, _ := strconv.Atoi(c.Param("gameId")) +// StartGame godoc +// @Summary Start a new game +// @Description Start a new game +// @Tags games +// @Accept json +// @Produce json +// @Param players body request.CreateGameRequest true "Players" +// @Success 200 {object} request.CreateGameResponse +// @Router /api/v1/games [post] +func (g *GameHandler) StartGame(c *gin.Context) { + var req request.CreateGameRequest - game, err := g.GameRepo.GetGameById(c, gameId) + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + game, err := g.gameService.InitGame(c) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) return } - c.JSON(http.StatusOK, gin.H{ - "Id": game.Id, - "Token": game.Token, - }) -} + // TODO 這邊可以優化 https://gorm.io/zh_CN/docs/associations.html + if err := g.gameService.PlayerService.InitPlayers(c, game, req); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } -func (g *GameHandler) StartGame(c *gin.Context) { - var req request.CreateGameRequest + game, _ = g.gameService.GetGameById(c, game.Id) + g.gameService.UpdateCurrentPlayer(c, game, game.Players[0].Id) + g.gameService.UpdateStatus(c, game, enums.ActionCardStage) - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if err := g.gameService.InitDeck(c, game); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) return } - game := new(repository.Game) - jwtToken := "the-message" // 先亂寫Token - jwtBytes := []byte(jwtToken) - hash := sha256.Sum256(jwtBytes) - hashString := hex.EncodeToString(hash[:]) - game.Token = hashString - - game, err := g.GameRepo.CreateGame(c, game) + if err := g.gameService.DrawCardsForAllPlayers(c, game); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } - // 建立身份牌牌堆 - identityCards := service.InitIdentityCards(len(req.Players)) + game, err = g.gameService.GetGameById(c, game.Id) + if err != nil { + return + } - for i, reqPlayer := range req.Players { - player := new(repository.Player) - player.Name = reqPlayer.Name - player.GameId = game.Id - player.IdentityCard = identityCards[i] - _, err = g.PlayerRepo.CreatePlayer(c, player) + g.SSE.Message <- gin.H{ + "message": "Game started", + "status": "started", + "game_id": game.Id, + "next_player": game.Players[0].Id, } + c.JSON(http.StatusOK, gin.H{ + "Id": game.Id, + "Token": game.Token, + }) +} + +func (g *GameHandler) GetGame(c *gin.Context) { + gameId, _ := strconv.Atoi(c.Param("gameId")) + + game, err := g.gameService.GetGameById(c, gameId) + if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) return } @@ -86,7 +114,7 @@ func (g *GameHandler) StartGame(c *gin.Context) { func (g *GameHandler) DeleteGame(c *gin.Context) { gameId, _ := strconv.Atoi(c.Param("gameId")) - err := g.GameRepo.DeleteGame(c, gameId) + err := g.gameService.DeleteGame(c, gameId) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) @@ -95,3 +123,64 @@ func (g *GameHandler) DeleteGame(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Game deleted"}) } + +// GameEvent godoc +// @Summary Get game events +// @Description Get game events +// @Tags games +// @Accept json +// @Produce json +// @Param gameId path int true "Game ID" +// @Success 200 {object} GameSSERequest +// @Router /api/v1/games/{gameId}/events [get] +func (g *GameHandler) GameEvent(c *gin.Context) { + gameId, err := strconv.Atoi(c.Param("gameId")) + if err != nil { + return + } + + v, ok := c.Get("clientChan") + if !ok { + return + } + + clientChan, ok := v.(ClientChan) + if !ok { + return + } + + game, err := g.gameService.GetGameById(c, gameId) + if err != nil { + return + } + + g.SSE.Message <- gin.H{ + "message": game.Status, + "status": game.Status, + "game_id": gameId, + "current_player": game.CurrentPlayerId, + } + + c.Stream(func(w io.Writer) bool { + if msg, ok := <-clientChan; ok { + log.Printf("msg: %+v", msg) + data := GameSSERequest{} + err := json.Unmarshal([]byte(msg), &data) + if err != nil { + log.Fatalf(err.Error()) + } + + if data.GameId == gameId { + c.SSEvent("message", msg) + } + return true + } + return false + }) +} + +type GameSSERequest struct { + GameId int `json:"game_id,int"` + Message string `json:"message"` + Status string `json:"status"` +} diff --git a/Backend/service/delivery/http/v1/heartbeat_handler.go b/Backend/service/delivery/http/v1/heartbeat_handler.go new file mode 100644 index 0000000..a3a07fb --- /dev/null +++ b/Backend/service/delivery/http/v1/heartbeat_handler.go @@ -0,0 +1,29 @@ +package http + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type HeartbeatHandler struct { + Engine *gin.Engine +} + +func RegisterHeartbeatHandler(opts *HeartbeatHandler) { + handler := &HeartbeatHandler{} + + opts.Engine.GET("/api/v1/heartbeat", handler.Heartbeat) +} + +// Heartbeat godoc +// @Summary Check if the server is alive +// @Description Check if the server is alive +// @Tags heartbeat +// @Accept json +// @Produce json +// @Success 204 +// @Router /api/v1/heartbeat [GET] +func (g *HeartbeatHandler) Heartbeat(c *gin.Context) { + c.JSON(http.StatusNoContent, http.NoBody) +} diff --git a/Backend/service/delivery/http/v1/player_handler.go b/Backend/service/delivery/http/v1/player_handler.go new file mode 100644 index 0000000..7f7b977 --- /dev/null +++ b/Backend/service/delivery/http/v1/player_handler.go @@ -0,0 +1,159 @@ +package http + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/Game-as-a-Service/The-Message/service/request" + "github.com/Game-as-a-Service/The-Message/service/service" + "github.com/gin-gonic/gin" +) + +type PlayerHandler struct { + playerService service.PlayerService + gameService service.GameService + SSE *Event +} + +type PlayerHandlerOptions struct { + Engine *gin.Engine + Service service.PlayerService + GameService service.GameService + SSE *Event +} + +func RegisterPlayerHandler(opts *PlayerHandlerOptions) { + handler := &PlayerHandler{ + playerService: opts.Service, + gameService: opts.GameService, + SSE: opts.SSE, + } + + opts.Engine.POST("/api/v1/players/:playerId/player-cards", handler.PlayCard) + opts.Engine.POST("/api/v1/player/:playerId/transmit-intelligence", handler.TransmitIntelligence) + opts.Engine.POST("/api/v1/players/:playerId/accept", handler.AcceptCard) +} + +// PlayCard godoc +// @Summary Play a card +// @Description Play a card +// @Tags players +// @Accept json +// @Produce json +// @Param playerId path int true "Player ID" +// @Param card_id body request.PlayCardRequest true "Card ID" +// @Success 200 {object} request.PlayCardResponse +// @Router /api/v1/players/{playerId}/player-cards [post] +func (p *PlayerHandler) PlayCard(c *gin.Context) { + playerId, _ := strconv.Atoi(c.Param("playerId")) + var req request.PlayCardRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + game, card, err := p.playerService.PlayCard(c, playerId, req.CardID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) + return + } + + // TODO to Service + p.SSE.Message <- gin.H{ + "game_id": game.Id, + "status": game.Status, + "message": fmt.Sprintf("玩家: %d 已出牌", playerId), + "card": card.Name, + "next_player": game.CurrentPlayerId, + } + + c.JSON(http.StatusOK, gin.H{ + "result": true, + }) +} + +// TransmitIntelligence godoc +// @Summary Transmit intelligence +// @Description Transmit an intelligence card +// @Tags players +// @Accept json +// @Produce json +// @Param playerId path int true "Player ID" +// @Param card_id body request.PlayCardRequest true "Card ID" +// @Success 200 {object} request.PlayCardResponse +// @Router /api/v1/player/{playerId}/transmit-intelligence [post] +func (p *PlayerHandler) TransmitIntelligence(c *gin.Context) { + playerId, _ := strconv.Atoi(c.Param("playerId")) + var req request.PlayCardRequest + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) + return + } + + player, err := p.playerService.GetPlayerById(c, playerId) + + if err != nil || player == nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Player not found"}) + return + } + + // Check card_id exists in player_cards + exist, err := p.playerService.CheckPlayerCardExist(c, playerId, player.GameId, req.CardID) + if err != nil || !exist { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Card not found"}) + return + } + + ret, err := p.playerService.TransmitIntelligenceCard(c, playerId, player.GameId, req.CardID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "result": ret, + }) +} + +// AcceptCard godoc +// @Summary Accept Card +// @Description Decide accept card or not +// @Tags players +// @Accept json +// @Produce json +// @Param playerId path int true "Player ID" +// @Param accept body request.AcceptCardRequest true "Accept" +// @Success 200 {object} request.PlayCardResponse +// @Router /api/v1/players/{playerId}/accept [post] +func (p *PlayerHandler) AcceptCard(c *gin.Context) { + playerId, _ := strconv.Atoi(c.Param("playerId")) + var req request.AcceptCardRequest + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) + return + } + + result, err := p.playerService.AcceptCard(c, playerId, req.Accept) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + + winner, err := p.playerService.CheckWin(c, playerId) + if winner != nil { + p.SSE.Message <- gin.H{ + "game_id": winner.Game.Id, + "status": winner.Game.Status, + "message": fmt.Sprintf("玩家: %d 已贏得遊戲", playerId), + "winner": winner.Name, + } + } + + c.JSON(http.StatusOK, gin.H{ + "result": result, + }) + +} diff --git a/Backend/service/delivery/http/v1/sse_handler.go b/Backend/service/delivery/http/v1/sse_handler.go new file mode 100644 index 0000000..e603ca4 --- /dev/null +++ b/Backend/service/delivery/http/v1/sse_handler.go @@ -0,0 +1,81 @@ +package http + +import ( + "encoding/json" + "log" + + "github.com/gin-gonic/gin" +) + +type Event struct { + Message chan map[string]any + NewClients chan chan string + ClosedClients chan chan string + TotalClients map[chan string]any +} + +type ClientChan chan string + +func NewSSEServer() (event *Event) { + event = &Event{ + Message: make(chan map[string]any), + NewClients: make(chan chan string), + ClosedClients: make(chan chan string), + TotalClients: make(map[chan string]any), + } + + go event.listen() + + return +} + +func (stream *Event) listen() { + for { + select { + case client := <-stream.NewClients: + stream.TotalClients[client] = true + log.Printf("Client added. %d registered clients", len(stream.TotalClients)) + + case client := <-stream.ClosedClients: + delete(stream.TotalClients, client) + close(client) + log.Printf("Removed client. %d registered clients", len(stream.TotalClients)) + + case eventMsg := <-stream.Message: + jsonMsg, err := json.Marshal(eventMsg) + if err != nil { + continue + } + + for clientMessageChan := range stream.TotalClients { + clientMessageChan <- string(jsonMsg) + } + } + } +} + +func (stream *Event) serveHTTP() gin.HandlerFunc { + return func(c *gin.Context) { + clientChan := make(ClientChan) + + stream.NewClients <- clientChan + + defer func() { + stream.ClosedClients <- clientChan + }() + + c.Set("clientChan", clientChan) + + c.Next() + } +} + +func HeadersMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + c.Next() + } +} diff --git a/Backend/service/repository/card_repository.go b/Backend/service/repository/card_repository.go new file mode 100644 index 0000000..ab174b3 --- /dev/null +++ b/Backend/service/repository/card_repository.go @@ -0,0 +1,27 @@ +package repository + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type Card struct { + gorm.Model + Id int `gorm:"primaryKey;auto_increment"` + Name string + Color string + IntelligenceType int + PlayerCards []PlayerCard + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt +} + +type CardRepository interface { + GetCardById(ctx context.Context, id int) (*Card, error) + CreateCard(ctx context.Context, card *Card) (*Card, error) + GetCards(ctx context.Context) ([]*Card, error) + GetPlayerCardsByPlayerId(ctx context.Context, id int, gid int) ([]*Card, error) +} diff --git a/Backend/service/repository/deck_repository.go b/Backend/service/repository/deck_repository.go new file mode 100644 index 0000000..580c249 --- /dev/null +++ b/Backend/service/repository/deck_repository.go @@ -0,0 +1,24 @@ +package repository + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type Deck struct { + gorm.Model + Id int `gorm:"primaryKey;auto_increment"` + GameId int + CardId int + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt +} + +type DeckRepository interface { + CreateDeck(ctx context.Context, deck *Deck) (*Deck, error) + GetDecksByGameId(ctx context.Context, id int) ([]*Deck, error) + DeleteDeck(ctx context.Context, id int) error +} diff --git a/Backend/service/repository/game_progresses_repository.go b/Backend/service/repository/game_progresses_repository.go new file mode 100644 index 0000000..77630f9 --- /dev/null +++ b/Backend/service/repository/game_progresses_repository.go @@ -0,0 +1,26 @@ +package repository + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type GameProgresses struct { + Id int `gorm:"primaryKey;auto_increment"` + PlayerId int + GameId int + CardId int + Action string + TargetPlayerId int + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt +} + +type GameProgressesRepository interface { + CreateGameProgress(c context.Context, gameProgress *GameProgresses) (*GameProgresses, error) + GetGameProgresses(c context.Context, targetPlayerId int, gameId int) (*GameProgresses, error) + UpdateGameProgress(c context.Context, gameProgress *GameProgresses, next_playerId int) (*GameProgresses, error) +} diff --git a/Backend/service/repository/gameRepository.go b/Backend/service/repository/game_repository.go similarity index 52% rename from Backend/service/repository/gameRepository.go rename to Backend/service/repository/game_repository.go index 924e9f0..9e1c506 100644 --- a/Backend/service/repository/gameRepository.go +++ b/Backend/service/repository/game_repository.go @@ -9,12 +9,14 @@ import ( type Game struct { gorm.Model - Id int `gorm:"primaryKey;auto_increment"` - Token string - Players []Player - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoCreateTime"` - DeletedAt gorm.DeletedAt + Id int `gorm:"primaryKey;auto_increment"` + Token string + Status string + CurrentPlayerId int + Players []Player + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt } type GameRepository interface { @@ -22,4 +24,5 @@ type GameRepository interface { CreateGame(ctx context.Context, game *Game) (*Game, error) DeleteGame(ctx context.Context, id int) error GetGameWithPlayers(ctx context.Context, id int) (*Game, error) + UpdateGame(ctx context.Context, game *Game) error } diff --git a/Backend/service/repository/mysql/card_repository.go b/Backend/service/repository/mysql/card_repository.go new file mode 100644 index 0000000..9ba518b --- /dev/null +++ b/Backend/service/repository/mysql/card_repository.go @@ -0,0 +1,76 @@ +package mysql + +import ( + "context" + + "github.com/Game-as-a-Service/The-Message/service/repository" + "gorm.io/gorm" +) + +type CardRepository struct { + db *gorm.DB +} + +func NewCardRepository(db *gorm.DB) *CardRepository { + return &CardRepository{ + db: db, + } +} + +func (c *CardRepository) GetCardById(ctx context.Context, id int) (*repository.Card, error) { + card := new(repository.Card) + + result := c.db.First(&card, "id = ?", id) + + if result.Error != nil { + return nil, result.Error + } + + return card, nil +} + +func (c *CardRepository) CreateCard(ctx context.Context, card *repository.Card) (*repository.Card, error) { + + result := c.db.Create(&card) + + if result.Error != nil { + return nil, result.Error + } + + return card, nil +} + +func (c *CardRepository) GetCards(ctx context.Context) ([]*repository.Card, error) { + var cards []*repository.Card + + result := c.db.Find(&cards) + + if result.Error != nil { + return nil, result.Error + } + + return cards, nil +} + +func (c *CardRepository) GetPlayerCardsByPlayerId(ctx context.Context, id int, gid int) ([]*repository.Card, error) { + + var playerCards []*repository.PlayerCard + var cards []*repository.Card + result := c.db.Find(&playerCards, "player_id = ? AND game_id = ?", id, gid) + + if result.Error != nil { + return nil, result.Error + } + var cardIDs []int + for _, pc := range playerCards { + cardIDs = append(cardIDs, pc.CardId) + } + + result = c.db.Find(&cards, "id IN ?", cardIDs) + if result.Error != nil { + return nil, result.Error + } + + return cards, nil + +} diff --git a/Backend/service/repository/mysql/deck_repository.go b/Backend/service/repository/mysql/deck_repository.go new file mode 100644 index 0000000..01e880f --- /dev/null +++ b/Backend/service/repository/mysql/deck_repository.go @@ -0,0 +1,52 @@ +package mysql + +import ( + "context" + + "github.com/Game-as-a-Service/The-Message/service/repository" + "gorm.io/gorm" +) + +type DeckRepository struct { + db *gorm.DB +} + +func NewDeckRepository(db *gorm.DB) *DeckRepository { + return &DeckRepository{ + db: db, + } +} + +func (d *DeckRepository) CreateDeck(ctx context.Context, deck *repository.Deck) (*repository.Deck, error) { + result := d.db.Create(&deck) + + if result.Error != nil { + return nil, result.Error + } + + return deck, nil +} + +func (d *DeckRepository) GetDecksByGameId(ctx context.Context, id int) ([]*repository.Deck, error) { + var decks []*repository.Deck + + result := d.db.Find(&decks, "game_id = ?", id) + + if result.Error != nil { + return nil, result.Error + } + + return decks, nil +} + +func (d *DeckRepository) DeleteDeck(ctx context.Context, id int) error { + deck := new(repository.Deck) + + result := d.db.Delete(&deck, "id = ?", id) + + if result.Error != nil { + return result.Error + } + + return nil +} diff --git a/Backend/service/repository/mysql/game_process_repository.go b/Backend/service/repository/mysql/game_process_repository.go new file mode 100644 index 0000000..04cc433 --- /dev/null +++ b/Backend/service/repository/mysql/game_process_repository.go @@ -0,0 +1,53 @@ +package mysql + +import ( + "context" + + "github.com/Game-as-a-Service/The-Message/service/repository" + "gorm.io/gorm" +) + +type GameProgressRepository struct { + db *gorm.DB +} + +func NewGameProgressRepository(db *gorm.DB) *GameProgressRepository { + return &GameProgressRepository{ + db: db, + } +} + +func (g *GameProgressRepository) CreateGameProgress(c context.Context, gameProgress *repository.GameProgresses) (*repository.GameProgresses, error) { + result := g.db.Create(&gameProgress) + + if result.Error != nil { + return nil, result.Error + } + + return gameProgress, nil +} + +func (g *GameProgressRepository) GetGameProgresses(c context.Context, targetPlayerId int, gameId int) (*repository.GameProgresses, error) { + var gameProgress *repository.GameProgresses + + result := g.db.First(&gameProgress, "target_player_id = ? AND game_id = ?", targetPlayerId, gameId) + if result.Error != nil { + return nil, result.Error + } + + return gameProgress, nil +} + +func (g *GameProgressRepository) UpdateGameProgress(c context.Context, gameProgress *repository.GameProgresses, next_playerId int) (*repository.GameProgresses, error) { + // result := g.db.Update(&gameProgress) + result := g.db.First(&gameProgress) + + gameProgress.TargetPlayerId = next_playerId + g.db.Save(&gameProgress) + + if result.Error != nil { + return nil, result.Error + } + + return gameProgress, nil +} diff --git a/Backend/service/repository/mysql/mysqlGameRepository.go b/Backend/service/repository/mysql/game_repository.go similarity index 63% rename from Backend/service/repository/mysql/mysqlGameRepository.go rename to Backend/service/repository/mysql/game_repository.go index d01a073..aae3fcc 100644 --- a/Backend/service/repository/mysql/mysqlGameRepository.go +++ b/Backend/service/repository/mysql/game_repository.go @@ -17,10 +17,10 @@ func NewGameRepository(db *gorm.DB) *GameRepository { } } -func (p *GameRepository) GetGameById(ctx context.Context, id int) (*repository.Game, error) { +func (g *GameRepository) GetGameById(ctx context.Context, id int) (*repository.Game, error) { game := new(repository.Game) - result := p.db.Table("games").First(game, "id = ?", id) + result := g.db.First(&game, "id = ?", id) if result.Error != nil { return nil, result.Error @@ -29,9 +29,9 @@ func (p *GameRepository) GetGameById(ctx context.Context, id int) (*repository.G return game, nil } -func (p *GameRepository) CreateGame(ctx context.Context, game *repository.Game) (*repository.Game, error) { +func (g *GameRepository) CreateGame(ctx context.Context, game *repository.Game) (*repository.Game, error) { - result := p.db.Table("games").Create(game) + result := g.db.Create(&game) if result.Error != nil { return nil, result.Error @@ -40,10 +40,10 @@ func (p *GameRepository) CreateGame(ctx context.Context, game *repository.Game) return game, nil } -func (p *GameRepository) DeleteGame(ctx context.Context, id int) error { +func (g *GameRepository) DeleteGame(ctx context.Context, id int) error { game := new(repository.Game) - result := p.db.Table("games").Delete(game, "id = ?", id) + result := g.db.Delete(&game, "id = ?", id) if result.Error != nil { return result.Error @@ -59,3 +59,13 @@ func (g *GameRepository) GetGameWithPlayers(ctx context.Context, id int) (*repos } return &game, nil } + +func (g *GameRepository) UpdateGame(ctx context.Context, game *repository.Game) error { + result := g.db.Save(&game) + + if result.Error != nil { + return result.Error + } + + return nil +} diff --git a/Backend/service/repository/mysql/mysqlPlayerRepository.go b/Backend/service/repository/mysql/mysqlPlayerRepository.go deleted file mode 100644 index f62f643..0000000 --- a/Backend/service/repository/mysql/mysqlPlayerRepository.go +++ /dev/null @@ -1,23 +0,0 @@ -package mysql - -import ( - "context" - - "github.com/Game-as-a-Service/The-Message/service/repository" - "gorm.io/gorm" -) - -type PlayerRepository struct { - db *gorm.DB -} - -func NewPlayerRepository(db *gorm.DB) *PlayerRepository { - return &PlayerRepository{ - db: db, - } -} - -func (p *PlayerRepository) CreatePlayer(ctx context.Context, player *repository.Player) (*repository.Player, error) { - err := p.db.Table("players").Create(player).Error - return player, err -} diff --git a/Backend/service/repository/mysql/player_card_repository.go b/Backend/service/repository/mysql/player_card_repository.go new file mode 100644 index 0000000..ebfd84b --- /dev/null +++ b/Backend/service/repository/mysql/player_card_repository.go @@ -0,0 +1,98 @@ +package mysql + +import ( + "context" + "errors" + + "github.com/Game-as-a-Service/The-Message/service/repository" + "gorm.io/gorm" +) + +type PlayerCardRepository struct { + db *gorm.DB +} + +func NewPlayerCardRepository(db *gorm.DB) *PlayerCardRepository { + return &PlayerCardRepository{ + db: db, + } +} + +func (p PlayerCardRepository) CreatePlayerCard(ctx context.Context, card *repository.PlayerCard) (*repository.PlayerCard, error) { + result := p.db.Create(&card) + + if result.Error != nil { + return nil, result.Error + } + + return card, nil +} + +func (p PlayerCardRepository) DeletePlayerCard(ctx context.Context, id int) error { + card := new(repository.PlayerCard) + + result := p.db.Delete(&card, "id = ?", id) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (p PlayerCardRepository) DeletePlayerCardByPlayerIdAndCardId(ctx context.Context, playerId int, gameId int, cardId int) (bool, error) { + card := new(repository.PlayerCard) + + result := p.db.Delete(&card, "player_id = ? AND game_id = ? AND card_id = ?", playerId, gameId, cardId) + if result.Error != nil { + return false, result.Error + } + + return true, nil +} + +func (p *PlayerCardRepository) ExistPlayerCardByPlayerIdAndCardId(ctx context.Context, playerId int, gameId int, cardId int) (bool, error) { + var card repository.PlayerCard + result := p.db.First(&card, "player_id = ? AND game_id = ? AND card_id = ?", playerId, gameId, cardId) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return false, nil + } + return false, result.Error + } + return true, nil +} + +func (p PlayerCardRepository) GetPlayerCardById(ctx context.Context, id int) (*repository.PlayerCard, error) { + card := new(repository.PlayerCard) + + result := p.db.First(&card, "id = ?", id) + + if result.Error != nil { + return nil, result.Error + } + + return card, nil +} + +func (p *PlayerCardRepository) GetPlayerCards(ctx context.Context, playerCard *repository.PlayerCard) (*[]repository.PlayerCard, error) { + var playerCards *[]repository.PlayerCard + result := p.db.Model(&repository.PlayerCard{}).Preload("Card").Where(&playerCard).Find(&playerCards) + if result.Error != nil { + return nil, result.Error + } + + return playerCards, nil +} + +func (p PlayerCardRepository) GetPlayerCardsByGameId(ctx context.Context, id int) ([]*repository.PlayerCard, error) { + var cards []*repository.PlayerCard + + result := p.db.Find(&cards, "game_id = ?", id) + + if result.Error != nil { + return nil, result.Error + } + + return cards, nil +} diff --git a/Backend/service/repository/mysql/player_repository.go b/Backend/service/repository/mysql/player_repository.go new file mode 100644 index 0000000..5fe8914 --- /dev/null +++ b/Backend/service/repository/mysql/player_repository.go @@ -0,0 +1,72 @@ +package mysql + +import ( + "context" + + "github.com/Game-as-a-Service/The-Message/service/repository" + "gorm.io/gorm" +) + +type PlayerRepository struct { + db *gorm.DB +} + +func NewPlayerRepository(db *gorm.DB) *PlayerRepository { + return &PlayerRepository{ + db: db, + } +} + +func (p *PlayerRepository) CreatePlayer(ctx context.Context, player *repository.Player) (*repository.Player, error) { + err := p.db.Create(&player).Error + return player, err +} + +func (p *PlayerRepository) GetPlayerById(ctx context.Context, playerId int) (*repository.Player, error) { + player := new(repository.Player) + + result := p.db.First(&player, "id = ?", playerId) + + if result.Error != nil { + return nil, result.Error + } + + return player, nil +} + +func (p *PlayerRepository) GetPlayersByGameId(ctx context.Context, id int) ([]*repository.Player, error) { + var players []*repository.Player + + result := p.db.Find(&players, "game_id = ?", id) + + if result.Error != nil { + return nil, result.Error + } + + return players, nil +} + +func (p *PlayerRepository) GetPlayerWithPlayerCards(ctx context.Context, playerId int) (*repository.Player, error) { + var player repository.Player + if err := p.db.Preload("PlayerCards").Preload("PlayerCards.Card").First(&player, playerId).Error; err != nil { + return nil, err + } + return &player, nil +} + +func (p *PlayerRepository) GetPlayerWithGame(ctx context.Context, playerId int) (*repository.Player, error) { + var player repository.Player + if err := p.db.Preload("Game").First(&player, playerId).Error; err != nil { + return nil, err + } + return &player, nil +} + +func (p *PlayerRepository) GetPlayerWithGamePlayersAndPlayerCardsCard(ctx context.Context, playerId int) (*repository.Player, error) { + var player repository.Player + if err := p.db.Preload("Game.Players.PlayerCards.Card").Preload("Game.Players.Game").Preload("PlayerCards.Card").First(&player, playerId).Error; err != nil { + return nil, err + } + + return &player, nil +} diff --git a/Backend/service/repository/playerRepository.go b/Backend/service/repository/playerRepository.go deleted file mode 100644 index fef1a2b..0000000 --- a/Backend/service/repository/playerRepository.go +++ /dev/null @@ -1,23 +0,0 @@ -package repository - -import ( - "context" - "time" - - "gorm.io/gorm" -) - -type Player struct { - gorm.Model - Id int `gorm:"primaryKey;auto_increment"` - Name string - GameId int `gorm:"foreignKey:GameId;references:Id"` - IdentityCard string - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoCreateTime"` - DeletedAt gorm.DeletedAt -} - -type PlayerRepository interface { - CreatePlayer(ctx context.Context, player *Player) (*Player, error) -} diff --git a/Backend/service/repository/player_card_repository.go b/Backend/service/repository/player_card_repository.go new file mode 100644 index 0000000..e833bd2 --- /dev/null +++ b/Backend/service/repository/player_card_repository.go @@ -0,0 +1,32 @@ +package repository + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type PlayerCard struct { + gorm.Model + Id int `gorm:"primaryKey;auto_increment"` + PlayerId int + GameId int + CardId int + Type string + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt + Card Card `gorm:"foreignKey:CardId"` + Player Player `gorm:"foreignKey:PlayerId"` +} + +type PlayerCardRepository interface { + GetPlayerCardById(ctx context.Context, id int) (*PlayerCard, error) + GetPlayerCardsByGameId(ctx context.Context, id int) ([]*PlayerCard, error) + CreatePlayerCard(ctx context.Context, card *PlayerCard) (*PlayerCard, error) + DeletePlayerCard(ctx context.Context, id int) error + DeletePlayerCardByPlayerIdAndCardId(ctx context.Context, playerId int, gameId int, cardId int) (bool, error) + ExistPlayerCardByPlayerIdAndCardId(ctx context.Context, playerId int, gameId int, cardId int) (bool, error) + GetPlayerCards(ctx context.Context, playerCard *PlayerCard) (*[]PlayerCard, error) +} diff --git a/Backend/service/repository/player_repository.go b/Backend/service/repository/player_repository.go new file mode 100644 index 0000000..9aceabf --- /dev/null +++ b/Backend/service/repository/player_repository.go @@ -0,0 +1,32 @@ +package repository + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type Player struct { + gorm.Model + Id int `gorm:"primaryKey;auto_increment"` + Name string + GameId int `gorm:"foreignKey:GameId;references:Id"` + IdentityCard string + Status string + OrderNumber int + PlayerCards []PlayerCard + Game *Game `gorm:"foreignKey:GameId;references:Id"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt +} + +type PlayerRepository interface { + CreatePlayer(ctx context.Context, player *Player) (*Player, error) + GetPlayerById(ctx context.Context, playerId int) (*Player, error) + GetPlayersByGameId(ctx context.Context, id int) ([]*Player, error) + GetPlayerWithPlayerCards(ctx context.Context, playerId int) (*Player, error) + GetPlayerWithGame(ctx context.Context, playerId int) (*Player, error) + GetPlayerWithGamePlayersAndPlayerCardsCard(ctx context.Context, playerId int) (*Player, error) +} diff --git a/Backend/service/request/CreateGameRequest.go b/Backend/service/request/CreateGameRequest.go deleted file mode 100644 index 1a30722..0000000 --- a/Backend/service/request/CreateGameRequest.go +++ /dev/null @@ -1,10 +0,0 @@ -package request - -type CreateGameRequest struct { - Players []PlayerInfo `json:"players"` -} - -type PlayerInfo struct { - ID string `json:"id"` - Name string `json:"name"` -} diff --git a/Backend/service/request/game_request.go b/Backend/service/request/game_request.go new file mode 100644 index 0000000..710e060 --- /dev/null +++ b/Backend/service/request/game_request.go @@ -0,0 +1,27 @@ +package request + +type CreateGameRequest struct { + Players []PlayerInfo `json:"players"` +} + +type PlayerInfo struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type CreateGameResponse struct { + ID string `json:"id"` + Token string `json:"token"` +} + +type PlayCardResponse struct { + Result bool `json:"result"` +} + +type PlayCardRequest struct { + CardID int `json:"card_id"` +} + +type AcceptCardRequest struct { + Accept bool `json:"accept"` +} diff --git a/Backend/service/request/player_card_request.go b/Backend/service/request/player_card_request.go new file mode 100644 index 0000000..a283e8e --- /dev/null +++ b/Backend/service/request/player_card_request.go @@ -0,0 +1,5 @@ +package request + +type PlayerCardsResponse struct { + Player_cards []map[string]interface{} `json:"player_cards"` +} diff --git a/Backend/service/service/card_service.go b/Backend/service/service/card_service.go new file mode 100644 index 0000000..d703aa9 --- /dev/null +++ b/Backend/service/service/card_service.go @@ -0,0 +1,53 @@ +package service + +import ( + "context" + + "github.com/Game-as-a-Service/The-Message/service/repository" +) + +type CardService struct { + CardRepo repository.CardRepository + GameRepo repository.GameRepository + PlayerRepo repository.PlayerRepository + PlayerCardRepo repository.PlayerCardRepository +} + +type CardServiceOptions struct { + CardRepo repository.CardRepository + GameRepo repository.GameRepository + PlayerRepo repository.PlayerRepository + PlayerCardRepo repository.PlayerCardRepository +} + +func NewCardService(opts *CardServiceOptions) CardService { + return CardService{ + CardRepo: opts.CardRepo, + GameRepo: opts.GameRepo, + PlayerRepo: opts.PlayerRepo, + PlayerCardRepo: opts.PlayerCardRepo, + } +} + +func (c *CardService) GetCards(ctx context.Context) ([]*repository.Card, error) { + cards, err := c.CardRepo.GetCards(ctx) + if err != nil { + return nil, err + } + return cards, nil +} + +func (c *CardService) GetPlayerCardsByPlayerId(ctx context.Context, id int) ([]*repository.Card, error) { + player, err := c.PlayerRepo.GetPlayerById(ctx, id) + game, err := c.GameRepo.GetGameWithPlayers(ctx, player.GameId) + if err != nil { + return nil, err + } + + cards, err := c.CardRepo.GetPlayerCardsByPlayerId(ctx, player.Id, game.Id) + + if err != nil { + return nil, err + } + return cards, nil +} diff --git a/Backend/service/service/deck_service.go b/Backend/service/service/deck_service.go new file mode 100644 index 0000000..1768edb --- /dev/null +++ b/Backend/service/service/deck_service.go @@ -0,0 +1,84 @@ +package service + +import ( + "context" + "math/rand" + "time" + + "github.com/Game-as-a-Service/The-Message/service/repository" +) + +type DeckService struct { + CardService CardService + DeckRepo repository.DeckRepository +} + +type DeckServiceOptions struct { + CardService CardService + DeckRepo repository.DeckRepository +} + +func NewDeckService(opts *DeckServiceOptions) DeckService { + return DeckService{ + CardService: opts.CardService, + DeckRepo: opts.DeckRepo, + } +} + +func (d *DeckService) InitDeck(c context.Context, game *repository.Game) error { + cards, err := d.CardService.GetCards(c) + if err != nil { + return err + } + + cards = d.ShuffleDeck(cards) + + var deck []*repository.Deck + + for _, card := range cards { + card, err := d.DeckRepo.CreateDeck(c, &repository.Deck{ + GameId: game.Id, + CardId: card.Id, + }) + if err != nil { + return err + } + deck = append(deck, card) + } + + return nil +} + +func (d *DeckService) ShuffleDeck(cards []*repository.Card) []*repository.Card { + rand.New(rand.NewSource(time.Now().UnixNano())) + + rand.Shuffle(len(cards), func(i, j int) { + cards[i], cards[j] = cards[j], cards[i] + }) + return cards +} + +func (d *DeckService) CreateDeck(c context.Context, deck *repository.Deck) (*repository.Deck, error) { + deck, err := d.DeckRepo.CreateDeck(c, deck) + if err != nil { + return nil, err + } + return deck, nil +} + +func (d *DeckService) GetDecksByGameId(c context.Context, id int) ([]*repository.Deck, error) { + decks, err := d.DeckRepo.GetDecksByGameId(c, id) + if err != nil { + return nil, err + } + return decks, nil + +} + +func (d *DeckService) DeleteDeckFromGame(c context.Context, id int) error { + err := d.DeckRepo.DeleteDeck(c, id) + if err != nil { + return err + } + return nil +} diff --git a/Backend/service/service/gameService.go b/Backend/service/service/gameService.go deleted file mode 100644 index 07b705f..0000000 --- a/Backend/service/service/gameService.go +++ /dev/null @@ -1,33 +0,0 @@ -package service - -import ( - "math/rand" - - "github.com/Game-as-a-Service/The-Message/enums" - "github.com/Game-as-a-Service/The-Message/service/repository" -) - -type GameService struct { - GameRepo repository.GameRepository - PlayerRepo repository.PlayerRepository -} - -func InitIdentityCards(count int) []string { - identityCards := make([]string, count) - // if count == 3 had 1 UndercoverFront, 1 MilitaryIntel, 1 Bystander - if count == 3 { - identityCards[0] = enums.UndercoverFront - identityCards[1] = enums.MilitaryIntel - identityCards[2] = enums.Bystander - } - identityCards = shuffle(identityCards) - return identityCards -} - -func shuffle(cards []string) []string { - shuffledCards := make([]string, len(cards)) - for i, j := range rand.Perm(len(cards)) { - shuffledCards[i] = cards[j] - } - return shuffledCards -} diff --git a/Backend/service/service/game_service.go b/Backend/service/service/game_service.go new file mode 100644 index 0000000..3fe72c8 --- /dev/null +++ b/Backend/service/service/game_service.go @@ -0,0 +1,164 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/hex" + + "github.com/Game-as-a-Service/The-Message/enums" + "github.com/Game-as-a-Service/The-Message/service/repository" +) + +type GameService struct { + GameRepo repository.GameRepository + PlayerService PlayerService + CardService CardService + DeckService DeckService +} + +type GameServiceOptions struct { + GameRepo repository.GameRepository + PlayerService PlayerService + CardService CardService + DeckService DeckService +} + +func NewGameService(opts *GameServiceOptions) GameService { + return GameService{ + GameRepo: opts.GameRepo, + PlayerService: opts.PlayerService, + CardService: opts.CardService, + DeckService: opts.DeckService, + } +} + +func (g *GameService) InitGame(c context.Context) (*repository.Game, error) { + token, err := g.GenerateSecureToken(256) + if err != nil { + return nil, err + } + + game, err := g.CreateGame(c, &repository.Game{ + Token: token, + Status: enums.GameStart, + }) + if err != nil { + return nil, err + } + + return game, nil +} + +func (g *GameService) InitDeck(c context.Context, game *repository.Game) error { + err := g.DeckService.InitDeck(c, game) + if err != nil { + return err + } + return nil +} + +func (g *GameService) DrawCard(c context.Context, game *repository.Game, player *repository.Player, drawCards []*repository.Deck, count int) error { + for i := 0; i < count; i++ { + card := &repository.PlayerCard{ + GameId: game.Id, + PlayerId: player.Id, + CardId: drawCards[i].CardId, + Type: "hand", + } + err := g.PlayerService.CreatePlayerCard(c, card) + if err != nil { + return err + } + err = g.DeckService.DeleteDeckFromGame(c, drawCards[i].Id) + if err != nil { + return err + } + } + return nil +} + +func (g *GameService) DrawCardsForAllPlayers(c context.Context, game *repository.Game) error { + players, err := g.PlayerService.GetPlayersByGameId(c, game.Id) + if err != nil { + return err + } + for _, player := range players { + drawCards, _ := g.DeckService.GetDecksByGameId(c, game.Id) + err2 := g.DrawCard(c, game, player, drawCards, 3) + if err2 != nil { + return err2 + } + + } + return nil +} + +func (g *GameService) GenerateSecureToken(n int) (string, error) { + bytes := make([]byte, n) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +func (g *GameService) CreateGame(c context.Context, game *repository.Game) (*repository.Game, error) { + game, err := g.GameRepo.CreateGame(c, game) + if err != nil { + return nil, err + } + return game, nil +} + +func (g *GameService) GetGameById(c context.Context, id int) (*repository.Game, error) { + game, err := g.GameRepo.GetGameWithPlayers(c, id) + if err != nil { + return nil, err + } + return game, nil +} + +func (g *GameService) DeleteGame(c context.Context, id int) error { + err := g.GameRepo.DeleteGame(c, id) + if err != nil { + return err + } + return nil +} + +func (g *GameService) UpdateCurrentPlayer(c context.Context, game *repository.Game, playerId int) { + game.CurrentPlayerId = playerId + err := g.GameRepo.UpdateGame(c, game) + if err != nil { + panic(err) + } +} + +func (g *GameService) NextPlayer(c context.Context, player *repository.Player) (*repository.Game, error) { + players := player.Game.Players + + currentPlayerId := player.Id + + var currentPlayerIndex int + for index, gPlayer := range players { + if gPlayer.Id == currentPlayerId { + currentPlayerIndex = index + break + } + } + + if currentPlayerIndex+1 >= len(players) { + player.Game.CurrentPlayerId = players[0].Id + player.Game.Status = enums.TransmitIntelligenceStage + } else { + player.Game.CurrentPlayerId = players[currentPlayerIndex+1].Id + } + return player.Game, nil +} + +func (g *GameService) UpdateStatus(c context.Context, game *repository.Game, stage string) { + game.Status = stage + err := g.GameRepo.UpdateGame(c, game) + if err != nil { + panic(err) + } +} diff --git a/Backend/service/service/player_service.go b/Backend/service/service/player_service.go new file mode 100644 index 0000000..7692c75 --- /dev/null +++ b/Backend/service/service/player_service.go @@ -0,0 +1,306 @@ +package service + +import ( + "context" + "errors" + "math/rand" + + "github.com/Game-as-a-Service/The-Message/enums" + "github.com/Game-as-a-Service/The-Message/service/repository" + "github.com/Game-as-a-Service/The-Message/service/request" +) + +type PlayerService struct { + PlayerRepo repository.PlayerRepository + PlayerCardRepo repository.PlayerCardRepository + GameRepo repository.GameRepository + GameServ *GameService + GameProgressRepo repository.GameProgressesRepository +} + +type PlayerServiceOptions struct { + PlayerRepo repository.PlayerRepository + PlayerCardRepo repository.PlayerCardRepository + GameRepo repository.GameRepository + GameServ *GameService + GameProgressRepo repository.GameProgressesRepository +} + +func NewPlayerService(opts *PlayerServiceOptions) PlayerService { + return PlayerService{ + PlayerRepo: opts.PlayerRepo, + PlayerCardRepo: opts.PlayerCardRepo, + GameRepo: opts.GameRepo, + GameServ: opts.GameServ, + GameProgressRepo: opts.GameProgressRepo, + } +} + +func (p *PlayerService) InitPlayers(c context.Context, game *repository.Game, req request.CreateGameRequest) error { + identityCards := p.InitIdentityCards(len(req.Players)) + for i, reqPlayer := range req.Players { + _, err := p.CreatePlayer(c, &repository.Player{ + Name: reqPlayer.Name, + GameId: game.Id, + IdentityCard: identityCards[i], + OrderNumber: i + 1, + Status: enums.PlayerStatusAlive, + }) + if err != nil { + return err + } + } + return nil +} + +func (p *PlayerService) InitIdentityCards(playersCount int) []string { + identityCards := make([]string, playersCount) + + if playersCount == 3 { + identityCards[0] = enums.UndercoverFront + identityCards[1] = enums.MilitaryAgency + identityCards[2] = enums.Bystander + } + identityCards = p.ShuffleIdentityCards(identityCards) + return identityCards +} + +func (p *PlayerService) ShuffleIdentityCards(cards []string) []string { + shuffledCards := make([]string, len(cards)) + for i, j := range rand.Perm(len(cards)) { + shuffledCards[i] = cards[j] + } + return shuffledCards +} + +func (p *PlayerService) CanPlayCard(c context.Context, player *repository.Player) (bool, error) { + if player.Game.Status == enums.GameEnd { + return false, errors.New("遊戲已結束") + } + + if player.Status == enums.PlayerStatusDead { + return false, errors.New("你已死亡") + } + + if player.Game.CurrentPlayerId != player.Id { + return false, errors.New("尚未輪到你出牌") + } + + return true, nil +} + +func (p *PlayerService) CheckPlayerCardExist(c context.Context, playerId int, gameId int, cardId int) (bool, error) { + exist, err := p.PlayerCardRepo.ExistPlayerCardByPlayerIdAndCardId(c, playerId, gameId, cardId) + + if err != nil { + return false, err + } + + return exist, nil +} + +func (p *PlayerService) CreatePlayer(c context.Context, player *repository.Player) (*repository.Player, error) { + player, err := p.PlayerRepo.CreatePlayer(c, player) + if err != nil { + return nil, err + } + return player, nil +} + +func (p *PlayerService) CreatePlayerCard(c context.Context, card *repository.PlayerCard) error { + _, err := p.PlayerCardRepo.CreatePlayerCard(c, card) + if err != nil { + return err + } + return nil +} + +func (p *PlayerService) GetPlayerById(c context.Context, id int) (*repository.Player, error) { + player, err := p.PlayerRepo.GetPlayerById(c, id) + if err != nil { + return nil, err + } + return player, nil +} + +func (p *PlayerService) GetPlayersByGameId(c context.Context, id int) ([]*repository.Player, error) { + players, err := p.PlayerRepo.GetPlayersByGameId(c, id) + if err != nil { + return nil, err + } + return players, nil +} + +func (p *PlayerService) GetHandCardId(player *repository.Player, cardId int) (*repository.PlayerCard, error) { + for _, card := range player.PlayerCards { + if card.CardId == cardId && card.Type == "hand" { + return &card, nil + } + } + return nil, errors.New("找不到手牌") +} + +func (p *PlayerService) PlayCard(c context.Context, playerId int, cardId int) (*repository.Game, *repository.Card, error) { + player, err := p.PlayerRepo.GetPlayerWithGamePlayersAndPlayerCardsCard(c, playerId) + if err != nil { + return nil, nil, err + } + + result, err := p.CanPlayCard(c, player) + if !result || err != nil { + return nil, nil, err + } + + handCard, err := p.GetHandCardId(player, cardId) + if err != nil { + return nil, nil, err + } + + game, err := p.GameServ.NextPlayer(c, player) + if err != nil { + return nil, nil, err + } + + err = p.PlayerCardRepo.DeletePlayerCard(c, handCard.Id) + if err != nil { + return nil, nil, err + } + + err = p.GameRepo.UpdateGame(c, game) + if err != nil { + return nil, nil, err + } + + return game, &handCard.Card, nil +} + +func (p *PlayerService) TransmitIntelligenceCard(c context.Context, playerId int, gameId int, cardId int) (bool, error) { + player, err := p.PlayerRepo.GetPlayerWithGamePlayersAndPlayerCardsCard(c, playerId) + if err != nil { + return false, err + } + + result, err := p.CanPlayCard(c, player) + if !result || err != nil { + return false, err + } + + game, err := p.GameServ.NextPlayer(c, player) + if err != nil { + return false, err + } + + ret, err := p.PlayerCardRepo.DeletePlayerCardByPlayerIdAndCardId(c, playerId, gameId, cardId) + if err != nil { + return false, err + } + + err = p.GameRepo.UpdateGame(c, game) + if err != nil { + return false, err + } + + _, err = p.GameProgressRepo.CreateGameProgress(c, &repository.GameProgresses{ + GameId: game.Id, + PlayerId: playerId, + CardId: cardId, + Action: enums.TransmitIntelligence, + TargetPlayerId: game.CurrentPlayerId, + }) + + if err != nil { + return false, err + } + + return ret, nil +} + +func (p *PlayerService) AcceptCard(c context.Context, playerId int, accept bool) (bool, error) { + player, err := p.PlayerRepo.GetPlayerWithGamePlayersAndPlayerCardsCard(c, playerId) + if err != nil { + return false, err + } + + result, err := p.CanPlayCard(c, player) + if !result || err != nil { + return false, err + } + + game, err := p.GameServ.NextPlayer(c, player) + if err != nil { + return false, err + } + + gameId := game.Id + gameProgress, err := p.GameProgressRepo.GetGameProgresses(c, playerId, gameId) + if err != nil { + return false, err + } + cardId := gameProgress.CardId + // assume the type is SecretTelegram + res := accept + if accept { + _, err := p.PlayerCardRepo.CreatePlayerCard(c, &repository.PlayerCard{ + PlayerId: playerId, + GameId: gameId, + CardId: cardId, + Type: "intelligence", + }) + if err != nil { + return false, err + } + p.GameServ.UpdateStatus(c, game, enums.ActionCardStage) + + } else { + _, err := p.GameProgressRepo.UpdateGameProgress(c, gameProgress, game.CurrentPlayerId) + if err != nil { + return false, err + } + + err = p.GameRepo.UpdateGame(c, game) + if err != nil { + return false, err + } + } + + return res, nil +} + +func (p *PlayerService) CheckWin(c context.Context, playerId int) (*repository.Player, error) { + player, err := p.PlayerRepo.GetPlayerWithGamePlayersAndPlayerCardsCard(c, playerId) + if err != nil { + return nil, err + } + + win := 0 + var winPlayer *repository.Player + for _, player := range player.Game.Players { + win = 0 + for _, card := range player.PlayerCards { + if card.Type == enums.Intelligence && player.IdentityCard == enums.MilitaryAgency && card.Card.Color == enums.Red { + win++ + if win == 3 { + winPlayer = &player + break + } + } + + if card.Type == enums.Intelligence && player.IdentityCard == enums.UndercoverFront && card.Card.Color == enums.Blue { + win++ + if win == 3 { + winPlayer = &player + break + } + } + + if card.Type == enums.Intelligence && player.IdentityCard == enums.MilitaryAgency && card.Card.Color == enums.Red || card.Card.Color == enums.Blue { + win++ + if win == 5 { + winPlayer = &player + break + } + } + } + } + return winPlayer, nil +} diff --git a/Backend/tests/acceptance/game_test.go b/Backend/tests/acceptance/game_test.go deleted file mode 100644 index 904e37d..0000000 --- a/Backend/tests/acceptance/game_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package acceptance - -import ( - "bytes" - "context" - "encoding/json" - "log" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/Game-as-a-Service/The-Message/config" - handler "github.com/Game-as-a-Service/The-Message/service/delivery/http/v1" - mysqlRepo "github.com/Game-as-a-Service/The-Message/service/repository/mysql" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -var serverURL string -var gameRepo *mysqlRepo.GameRepository - -func TestMain(m *testing.M) { - testDB := config.InitTestDB() - - engine := gin.Default() - - gameRepo = mysqlRepo.NewGameRepository(testDB) - playerRepo := mysqlRepo.NewPlayerRepository(testDB) - - handler.NewGameHandler(engine, gameRepo, playerRepo) - - server := httptest.NewServer(engine) - serverURL = server.URL - - code := m.Run() - defer server.Close() - os.Exit(code) -} - -type Player struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type Request struct { - Players []Player `json:"players"` -} - -func TestGive3PlayersABC_whenStartGame_thenABCHadIdentityCard(t *testing.T) { - - players := []Player{ - {ID: "6497f6f226b40d440b9a90cc", Name: "A"}, - {ID: "6498112b26b40d440b9a90ce", Name: "B"}, - {ID: "6499df157fed0c21a4fd0425", Name: "C"}, - } - requestBody := Request{Players: players} - - jsonBody, err := json.Marshal(requestBody) - if err != nil { - t.Fatalf("Failed to marshal JSON: %v", err) - } - - api := "/api/v1/games" - resp := request(t, api, jsonBody) - - assert.Equal(t, 200, resp.StatusCode) - - responseJson := response(t, resp) - - assert.NotNil(t, responseJson["Token"], "JSON response should contain a 'Token' field") - assert.NotNil(t, responseJson["Id"], "JSON response should contain a 'Id' field") - - // 驗證Game內的玩家都持有identity - game, _ := gameRepo.GetGameWithPlayers(context.TODO(), int(responseJson["Id"].(float64))) - assert.NotEmpty(t, game.Players[0].IdentityCard) - assert.NotEmpty(t, game.Players[1].IdentityCard) - assert.NotEmpty(t, game.Players[2].IdentityCard) -} - -func response(t *testing.T, resp *http.Response) map[string]interface{} { - var responseMap map[string]interface{} - err := json.NewDecoder(resp.Body).Decode(&responseMap) - if err != nil { - t.Fatalf("Failed to decode JSON: %v", err) - } - return responseMap -} - -func request(t *testing.T, api string, jsonBody []byte) *http.Response { - req, err := http.NewRequest(http.MethodPost, serverURL+api, bytes.NewBuffer(jsonBody)) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - - client := &http.Client{} - - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - - return resp -} diff --git a/Backend/tests/e2e/game_api_test.go b/Backend/tests/e2e/game_api_test.go index a2bdfd9..7174c64 100644 --- a/Backend/tests/e2e/game_api_test.go +++ b/Backend/tests/e2e/game_api_test.go @@ -1,107 +1,54 @@ package e2e import ( - "bytes" "context" "encoding/json" - "log" "net/http" - "net/http/httptest" - "os" - "testing" - "github.com/Game-as-a-Service/The-Message/config" - handler "github.com/Game-as-a-Service/The-Message/service/delivery/http/v1" - - mysqlRepo "github.com/Game-as-a-Service/The-Message/service/repository/mysql" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) -var serverURL string -var gameRepo *mysqlRepo.GameRepository - -func TestMain(m *testing.M) { - testDB := config.InitTestDB() - - engine := gin.Default() - - gameRepo = mysqlRepo.NewGameRepository(testDB) - playerRepo := mysqlRepo.NewPlayerRepository(testDB) - // gameServ := service.NewGameService(gameRepo, playerRepo) - - handler.NewGameHandler(engine, gameRepo, playerRepo) - - server := httptest.NewServer(engine) - serverURL = server.URL - - code := m.Run() - defer server.Close() - os.Exit(code) -} - -func TestStartGameE2E(t *testing.T) { +func (suite *IntegrationTestSuite) TestStartGameE2E() { players := []Player{ {ID: "6497f6f226b40d440b9a90cc", Name: "A"}, {ID: "6498112b26b40d440b9a90ce", Name: "B"}, {ID: "6499df157fed0c21a4fd0425", Name: "C"}, } - requestBody := Request{Players: players} + requestBody := StartGameRequest{Players: players} jsonBody, err := json.Marshal(requestBody) if err != nil { - t.Fatalf("Failed to marshal JSON: %v", err) + suite.T().Fatalf("Failed to marshal JSON: %v", err) } api := "/api/v1/games" - resp := requestJson(t, api, jsonBody) + resp := suite.requestJson(api, jsonBody, http.MethodPost) - assert.Equal(t, 200, resp.StatusCode) + assert.Equal(suite.T(), 200, resp.StatusCode) - responseJson := responseJson(t, resp) + responseJson := suite.responseJson(resp) - assert.NotNil(t, responseJson["Token"], "JSON response should contain a 'Token' field") - assert.NotNil(t, responseJson["Id"], "JSON response should contain a 'Id' field") + assert.NotNil(suite.T(), responseJson["Token"], "JSON response should contain a 'Token' field") + assert.NotNil(suite.T(), responseJson["Id"], "JSON response should contain a 'Id' field") // 驗證Game內的玩家都持有identity - game, _ := gameRepo.GetGameWithPlayers(context.TODO(), int(responseJson["Id"].(float64))) + game, _ := suite.gameRepo.GetGameWithPlayers(context.TODO(), int(responseJson["Id"].(float64))) - assert.NotEmpty(t, game.Players[0].IdentityCard) - assert.NotEmpty(t, game.Players[1].IdentityCard) - assert.NotEmpty(t, game.Players[2].IdentityCard) + assert.NotEmpty(suite.T(), game.Players[0].IdentityCard) + assert.NotEmpty(suite.T(), game.Players[1].IdentityCard) + assert.NotEmpty(suite.T(), game.Players[2].IdentityCard) + + for _, player := range game.Players { + playerCards, _ := suite.playerRepo.GetPlayerWithPlayerCards(context.TODO(), player.Id) + assert.NotEmpty(suite.T(), playerCards.PlayerCards) + } } -// Helper functions type Player struct { ID string `json:"id"` Name string `json:"name"` } -type Request struct { +type StartGameRequest struct { Players []Player `json:"players"` } - -func responseJson(t *testing.T, resp *http.Response) map[string]interface{} { - var responseMap map[string]interface{} - err := json.NewDecoder(resp.Body).Decode(&responseMap) - if err != nil { - t.Fatalf("Failed to decode JSON: %v", err) - } - return responseMap -} - -func requestJson(t *testing.T, api string, jsonBody []byte) *http.Response { - req, err := http.NewRequest(http.MethodPost, serverURL+api, bytes.NewBuffer(jsonBody)) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - - client := &http.Client{} - - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - - return resp -} diff --git a/Backend/tests/e2e/hearbeat_api_test.go b/Backend/tests/e2e/hearbeat_api_test.go new file mode 100644 index 0000000..90c660d --- /dev/null +++ b/Backend/tests/e2e/hearbeat_api_test.go @@ -0,0 +1,32 @@ +package e2e + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestHeartbeatEndpoint(t *testing.T) { + // Initiate a new gin router + gin.SetMode(gin.TestMode) + router := gin.Default() + + // Set up the heartbeat endpoint + router.GET("/api/v1/heartbeat", func(c *gin.Context) { + c.Status(http.StatusNoContent) + }) + + // Prepare a new HTTP request + req, _ := http.NewRequest("GET", "/api/v1/heartbeat", nil) + + // Create a response recorder + res := httptest.NewRecorder() + router.ServeHTTP(res, req) + + // Assert that the response body is empty + assert.Equal(t, http.StatusNoContent, res.Code) + assert.Empty(t, res.Body.String()) +} diff --git a/Backend/tests/e2e/player_api_test.go b/Backend/tests/e2e/player_api_test.go new file mode 100644 index 0000000..7fdde4c --- /dev/null +++ b/Backend/tests/e2e/player_api_test.go @@ -0,0 +1,253 @@ +package e2e + +import ( + "context" + "encoding/json" + "io" + "math" + "math/rand" + "net/http" + "strconv" + "strings" + "testing" + + "github.com/Game-as-a-Service/The-Message/enums" + "github.com/Game-as-a-Service/The-Message/service/request" + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" +) + +func (suite *IntegrationTestSuite) TestPlayCardE2E() { + // Given + api := "/api/v1/players/{player_id}/player-cards" + game, _ := suite.gameServ.InitGame(context.TODO()) + + // Fake player count random 1~3 + playerCount := rand.Intn(3) + 1 + + // Fake players data + var players []request.PlayerInfo + for i := 0; i < playerCount; i++ { + player := request.PlayerInfo{ + ID: faker.UUIDDigit(), + Name: faker.FirstName(), + } + players = append(players, player) + } + + // Fake game data + createGameRequest := request.CreateGameRequest{ + Players: players, + } + + _ = suite.playerServ.InitPlayers(context.TODO(), game, createGameRequest) + _ = suite.gameServ.InitDeck(context.TODO(), game) + _ = suite.gameServ.DrawCardsForAllPlayers(context.TODO(), game) + + playerId := rand.Intn(playerCount) + 1 + + // Get player's card + cards, _ := suite.playerRepo.GetPlayerWithPlayerCards(context.TODO(), playerId) + + // Random card id + num := rand.Intn(len(cards.PlayerCards)) + cardId := cards.PlayerCards[num].CardId + + // Set player to current player + suite.gameServ.UpdateCurrentPlayer(context.TODO(), game, playerId) + + url := strings.ReplaceAll(api, "{player_id}", strconv.Itoa(playerId)) + req := PlayCardRequest{CardId: cardId} + reqBody, _ := json.Marshal(req) + + res := suite.requestJson(url, reqBody, http.MethodPost) + + // Convert response body from json to map + resBodyAsByteArray, _ := io.ReadAll(res.Body) + resBody := make(map[string]interface{}) + _ = json.Unmarshal(resBodyAsByteArray, &resBody) + + // Then + assert.Equal(suite.T(), 200, res.StatusCode) + + player, _ := suite.playerRepo.GetPlayerWithPlayerCards(context.TODO(), playerId) + assert.Equal(suite.T(), len(cards.PlayerCards)-1, len(player.PlayerCards)) +} + +func (suite *IntegrationTestSuite) TestTransmitIntelligenceE2E() { + api := "/api/v1/player/{player_id}/transmit-intelligence" + game, _ := suite.gameServ.InitGame(context.TODO()) + + // Fake player count random 1~3 + playerCount := rand.Intn(3) + 1 + + // Fake players data + var players []request.PlayerInfo + for i := 0; i < playerCount; i++ { + player := request.PlayerInfo{ + ID: faker.UUIDDigit(), + Name: faker.FirstName(), + } + players = append(players, player) + } + + // Fake game data + createGameRequest := request.CreateGameRequest{ + Players: players, + } + + _ = suite.playerServ.InitPlayers(context.TODO(), game, createGameRequest) + _ = suite.gameServ.InitDeck(context.TODO(), game) + _ = suite.gameServ.DrawCardsForAllPlayers(context.TODO(), game) + + suite.T().Run("it can validate card id", func(t *testing.T) { + playerId := rand.Intn(playerCount) + 1 + + // Request only intelligence type + url := strings.ReplaceAll(api, "{player_id}", strconv.Itoa(playerId)) + req := PlayCardRequest{} + reqBody, _ := json.Marshal(req) + + res := suite.requestJson(url, reqBody, http.MethodPost) + + // Convert response body from json to map + resBodyAsByteArray, _ := io.ReadAll(res.Body) + resBody := make(map[string]interface{}) + _ = json.Unmarshal(resBodyAsByteArray, &resBody) + + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Equal(t, "Card not found", resBody["message"]) + }) + + suite.T().Run("it can fail when player not found", func(t *testing.T) { + playerId := math.MaxInt32 + cardId := rand.Intn(playerCount) + + url := strings.ReplaceAll(api, "{player_id}", strconv.Itoa(playerId)) + req := PlayCardRequest{CardId: cardId} + reqBody, _ := json.Marshal(req) + + res := suite.requestJson(url, reqBody, http.MethodPost) + + // Convert response body from json to map + resBodyAsByteArray, _ := io.ReadAll(res.Body) + resBody := make(map[string]interface{}) + _ = json.Unmarshal(resBodyAsByteArray, &resBody) + + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Equal(t, "Player not found", resBody["message"]) + }) + + suite.T().Run("it can fail when player card not found", func(t *testing.T) { + playerId := rand.Intn(playerCount) + 1 + cardId := math.MaxInt32 + + url := strings.ReplaceAll(api, "{player_id}", strconv.Itoa(playerId)) + req := PlayCardRequest{CardId: cardId} + reqBody, _ := json.Marshal(req) + + res := suite.requestJson(url, reqBody, http.MethodPost) + + // Convert response body from json to map + resBodyAsByteArray, _ := io.ReadAll(res.Body) + resBody := make(map[string]interface{}) + _ = json.Unmarshal(resBodyAsByteArray, &resBody) + + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Equal(t, "Card not found", resBody["message"]) + }) + + suite.T().Run("it can fail when game is end", func(t *testing.T) { + playerId := rand.Intn(playerCount) + 1 + + // Get player's card + cards, _ := suite.playerRepo.GetPlayerWithPlayerCards(context.TODO(), playerId) + + // Random card id + num := rand.Intn(len(cards.PlayerCards)) + cardId := cards.PlayerCards[num].CardId + + // Set player to current player + suite.gameServ.UpdateCurrentPlayer(context.TODO(), game, playerId) + + // Set game status to end + suite.gameServ.UpdateStatus(context.TODO(), game, enums.GameEnd) + + url := strings.ReplaceAll(api, "{player_id}", strconv.Itoa(playerId)) + req := PlayCardRequest{CardId: cardId} + reqBody, _ := json.Marshal(req) + + res := suite.requestJson(url, reqBody, http.MethodPost) + + // Convert response body from json to map + resBodyAsByteArray, _ := io.ReadAll(res.Body) + resBody := make(map[string]interface{}) + _ = json.Unmarshal(resBodyAsByteArray, &resBody) + + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Equal(t, "遊戲已結束", resBody["message"]) + + // Recover game status to start + suite.gameServ.UpdateStatus(context.TODO(), game, enums.GameStart) + }) + + suite.T().Run("it can fail when not player's turn", func(t *testing.T) { + playerId := rand.Intn(playerCount) + 1 + + // Get player's card + cards, _ := suite.playerRepo.GetPlayerWithPlayerCards(context.TODO(), playerId) + + // Random card id + num := rand.Intn(len(cards.PlayerCards)) + cardId := cards.PlayerCards[num].CardId + + // Set other player to current player + suite.gameServ.UpdateCurrentPlayer(context.TODO(), game, playerId-1) + + url := strings.ReplaceAll(api, "{player_id}", strconv.Itoa(playerId)) + req := PlayCardRequest{CardId: cardId} + reqBody, _ := json.Marshal(req) + + res := suite.requestJson(url, reqBody, http.MethodPost) + + // Convert response body from json to map + resBodyAsByteArray, _ := io.ReadAll(res.Body) + resBody := make(map[string]interface{}) + _ = json.Unmarshal(resBodyAsByteArray, &resBody) + + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Equal(t, "尚未輪到你出牌", resBody["message"]) + }) + + suite.T().Run("it can success when valid card id", func(t *testing.T) { + playerId := rand.Intn(playerCount) + 1 + + // Get player's card + cards, _ := suite.playerRepo.GetPlayerWithPlayerCards(context.TODO(), playerId) + + // Random card id + num := rand.Intn(len(cards.PlayerCards)) + cardId := cards.PlayerCards[num].CardId + + // Set player to current player + suite.gameServ.UpdateCurrentPlayer(context.TODO(), game, playerId) + + url := strings.ReplaceAll(api, "{player_id}", strconv.Itoa(playerId)) + req := PlayCardRequest{CardId: cardId} + reqBody, _ := json.Marshal(req) + + res := suite.requestJson(url, reqBody, http.MethodPost) + + // Convert response body from json to map + resBodyAsByteArray, _ := io.ReadAll(res.Body) + resBody := make(map[string]interface{}) + _ = json.Unmarshal(resBodyAsByteArray, &resBody) + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, true, resBody["result"]) + }) +} + +type PlayCardRequest struct { + CardId int `json:"card_id"` +} diff --git a/Backend/tests/e2e/player_card_api_test.go b/Backend/tests/e2e/player_card_api_test.go new file mode 100644 index 0000000..a9af03f --- /dev/null +++ b/Backend/tests/e2e/player_card_api_test.go @@ -0,0 +1,69 @@ +package e2e + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/Game-as-a-Service/The-Message/service/repository" + "github.com/stretchr/testify/assert" +) + +func (suite *IntegrationTestSuite) TestGetPlayerCards() { + + // given + game := repository.Game{} + _, err := suite.gameRepo.CreateGame(context.TODO(), &game) + player := repository.Player{ + Name: "player1", + GameId: 1, + IdentityCard: "醬油", + } + _, err = suite.playerRepo.CreatePlayer(context.TODO(), &player) + if err != nil { + panic(err) + } + + _, err = suite.playerCardRepo.CreatePlayerCard(context.TODO(), &repository.PlayerCard{ + PlayerId: 1, + GameId: 1, + CardId: 1, + Type: "hand", + }) + if err != nil { + panic(err) + } + + // when + api := "/api/v1/player/1/player-cards/" + resp := suite.requestJson(api, nil, http.MethodGet) + response := suite.responseJson(resp) + // then + assert.Equal(suite.T(), 200, resp.StatusCode) + + playerCards, ok := response["player_cards"] + if !ok { + fmt.Println("Error: player_cards is not of type []interface{}") + return + } + + if str, ok := playerCards.(string); ok { + var slice []map[string]interface{} + if err := json.Unmarshal([]byte(str), &slice); err != nil { + fmt.Println("Error decoding JSON:", err) + return + } + + // Range over the slice + for _, item := range slice { + fmt.Println(item["id"], item["name"], item["color"]) + for key, value := range item { + if value == nil { + suite.T().Errorf("Field %s is nil", key) + } + } + + } + } +} diff --git a/Backend/tests/e2e/suite_test.go b/Backend/tests/e2e/suite_test.go new file mode 100644 index 0000000..a2f70d9 --- /dev/null +++ b/Backend/tests/e2e/suite_test.go @@ -0,0 +1,212 @@ +package e2e + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/Game-as-a-Service/The-Message/config" + "github.com/Game-as-a-Service/The-Message/database/seeders" + v1 "github.com/Game-as-a-Service/The-Message/service/delivery/http/v1" + "github.com/Game-as-a-Service/The-Message/service/repository" + mysqlRepo "github.com/Game-as-a-Service/The-Message/service/repository/mysql" + "github.com/Game-as-a-Service/The-Message/service/service" + "github.com/gin-gonic/gin" + _ "github.com/go-sql-driver/mysql" + _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/joho/godotenv" + _ "github.com/joho/godotenv/autoload" + "github.com/stretchr/testify/suite" + "gorm.io/gorm" +) + +type IntegrationTestSuite struct { + suite.Suite + db *gorm.DB + tx *gorm.DB + server *httptest.Server + gameRepo repository.GameRepository + playerRepo repository.PlayerRepository + playerCardRepo repository.PlayerCardRepository + gameServ *service.GameService + playerServ *service.PlayerService +} + +func (suite *IntegrationTestSuite) SetupSuite() { + sourceURL := config.GetSourceURL() + + err := godotenv.Load("../../.env") + if err != nil { + log.Fatalf("Error loading .env file: %v", err) + } + + dsn := config.BaseTestDSN() + val := url.Values{} + val.Add("multiStatements", "true") + dsn = fmt.Sprintf("%s?%s", dsn, val.Encode()) + + m, err := config.NewMigration(dsn, sourceURL) + if err != nil { + panic(err) + } + + err = m.Down() + if err != nil { + if err.Error() == "no change" { + fmt.Println("no change") + } else { + panic(err) + } + } + + err = m.Up() + if err != nil { + if err.Error() == "no change" { + fmt.Println("no change") + } else { + panic(err) + } + } + db := config.NewDatabase() + + seeders.SeederCards(db) + + engine := gin.Default() + sse := v1.NewSSEServer() + + gameRepo := mysqlRepo.NewGameRepository(db) + playerRepo := mysqlRepo.NewPlayerRepository(db) + cardRepo := mysqlRepo.NewCardRepository(db) + deckRepo := mysqlRepo.NewDeckRepository(db) + playerCardRepo := mysqlRepo.NewPlayerCardRepository(db) + gameProgressRepo := mysqlRepo.NewGameProgressRepository(db) + + cardService := service.NewCardService(&service.CardServiceOptions{ + CardRepo: cardRepo, + GameRepo: gameRepo, + PlayerRepo: playerRepo, + PlayerCardRepo: playerCardRepo, + }) + + deckService := service.NewDeckService(&service.DeckServiceOptions{ + DeckRepo: deckRepo, + CardService: cardService, + }) + + playerService := service.NewPlayerService(&service.PlayerServiceOptions{ + PlayerRepo: playerRepo, + PlayerCardRepo: playerCardRepo, + GameRepo: gameRepo, + GameProgressRepo: gameProgressRepo, + }) + + gameService := service.NewGameService( + &service.GameServiceOptions{ + GameRepo: gameRepo, + PlayerService: playerService, + CardService: cardService, + DeckService: deckService, + }, + ) + playerService.GameServ = &gameService + + v1.RegisterGameHandler( + &v1.GameHandlerOptions{ + Engine: engine, + Service: gameService, + SSE: sse, + }, + ) + + v1.RegisterCardHandler( + &v1.CardHandlerOptions{ + Engine: engine, + Service: cardService, + }, + ) + + v1.RegisterPlayerHandler( + &v1.PlayerHandlerOptions{ + Engine: engine, + Service: playerService, + GameService: gameService, + SSE: sse, + }, + ) + + server := httptest.NewServer(engine) + + suite.db = db + suite.server = server + suite.gameRepo = gameRepo + suite.playerRepo = playerRepo + suite.gameServ = &gameService + suite.playerServ = &playerService + suite.playerCardRepo = playerCardRepo +} + +func (suite *IntegrationTestSuite) TearDownSuite() { + sqlDB, _ := suite.db.DB() + err := sqlDB.Close() + if err != nil { + return + } + + suite.server.Close() +} + +func (suite *IntegrationTestSuite) SetupTest() { + suite.tx = suite.db.Begin() + + //Fixme Run db refresh and seeders + config.RunRefresh() + db := config.NewDatabase() + seeders.Run(db) +} + +func (suite *IntegrationTestSuite) TearDownTest() { + suite.tx.Rollback() +} + +func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} + +func (suite *IntegrationTestSuite) responseJson(resp *http.Response) map[string]interface{} { + var responseMap map[string]interface{} + err := json.NewDecoder(resp.Body).Decode(&responseMap) + if err != nil { + suite.T().Fatalf("Failed to decode JSON: %v", err) + } + return responseMap +} + +func (suite *IntegrationTestSuite) requestJson(api string, jsonBody []byte, method string) *http.Response { + req, err := http.NewRequest(method, suite.server.URL+api, bytes.NewBuffer(jsonBody)) + if err != nil { + suite.T().Fatalf("Failed to send request: %v", err) + } + + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + + return resp +} + +func (suite *IntegrationTestSuite) responseTest(resp *http.Response) interface{} { + var responseMap interface{} + err := json.NewDecoder(resp.Body).Decode(&responseMap) + if err != nil { + suite.T().Fatalf("Failed to decode JSON: %v", err) + } + return responseMap +} diff --git a/Backend/utills/response_writer.go b/Backend/utills/response_writer.go deleted file mode 100644 index 7cbcaff..0000000 --- a/Backend/utills/response_writer.go +++ /dev/null @@ -1 +0,0 @@ -package utills diff --git a/Backend/utils/response_writer.go b/Backend/utils/response_writer.go new file mode 100644 index 0000000..d4b585b --- /dev/null +++ b/Backend/utils/response_writer.go @@ -0,0 +1 @@ +package utils diff --git a/README.md b/README.md index 8df9491..ac41d03 100644 --- a/README.md +++ b/README.md @@ -3,38 +3,53 @@ ## Step to Run in Backend -1. 進入 Backend 資料夾 +- 進入 Backend 資料夾 ```bash -cd Backend + cd Backend ``` -2. docker 建置出 MySQL 環境 +- docker 建置出 MySQL 環境 ```bash -docker-compose up -d + docker-compose up -d ``` -3. 進行 DB 資料表清空與重新建置 +- 進行 DB 資料表rollback ```bash -go run ./cmd/migrate/fresh.go + go run ./cmd/migrate/rollback.go ``` -4. 進行 DB Migration +- 進行 DB Migration ```bash -go run ./cmd/migrate/migrate.go + go run ./cmd/migrate/migrate.go ``` -5. 開啟 Go Web Server +- 進行 DB 資料表清空與重新建置 ```bash -go run cmd/app/main.go + go run ./cmd/migrate/refresh.go ``` -6. 進行第一個 Request 呼叫 +- 進行 DB Seeder +```bash + go run ./cmd/migrate/game_card_seeder.go +``` + +- 自動產生 Swagger API 文件 +```bash + swag init -g ./cmd/app/main.go -output ./cmd/app/docs +``` + +- 開啟 Go Web Server +```bash + go run cmd/app/main.go +``` + +- 進行第一個 Request 呼叫 - 有兩種方式可以進行 Request 呼叫,如以下所示: - 1. 將 `The-Message.postman_collection.json` 檔匯入至 Postman 中,並執行 Collection 當中的 API。 + - 將 `The-Message.postman_collection.json` 檔匯入至 Postman 中,並執行 Collection 當中的 API。 - 2. 撰寫一個 HTTP Method 為 POST 的 Request,路徑為 `localhost:8080/api/v1/games`,並帶入以下玩家 JSON 資料。 + - 撰寫一個 HTTP Method 為 POST 的 Request,路徑為 `localhost:8080/api/v1/games`,並帶入以下玩家 JSON 資料。 ```json { "players": [{ @@ -50,6 +65,11 @@ go run cmd/app/main.go } ``` +查看 API 文件網址: +``` +http://127.0.0.1:8080/swagger/index.html +``` + ## Class Diagram ```mermaid @@ -96,4 +116,41 @@ classDiagram + getCardWeight(firstCard : MissionCard, secondCard : MissionCard) : MissionCard[] + getCanShowCard(heads : MissionCard[]) : MissionCard[] } -``` \ No newline at end of file +``` + +## Usage + +### Development + +#### Add missing and remove unused modules +```bash + go mod tidy +``` + +### Goimports + +#### Install + +```bash + go get golang.org/x/tools/cmd/goimports +``` + +#### Run + +```bash + goimports -l -w . +``` + +### GoLangCI-Lint + +#### Install + +```bash + go get github.com/golangci/golangci-lint/cmd/golangci-lint +``` + +#### Run + +```bash + golangci-lint run ./... +```