diff --git a/backend/cmd/main.go b/backend/cmd/main.go index f48d307..a8d1e78 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -31,12 +31,13 @@ func main() { r.Get("/api/healthcheck", api.Healthcheck) r.Post("/api/login", api.Login) - r.Get("/api/leaderboard", api.GetLeaderboard) r.Group(func(r chi.Router) { r.Use(middleware.JWTAuth) + r.Get("/api/user", api.GetUser) r.Post("/api/user/deposit", api.Deposit) - r.Post("/api/roulette/bet", api.PlaceBet) + r.Get("/api/leaderboard", api.GetLeaderboard) + r.Post("/api/roulette", api.PlaceBet) }) log.Println("Server running on port 8080") diff --git a/backend/pkg/api/handlers.go b/backend/pkg/api/handlers.go index 2f98720..9097bdc 100644 --- a/backend/pkg/api/handlers.go +++ b/backend/pkg/api/handlers.go @@ -3,8 +3,10 @@ package api import ( "encoding/json" "errors" + "log" "net/http" "strconv" + "strings" "github.com/alik-r/casino-roulette/backend/pkg/auth" "github.com/alik-r/casino-roulette/backend/pkg/db" @@ -26,8 +28,9 @@ type DepositRequest struct { } type BetRequest struct { - BetAmount int `json:"bet_amount"` - BetColor string `json:"bet_color"` + BetAmount int `json:"bet_amount"` + BetType string `json:"bet_type"` + BetValue interface{} `json:"bet_value"` } func Login(w http.ResponseWriter, r *http.Request) { @@ -54,6 +57,8 @@ func Login(w http.ResponseWriter, r *http.Request) { if loginRequest.Avatar == "" { loginRequest.Avatar = "images/avatars/avatar1.png" + } else { + loginRequest.Avatar = "images/avatars/" + strings.Split(loginRequest.Avatar, "images/avatars/")[1] } user = models.User{ @@ -95,7 +100,7 @@ func Login(w http.ResponseWriter, r *http.Request) { func Deposit(w http.ResponseWriter, r *http.Request) { username, ok := r.Context().Value(middleware.UsernameKey).(string) if !ok || username == "" { - http.Error(w, "missing username", http.StatusBadRequest) + http.Error(w, "Unauthenticated user", http.StatusUnauthorized) return } @@ -137,19 +142,25 @@ func Deposit(w http.ResponseWriter, r *http.Request) { func PlaceBet(w http.ResponseWriter, r *http.Request) { username, ok := r.Context().Value(middleware.UsernameKey).(string) if !ok || username == "" { - http.Error(w, "missing username", http.StatusBadRequest) + http.Error(w, "Unauthenticated user", http.StatusUnauthorized) return } var betRequest BetRequest err := json.NewDecoder(r.Body).Decode(&betRequest) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, "Invalid request payload", http.StatusBadRequest) + return + } + + if betRequest.BetAmount <= 0 { + http.Error(w, "Bet amount must be greater than zero", http.StatusBadRequest) return } - if betRequest.BetAmount <= 0 || !roulette.IsValidColor(betRequest.BetColor) { - http.Error(w, "invalid bet amount or color", http.StatusBadRequest) + if !roulette.IsValidBet(roulette.BetType(betRequest.BetType), betRequest.BetValue) { + log.Println("Invalid bet type or value:", betRequest.BetType, roulette.BetType(betRequest.BetType), betRequest.BetValue) + http.Error(w, "Invalid bet type or value", http.StatusBadRequest) return } @@ -157,69 +168,151 @@ func PlaceBet(w http.ResponseWriter, r *http.Request) { err = db.DB.Where("username = ?", username).First(&user).Error if err != nil { if err == gorm.ErrRecordNotFound { - http.Error(w, "user not found", http.StatusNotFound) + http.Error(w, "User not found", http.StatusNotFound) return } - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, "Database error", http.StatusInternalServerError) + log.Println("Database error in PlaceBet:", err) return } if user.Balance < betRequest.BetAmount { - http.Error(w, "insufficient balance", http.StatusBadRequest) + http.Error(w, "Insufficient balance", http.StatusBadRequest) return } result := roulette.Spin() - payout := roulette.Payout(betRequest.BetColor, string(result.Color)) + + payoutMultiplier := roulette.Payout(roulette.BetType(betRequest.BetType), betRequest.BetValue, result) + var payout int var betResult string - if payout > 0 { - user.Balance += betRequest.BetAmount * (payout - 1) + if payoutMultiplier > 0 { + payout = betRequest.BetAmount * payoutMultiplier + user.Balance += payout betResult = "win" } else { + payout = 0 user.Balance -= betRequest.BetAmount betResult = "lose" } if err := db.DB.Save(&user).Error; err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, "Failed to update user balance", http.StatusInternalServerError) + return + } + + var betValueStr string + switch betRequest.BetType { + case "color", "evenodd", "highlow": + value, ok := betRequest.BetValue.(string) + if !ok { + log.Printf("Invalid bet value, expected string, got %T", betRequest.BetValue) + http.Error(w, "Invalid bet value", http.StatusBadRequest) + return + } + betValueStr = value + case "number": + value, ok := betRequest.BetValue.(float64) + if !ok { + log.Printf("Invalid bet value, expected float64, got %T", betRequest.BetValue) + http.Error(w, "Invalid bet value", http.StatusBadRequest) + return + } + betValueStr = strconv.Itoa(int(value)) + default: + log.Println("Invalid bet type:", betRequest.BetType) + http.Error(w, "Invalid bet type", http.StatusBadRequest) return } bet := models.Bet{ UserID: user.ID, BetAmount: betRequest.BetAmount, - BetColor: betRequest.BetColor, + BetType: betRequest.BetType, + BetValue: betValueStr, + Payout: payout, Result: betResult, } if err := db.DB.Create(&bet).Error; err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, "Failed to record bet", http.StatusInternalServerError) return } response := map[string]interface{}{ - "username": user.Username, - "balance": user.Balance, - "bet_amount": bet.BetAmount, - "bet_color": bet.BetColor, - "result": bet.Result, - "result_color": result.Color, + "balance": user.Balance, + "bet_amount": bet.BetAmount, + "bet_type": bet.BetType, + "bet_value": bet.BetValue, + "payout": bet.Payout, + "result": bet.Result, + "result_color": result.Color, + "result_number": result.Number, } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(response) } func GetLeaderboard(w http.ResponseWriter, r *http.Request) { + username, ok := r.Context().Value(middleware.UsernameKey).(string) + if !ok || username == "" { + http.Error(w, "Unauthenticated user", http.StatusUnauthorized) + return + } + var users []models.User - err := db.DB.Order("balance desc").Limit(10).Find(&users).Error - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + if err := db.DB.Order("balance DESC").Limit(10).Find(&users).Error; err != nil { + http.Error(w, "Failed to retrieve leaderboard", http.StatusInternalServerError) return } + type LeaderboardRow struct { + Username string `json:"username"` + Balance int `json:"balance"` + Avatar string `json:"avatar"` + } + + var leaderboard []LeaderboardRow + for _, user := range users { + leaderboard = append(leaderboard, LeaderboardRow{ + Username: user.Username, + Balance: user.Balance, + Avatar: user.Avatar, + }) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(leaderboard) +} + +func GetUser(w http.ResponseWriter, r *http.Request) { + username, ok := r.Context().Value(middleware.UsernameKey).(string) + if !ok || username == "" { + http.Error(w, "Unauthenticated user", http.StatusUnauthorized) + return + } + + var user models.User + if err := db.DB.Where("username = ?", username).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + http.Error(w, "User not found", http.StatusNotFound) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + response := map[string]interface{}{ + "username": user.Username, + "email": user.Email, + "avatar": user.Avatar, + "balance": user.Balance, + } + w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(users) + json.NewEncoder(w).Encode(response) } func Healthcheck(w http.ResponseWriter, r *http.Request) { diff --git a/backend/pkg/models/models.go b/backend/pkg/models/models.go index 665f540..8973fcf 100644 --- a/backend/pkg/models/models.go +++ b/backend/pkg/models/models.go @@ -20,7 +20,9 @@ type Bet struct { UserID uint `json:"user_id"` User User `gorm:"foreignKey:UserID" json:"-"` BetAmount int `json:"bet_amount"` - BetColor string `json:"bet_color"` + BetType string `json:"bet_type"` + BetValue string `json:"bet_value"` + Payout int `json:"payout"` Result string `json:"result"` CreatedAt time.Time `json:"-" gorm:"autoCreateTime"` } diff --git a/backend/pkg/roulette/roulette.go b/backend/pkg/roulette/roulette.go index d427db6..2f58083 100644 --- a/backend/pkg/roulette/roulette.go +++ b/backend/pkg/roulette/roulette.go @@ -1,12 +1,14 @@ package roulette import ( + "log" "time" "golang.org/x/exp/rand" ) type Color string +type BetType string const ( Red Color = "red" @@ -14,39 +16,106 @@ const ( Green Color = "green" ) +const ( + ColorBet BetType = "color" + EvenOddBet BetType = "evenodd" + HighLowBet BetType = "highlow" + NumberBet BetType = "number" +) + type RouletteResult struct { - Color Color `json:"color"` + Color Color `json:"color"` + Number int `json:"number"` +} + +var numberColorMap = map[int]Color{ + 0: Green, + 1: Red, 2: Black, 3: Red, 4: Black, 5: Red, 6: Black, 7: Red, 8: Black, 9: Red, 10: Black, + 11: Black, 12: Red, 13: Black, 14: Red, 15: Black, 16: Red, 17: Black, 18: Red, + 19: Red, 20: Black, 21: Red, 22: Black, 23: Red, 24: Black, 25: Red, 26: Black, 27: Red, 28: Black, + 29: Black, 30: Red, 31: Black, 32: Red, 33: Black, 34: Red, 35: Black, 36: Red, } func Spin() RouletteResult { rand.Seed(uint64(time.Now().UnixNano())) - num := rand.Intn(100) + 1 // 1-100 + num := rand.Intn(37) // 0-36 + color, exists := numberColorMap[num] + if !exists { + color = Green + } - switch { - case num <= 48: - return RouletteResult{Color: Red} - case num <= 96: - return RouletteResult{Color: Black} - default: - return RouletteResult{Color: Green} + return RouletteResult{ + Color: color, + Number: num, } } -func Payout(betColor, resultColor string) int { - if betColor == resultColor { - if betColor == string(Green) { - return 14 +func Payout(betType BetType, betValue interface{}, result RouletteResult) int { + switch betType { + case ColorBet: + betColor := betValue.(string) + if betColor == string(result.Color) { + if betColor == string(Green) { + return 35 + } + return 1 + } + case EvenOddBet: + betParity := betValue.(string) + if betParity == "even" && result.Number%2 == 0 { + return 1 + } + if betParity == "odd" && result.Number%2 == 1 { + return 1 + } + case HighLowBet: + betRange := betValue.(string) + if betRange == "high" && result.Number >= 19 { + return 1 + } + if betRange == "low" && result.Number <= 18 { + return 1 + } + case NumberBet: + number := betValue.(float64) + betNumber := int(number) + if betNumber == result.Number { + return 35 } - return 2 } return 0 } -func IsValidColor(color string) bool { - switch color { - case string(Red), string(Black), string(Green): - return true +func IsValidBet(betType BetType, betValue interface{}) bool { + switch betType { + case ColorBet: + color, ok := betValue.(string) + if !ok { + return false + } + return color == string(Red) || color == string(Black) || color == string(Green) + case EvenOddBet: + parity, ok := betValue.(string) + if !ok { + return false + } + return parity == "even" || parity == "odd" + case HighLowBet: + rangeValue, ok := betValue.(string) + if !ok { + return false + } + return rangeValue == "high" || rangeValue == "low" + case NumberBet: + numFloat, ok := betValue.(float64) + if !ok { + log.Println("number bet value is not int") + return false + } + num := int(numFloat) + return num >= 0 && num <= 36 default: + log.Println("invalid bet type", betType) return false } } diff --git a/backend/pkg/roulette/roulette_test.go b/backend/pkg/roulette/roulette_test.go index b212aac..9548163 100644 --- a/backend/pkg/roulette/roulette_test.go +++ b/backend/pkg/roulette/roulette_test.go @@ -5,32 +5,68 @@ import ( ) func TestSpin(t *testing.T) { - for i := 0; i < 100; i++ { - result := Spin() - if result.Color != Red && result.Color != Black && result.Color != Green { - t.Errorf("Invalid result color: %v", result.Color) - } + result := Spin() + if result.Number < 0 || result.Number > 36 { + t.Errorf("Invalid number: got %d, want between 0 and 36", result.Number) + } + if result.Color != numberColorMap[result.Number] { + t.Errorf("Invalid color: got %s, want %s", result.Color, numberColorMap[result.Number]) } } func TestPayout(t *testing.T) { tests := []struct { - betColor string - resultColor string - expected int + betType BetType + betValue interface{} + result RouletteResult + expected int + }{ + {ColorBet, "red", RouletteResult{Red, 1}, 1}, + {ColorBet, "black", RouletteResult{Black, 2}, 1}, + {ColorBet, "green", RouletteResult{Green, 0}, 35}, + {EvenOddBet, "even", RouletteResult{Red, 2}, 1}, + {EvenOddBet, "odd", RouletteResult{Black, 3}, 1}, + {HighLowBet, "high", RouletteResult{Red, 20}, 1}, + {HighLowBet, "low", RouletteResult{Black, 10}, 1}, + {NumberBet, 7.0, RouletteResult{Red, 7}, 35}, + {NumberBet, 8.0, RouletteResult{Black, 9}, 0}, + } + + for _, tt := range tests { + t.Run(string(tt.betType), func(t *testing.T) { + actual := Payout(tt.betType, tt.betValue, tt.result) + if actual != tt.expected { + t.Errorf("Payout(%v, %v, %v) = %d; want %d", tt.betType, tt.betValue, tt.result, actual, tt.expected) + } + }) + } +} + +func TestIsValidBet(t *testing.T) { + tests := []struct { + betType BetType + betValue interface{} + expected bool }{ - {string(Red), string(Red), 2}, - {string(Black), string(Black), 2}, - {string(Green), string(Green), 14}, - {string(Red), string(Black), 0}, - {string(Black), string(Red), 0}, - {string(Green), string(Red), 0}, + {ColorBet, "red", true}, + {ColorBet, "blue", false}, + {EvenOddBet, "even", true}, + {EvenOddBet, "odd", true}, + {EvenOddBet, "none", false}, + {HighLowBet, "high", true}, + {HighLowBet, "low", true}, + {HighLowBet, "medium", false}, + {NumberBet, 7.0, true}, + {NumberBet, 37.0, false}, + {NumberBet, -1.0, false}, } for _, tt := range tests { - got := Payout(tt.betColor, tt.resultColor) - if got != tt.expected { - t.Errorf("Payout(%q, %q) = %d; want %d", tt.betColor, tt.resultColor, got, tt.expected) - } + t.Run(string(tt.betType), func(t *testing.T) { + actual := IsValidBet(tt.betType, tt.betValue) + if actual != tt.expected { + t.Errorf("IsValidBet(%v, %v) = %v; want %v", tt.betType, tt.betValue, actual, tt.expected) + } + }) } } diff --git a/frontend/images/leaderboard_icon.png b/frontend/images/leaderboard_icon.png new file mode 100644 index 0000000..356dc2e Binary files /dev/null and b/frontend/images/leaderboard_icon.png differ diff --git a/frontend/images/logout_icon.png b/frontend/images/logout_icon.png new file mode 100644 index 0000000..f24edd6 Binary files /dev/null and b/frontend/images/logout_icon.png differ diff --git a/frontend/images/roullete_icon.png b/frontend/images/roullete_icon.png new file mode 100644 index 0000000..da39ff4 Binary files /dev/null and b/frontend/images/roullete_icon.png differ diff --git a/frontend/images/soul_icon.png b/frontend/images/soul_icon.png new file mode 100644 index 0000000..cdeeb25 Binary files /dev/null and b/frontend/images/soul_icon.png differ diff --git a/frontend/images/user_icon.png b/frontend/images/user_icon.png new file mode 100644 index 0000000..66926cf Binary files /dev/null and b/frontend/images/user_icon.png differ diff --git a/frontend/roulette.html b/frontend/roulette.html index f63e96c..74a7955 100644 --- a/frontend/roulette.html +++ b/frontend/roulette.html @@ -1,17 +1,61 @@ - + + + + + Casino + -
ROULLETTE
+ +
-

Your souls

- 49991 +
+

Your Souls

+
+

1000

+ Soul Icon +
+
+
@@ -20,33 +64,332 @@
-

Your bet

+ + +
+

Your Bet

+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + + +
-

By color

-
+

By color

+

Red

+

+

1:1

Black

+

+

1:1

Green

-

+

35:1

- - - +

Even/Odd

+ + +
+
+ +

+

1:1

+
+ +
+ +

+

1:1

+
+ +
+

High/Low

+
+
+ +

+

1:1

+
+ +
+ +

+

1:1

+
+ +
+

Specific Number

+
+
+
+ +
+

+

1:35

+
+
-
+ + + \ No newline at end of file diff --git a/frontend/styles/roulette_style.css b/frontend/styles/roulette_style.css index aab0c73..c3a5fad 100644 --- a/frontend/styles/roulette_style.css +++ b/frontend/styles/roulette_style.css @@ -2,26 +2,192 @@ font-family: 'CupheadFont'; src: url(../fonts/CupheadPoster-Regular.ttf); } + body { + box-sizing: border-box; background-color: #FED119; font-family: 'Gabarito', sans-serif; display: flex; flex-direction: column; align-items: center; height: 100vh; + width: 100vw; +} + + +/* Your Souls */ +/* Container styling for the souls section */ +.souls { + width: 160px; + margin: 2px auto; + padding: 2px; + text-align: center; + font-family: 'Gabarito', sans-serif; +} + +/* Title styling */ +.souls-title { + font-size: 1.2em; + font-weight: bold; + color: #3F1B12; + margin-bottom: 2px; +} + +/* Display container for amount and icon */ +.souls-display { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +/* Amount styling */ +.souls-amount { + font-size: 1.5em; + font-weight: bold; + font-family: 'Gabarito', monospace; + color: #3F1B12; + transition: color 0.5s ease; +} + +/* Icon styling */ +.soul-icon { + width: 24px; + height: 24px; + display: inline-block; +} + +/* END Your Souls */ + +/* Your bet form */ +.your-bet { + width: 300px; + margin: 20px auto; + padding: 20px; + font-family: 'Gabarito', sans-serif; +} + +.your-bet-title { + font-size: 1.5em; + margin-bottom: 6px; + text-align: center; + color: #3F1B12; +} + +.form-group { + margin-bottom: 15px; +} + +label { + display: block; + font-weight: bold; + margin-bottom: 5px; + color: #3F1B12; +} + +input[type="number"], +select { width: 100%; + padding: 8px; + margin-bottom: 5px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 1em; } -.title{ + +button.bet-button { + background-color: #3F1B12; + color: white; + font-family: 'CupheadFont', sans-serif; + font-size: 1em; + font-weight: bold; + border-radius: 8px; + border: none; + cursor: pointer; + margin-top: 12px; + padding: 6px 40px; + text-align: center; +} + +button.bet-button:hover { + background-color: #652515; +} + +/* end Your Bet */ + +/* header */ +.site-header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: white; + gap: 50px; + padding: 10px 20px; + height: 60px; + font-family: inherit; +} + +.header-section { + display: flex; + gap: 50px; + padding: 40px; +} + +.icon-with-label { + margin-top: 20px; + width: 50px; + + font-family: inherit; + color: #3F1B12; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; +} + +.icon-button img { + width: 32px; + height: 32px; +} + +.icon-button:hover { + background-color: rgba(255, 255, 255, 0.2); +} + +.icon-button { + background: none; + border: none; + cursor: pointer; + padding: 5px; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + transition: background-color 0.3s ease; +} + +.header-title { font-family: 'CupheadFont', sans-serif; font-size: 48px; + color: #3F1B12; + flex-grow: 1; + text-align: center; } -.container{ + +/* END header */ + +.container { text-align: center; font-size: 24px; } + .roulette_container { position: relative; - /* Adjust to match the circle image size */ display: flex; justify-content: center; align-items: center; @@ -30,61 +196,73 @@ body { } .roulette_circle img { - width: 50%; /* Ensures the image takes up the full container */ + width: 50%; height: auto; animation: none; animation-duration: 5000ms; animation-iteration-count: infinite; - animation-timing-function: linear; + animation-timing-function: linear; } -.roulette_arrow img{ + +.roulette_arrow img { width: 20%; height: auto; } + .roulette_arrow { position: absolute; top: 50%; left: 50%; - transform: translate(-50%, -90%); /* Center align with slight upward adjustment */ - width: 20%; /* Adjust as needed for size */ + transform: translate(-50%, -90%); + width: 20%; } -.bet_form{ + +.bet_form { background-color: #FFF0B3; width: 100%; - height: 300px; + height: 400px; text-align: center; font-family: inherit; border-radius: 8px; - + padding-bottom: 10px; margin-top: 10px; + margin-bottom: 10px; } -.bet_form p{ + +.bet_form p { display: flex; - margin: 10px; + margin: 5px; font-size: 28px; - } -.input-box label{ - font-size: 24px; +.input-box label { + font-size: 24px; } -.input-color{ +.bet-category { display: flex; - padding-left: 15px; - gap: 20px; + padding-left: 20px; + gap: 10px; } -.color-item{ + +.input-label { + padding-left: 15px; + padding-top: 15px; + margin: 0px; +} + +.color-item { display: flex; align-items: center; - gap: 8px; + gap: 0px; } -.color-item p{ + +.color-item p { font-size: 16px; } -.dot{ +.dot { height: 25px; width: 25px; background-color: #080002; @@ -92,13 +270,60 @@ body { display: inline-block; } +.even-odd-item { + display: flex; + align-items: center; + gap: 0px; +} + +.high-low-item { + display: flex; + align-items: center; + gap: 0px; +} +.number-item { + display: flex; + align-items: center; + gap: 0px; +} - @keyframes spin { +.choise-button { + background-color: #3F1B12; + width: 70px; + height: 40px; + font-size: 16px; + border-radius: 16px; + font-family: inherit; + color: white; + border: none; + cursor: pointer; + +} + +.money_icon { + width: 25px; + height: 25px; +} + +.input-box input { + height: 30px; + width: 200px; + padding: 0 15px; + max-width: 200px; + font-family: inherit; + background-color: #E6DDCF; + outline: none; + border-radius: 8px; + border: none; +} + +@keyframes spin { from { - transform:rotate(0deg); + transform: rotate(0deg); } + to { - transform:rotate(360deg); + transform: rotate(360deg); } -} \ No newline at end of file +} \ No newline at end of file