Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func setupRouter(cfg *config.Config) *gin.Engine {

// WebSocket routes (handle auth internally)
router.GET("/ws/matchmaking", websocket.MatchmakingHandler)
router.GET("/ws/gamification", websocket.GamificationWebSocketHandler)

// Protected routes (JWT auth)
auth := router.Group("/")
Expand All @@ -112,6 +113,11 @@ func setupRouter(cfg *config.Config) *gin.Engine {
auth.PUT("/user/updateprofile", routes.UpdateProfileRouteHandler)
auth.GET("/leaderboard", routes.GetLeaderboardRouteHandler)
auth.POST("/debate/result", routes.UpdateRatingAfterDebateRouteHandler)

// Gamification routes
auth.POST("/api/award-badge", routes.AwardBadgeRouteHandler)
auth.POST("/api/update-score", routes.UpdateScoreRouteHandler)
auth.GET("/api/leaderboard", routes.GetGamificationLeaderboardRouteHandler)
routes.SetupDebateVsBotRoutes(auth)

// WebSocket signaling endpoint (handles auth internally)
Expand Down
6 changes: 6 additions & 0 deletions backend/controllers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func GoogleLogin(ctx *gin.Context) {
LastRatingUpdate: now,
AvatarURL: avatarURL,
IsVerified: true,
Score: 0, // Initialize gamification score
Badges: []string{}, // Initialize badges array
CurrentStreak: 0, // Initialize streak
CreatedAt: now,
UpdatedAt: now,
}
Expand Down Expand Up @@ -168,6 +171,9 @@ func SignUp(ctx *gin.Context) {
Password: string(hashedPassword),
IsVerified: false,
VerificationCode: verificationCode,
Score: 0, // Initialize gamification score
Badges: []string{}, // Initialize badges array
CurrentStreak: 0, // Initialize streak
CreatedAt: now,
UpdatedAt: now,
}
Expand Down
236 changes: 236 additions & 0 deletions backend/controllers/debatevsbot_controller.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package controllers

import (
"context"
"encoding/json"
"log"
"strings"
"time"

"arguehub/db"
"arguehub/models"
"arguehub/services"
"arguehub/utils"
"arguehub/websocket"

"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)

Expand Down Expand Up @@ -274,7 +278,239 @@ func JudgeDebate(c *gin.Context) {
nil,
)

// Update gamification (score, badges, streaks) after bot debate
log.Printf("About to call updateGamificationAfterBotDebate for user %s, result: %s, topic: %s",
userID.Hex(), resultStatus, latestDebate.Topic)

// Call synchronously but with recover to prevent panics from crashing the request
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in updateGamificationAfterBotDebate: %v", r)
}
}()
updateGamificationAfterBotDebate(userID, resultStatus, latestDebate.Topic)
}()

c.JSON(200, JudgeResponse{
Result: result,
})
}
// updateGamificationAfterBotDebate updates user score, checks for badges, and updates streaks after a bot debate
func updateGamificationAfterBotDebate(userID primitive.ObjectID, resultStatus, topic string) {
// Add recover to catch any panics
defer func() {
if r := recover(); r != nil {
log.Printf("Panic recovered in updateGamificationAfterBotDebate: %v", r)
}
}()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

log.Printf("Starting gamification update for user %s, result: %s", userID.Hex(), resultStatus)

// Check if database is initialized
if db.MongoDatabase == nil {
log.Printf("ERROR: MongoDatabase is nil! Cannot update gamification.")
return
}

userCollection := db.MongoDatabase.Collection("users")
log.Printf("User collection retrieved, attempting to find user %s", userID.Hex())

// Get current user to check existing badges and score
var user models.User
err := userCollection.FindOne(ctx, bson.M{"_id": userID}).Decode(&user)
if err != nil {
log.Printf("ERROR: Failed to get user for gamification update: %v (userID: %s)", err, userID.Hex())
return
}

log.Printf("Successfully retrieved user: %s (email: %s)", userID.Hex(), user.Email)

log.Printf("Current user score: %d, badges: %v", user.Score, user.Badges)

// Ensure score field exists - if it's 0 or not set, initialize it
// Note: MongoDB's $inc will create the field if it doesn't exist, but we'll ensure it's set
if user.Score < 0 {
// If score is negative (shouldn't happen), reset it to 0
user.Score = 0
}

// Calculate points based on result
var pointsToAdd int
var action string
switch resultStatus {
case "win":
pointsToAdd = 50 // Points for winning against bot
action = "debate_win"
case "loss":
pointsToAdd = 10 // Participation points
action = "debate_loss"
case "draw":
pointsToAdd = 25 // Points for draw
action = "debate_complete"
default:
pointsToAdd = 10 // Default participation points
action = "debate_complete"
}

log.Printf("Adding %d points for result: %s", pointsToAdd, resultStatus)

// Update user score atomically - $inc will create the field if it doesn't exist
update := bson.M{
"$inc": bson.M{"score": pointsToAdd},
"$set": bson.M{"updatedAt": time.Now()},
}

// Use UpdateOne first to ensure the update happens
updateResult, err := userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update)
if err != nil {
log.Printf("Error updating score with UpdateOne: %v", err)
return
}

if updateResult.MatchedCount == 0 {
log.Printf("User not found for score update: %s", userID.Hex())
return
}

// Now fetch the updated user
var updatedUser models.User
err = userCollection.FindOne(ctx, bson.M{"_id": userID}).Decode(&updatedUser)
if err != nil {
log.Printf("Error fetching updated user: %v", err)
return
}

log.Printf("Successfully updated score. New score: %d (was %d, added %d)",
updatedUser.Score, user.Score, pointsToAdd)

// Save score update record
scoreCollection := db.MongoDatabase.Collection("score_updates")
scoreUpdate := models.ScoreUpdate{
ID: primitive.NewObjectID(),
UserID: userID,
Points: pointsToAdd,
Action: action,
CreatedAt: time.Now(),
Metadata: map[string]interface{}{
"debateType": "user_vs_bot",
"topic": topic,
"result": resultStatus,
},
}
_, err = scoreCollection.InsertOne(ctx, scoreUpdate)
if err != nil {
log.Printf("Error saving score update record: %v", err)
// Don't fail, continue with badge checks
}

// Check for badges (FirstWin, etc.)
hasBadge := make(map[string]bool)
for _, badge := range updatedUser.Badges {
hasBadge[badge] = true
}

// Check for FirstWin badge (first win against bot)
if resultStatus == "win" && !hasBadge["FirstWin"] {
badgeUpdate := bson.M{"$addToSet": bson.M{"badges": "FirstWin"}}
userCollection.UpdateOne(ctx, bson.M{"_id": userID}, badgeUpdate)

// Update the updatedUser object to include the new badge
updatedUser.Badges = append(updatedUser.Badges, "FirstWin")
hasBadge["FirstWin"] = true

// Save badge record
badgeCollection := db.MongoDatabase.Collection("user_badges")
userBadge := models.UserBadge{
ID: primitive.NewObjectID(),
UserID: userID,
BadgeName: "FirstWin",
EarnedAt: time.Now(),
Metadata: map[string]interface{}{
"debateType": "user_vs_bot",
"topic": topic,
},
}
badgeCollection.InsertOne(ctx, userBadge)

// Broadcast badge award via WebSocket
websocket.BroadcastGamificationEvent(models.GamificationEvent{
Type: "badge_awarded",
UserID: userID.Hex(),
BadgeName: "FirstWin",
Timestamp: time.Now(),
})
log.Printf("Awarded FirstWin badge to user %s", userID.Hex())
}

// Check for automatic badges (Novice, Streak5, FactMaster, etc.)
checkAndAwardAutomaticBadges(ctx, userID, updatedUser)

// Broadcast score update via WebSocket
websocket.BroadcastGamificationEvent(models.GamificationEvent{
Type: "score_updated",
UserID: userID.Hex(),
Points: pointsToAdd,
NewScore: updatedUser.Score,
Action: action,
Timestamp: time.Now(),
})

log.Printf("Updated gamification for user %s: +%d points (new score: %d), result: %s",
userID.Hex(), pointsToAdd, updatedUser.Score, resultStatus)
}

// checkAndAwardAutomaticBadges checks if user qualifies for automatic badges
func checkAndAwardAutomaticBadges(ctx context.Context, userID primitive.ObjectID, user models.User) {
userCollection := db.MongoDatabase.Collection("users")
hasBadge := make(map[string]bool)
for _, badge := range user.Badges {
hasBadge[badge] = true
}

// Check for Novice badge (first debate completed)
if user.Score >= 10 && !hasBadge["Novice"] {
update := bson.M{"$addToSet": bson.M{"badges": "Novice"}}
userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update)

websocket.BroadcastGamificationEvent(models.GamificationEvent{
Type: "badge_awarded",
UserID: userID.Hex(),
BadgeName: "Novice",
Timestamp: time.Now(),
})
}

// Check for Streak5 badge (5 day streak)
if user.CurrentStreak >= 5 && !hasBadge["Streak5"] {
update := bson.M{"$addToSet": bson.M{"badges": "Streak5"}}
userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update)

websocket.BroadcastGamificationEvent(models.GamificationEvent{
Type: "badge_awarded",
UserID: userID.Hex(),
BadgeName: "Streak5",
Timestamp: time.Now(),
})
}

// Check for FactMaster badge (high score threshold)
if user.Score >= 500 && !hasBadge["FactMaster"] {
update := bson.M{"$addToSet": bson.M{"badges": "FactMaster"}}
userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update)

websocket.BroadcastGamificationEvent(models.GamificationEvent{
Type: "badge_awarded",
UserID: userID.Hex(),
BadgeName: "FactMaster",
Timestamp: time.Now(),
})
}

// Check for Debater10 badge (10 debates completed)
// Note: This would require tracking debate count, which might need to be added
}

Loading