package main import ( "errors" "fmt" "log" "math" "net/http" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) func getEnter(c *gin.Context) { tm.Render(c, "enter", gin.H{}) } func postEnter(c *gin.Context) { current := getSessionUser(c) if current == nil { c.Redirect(http.StatusFound, "/login") return } // Parse form a1h := strings.TrimSpace(c.PostForm("a1")) a2h := strings.TrimSpace(c.PostForm("a2")) b1h := strings.TrimSpace(c.PostForm("b1")) b2h := strings.TrimSpace(c.PostForm("b2")) scoreA := atoiSafe(c.PostForm("scoreA")) scoreB := atoiSafe(c.PostForm("scoreB")) // Require a winner if scoreA == scoreB { SaveForm(c) SetMessage(c, "There must be a winner") c.Redirect(http.StatusSeeOther, "/enter") return } // Resolve players resolveUser := func(handle string) (*User, error) { if handle == "" { return nil, nil } u, err := findUserByHandle(handle) if err != nil { SaveForm(c) SetMessage(c, fmt.Sprintf(`Player "%v" does not exist`, handle)) c.Redirect(http.StatusSeeOther, "/enter") return nil, err } return u, nil } var a1, a2, b1, b2 *User // Always ensure A1 exists, fallback to current if empty var err error if a1, err = resolveUser(a1h); err != nil { return } if a2, err = resolveUser(a2h); err != nil { return } if b1, err = resolveUser(b1h); err != nil { return } if b2, err = resolveUser(b2h); err != nil { return } // Check for at least one player on each side if b1 == nil && b2 == nil || a1 == nil && a2 == nil { SaveForm(c) SetMessage(c, "There must be at least one player on each side") c.Redirect(http.StatusSeeOther, "/enter") return } // Check for duplicate users seen := map[uint]bool{} users := []*User{a1, a2, b1, b2} for i, u := range users { if u != nil { if seen[u.ID] { SaveForm(c) SetMessage(c, fmt.Sprintf(`Player "%v" cannot play twice in one game`, users[i].Username)) c.Redirect(http.StatusSeeOther, "/enter") return } seen[u.ID] = true } } // Create game g := Game{ ScoreA: scoreA, ScoreB: scoreB, } if err := db.Create(&g).Error; err != nil { c.String(http.StatusInternalServerError, "game create error") return } var players []Player var playerbuf []float64 for i, user := range users { if user != nil { var side string if i > 1 { side = "B" } else { side = "A" } players = append(players, Player{u: user, side: side, dElo: 0}) playerbuf = append(playerbuf, user.Elo) } } team1 := NewTeam(players[:len(players)/2], g.ScoreA) team2 := NewTeam(players[len(players)/2:], g.ScoreB) // Set new elo for all players GetNewElo([]*Team{team1, team2}) var winningSide string if g.ScoreA > g.ScoreB { winningSide = "A" } else { winningSide = "B" } for i, p := range players { db.Save(p.u) p.dElo = players[i].u.Elo - playerbuf[i] var updateString string if p.side == winningSide { updateString = "win_count" } else { updateString = "loss_count" } var expr = gorm.Expr(updateString+" + ?", 1) // Update win or loss err = db.Model(&User{}). Where("id = ?", p.u.ID). UpdateColumn(updateString, expr).UpdateColumn("game_count", gorm.Expr("game_count + ?", 1)).Error if err != nil { fmt.Println(err) } if err := db.Create(&GameUser{GameID: g.ID, UserID: p.u.ID, Side: p.side, DeltaElo: p.dElo}).Error; err != nil { c.String(http.StatusInternalServerError, "failed to assign player") return } } c.Redirect(http.StatusFound, "/history") } func getIndex(c *gin.Context) { if u := getSessionUser(c); u != nil { c.Redirect(302, "/enter") return } getLogin(c) } func roundFloat(val float64, precision uint) float64 { ratio := math.Pow(10, float64(precision)) return math.Round(val*ratio) / ratio } func getHistory(c *gin.Context) { // Load recent games var games []Game db.Order("created_at desc").Find(&games) type UserElo struct { User DeltaElo float64 } type GRow struct { Game PlayersA []UserElo PlayersB []UserElo WinnerIsA bool } var rows []GRow for _, g := range games { var gps []GameUser db.Model(&GameUser{}).Preload("User").Where("game_id = ?", g.ID).Find(&gps) var a, b []UserElo for _, gp := range gps { if gp.Side == "A" { a = append(a, UserElo{gp.User, roundFloat(gp.DeltaElo, 2)}) } else { b = append(b, UserElo{gp.User, roundFloat(gp.DeltaElo, 2)}) } } rows = append(rows, GRow{Game: g, PlayersA: a, PlayersB: b, WinnerIsA: g.ScoreA > g.ScoreB}) log.Printf("%+v", rows[0].PlayersA[0].Username) } tm.Render(c, "history", gin.H{"Games": rows}) } func getLeaderboard(c *gin.Context) { var users []User if err := db.Order("elo DESC").Find(&users).Error; err != nil { c.String(http.StatusInternalServerError, "Error: %v", err) return } tm.Render(c, "leaderboard", gin.H{ "Users": users, }) } func getUserView(c *gin.Context) { slug := c.Param("userSlug") u, err := userBySlug(slug) if err != nil { c.String(404, "user not found") return } // Check if own user var own bool if slug == u.Slug { own = true } tm.Render(c, "user", gin.H{"User": u, "Own": own}) } func getMe(c *gin.Context) { u := getSessionUser(c) tm.Render(c, "user", gin.H{"User": u, "Own": true}) } func postMe(c *gin.Context) { cu := getSessionUser(c) if cu == nil { c.Redirect(302, "/login") return } newU := strings.TrimSpace(c.PostForm("username")) if newU == "" || newU == cu.Username { c.Redirect(302, "/me") return } // Update username and slug cu.Username = newU cu.Slug = slugify(newU) if err := ensureUniqueUsernameAndSlug(cu); err != nil { log.Println("unique username error:", err) } if err := db.Save(cu).Error; err != nil { log.Println("save user:", err) } c.Redirect(302, "/me") } func getLogin(c *gin.Context) { tm.Render(c, "login", gin.H{}) } func postLogin(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.PostForm("email"))) if email == "" { c.Redirect(302, "/login") return } // ensure user exists var u User tx := db.Where("email = ?", email).First(&u) if errors.Is(tx.Error, gorm.ErrRecordNotFound) { u = User{Email: email, Username: defaultUsername()} if err := ensureUniqueUsernameAndSlug(&u); err != nil { log.Println(err) } if err := db.Create(&u).Error; err != nil { log.Println("create user:", err) } } // create token valid 30 days token := mustRandToken(24) lt := LoginToken{Token: token, Email: email, ExpiresAt: now().Add(30 * 24 * time.Hour)} if err := db.Create(<).Error; err != nil { log.Println("create token:", err) } link := strings.TrimRight(baseURL, "/") + "/magic?token=" + token log.Printf("[MAGIC LINK] %s for %s (valid until %s)\n", link, email, lt.ExpiresAt.Format(time.RFC3339)) tm.Render(c, "sent", gin.H{"Email": email}) } func getMagic(c *gin.Context) { token := c.Query("token") if token == "" { c.Redirect(302, "/login") return } var lt LoginToken if err := db.Where("token = ? AND expires_at > ?", token, now()).First(<).Error; err != nil { c.String(400, "Invalid or expired token") return } // find or create user by email again (in case) u, err := userByEmail(lt.Email) if err != nil { c.String(500, "user lookup error") return } // create session sessTok := mustRandToken(24) if err := db.Create(&Session{Token: sessTok, UserID: u.ID}).Error; err != nil { c.String(500, "session error") return } setSessionCookie(c, sessTok) c.Redirect(302, "/enter") }