package domain import ( "math" "testing" "time" ) func TestScoreForPlayerHandlesDrawOvertimeAndAbandon(t *testing.T) { draw := MatchOutcome{Team0Score: 2, Team1Score: 2} if score, err := ScoreForPlayer(draw, "player-1", 0); err != nil || score != 0.5 { t.Fatalf("draw score = %v, %v", score, err) } overtime := MatchOutcome{Team0Score: 2, Team1Score: 3, Overtime: true} if score, err := ScoreForPlayer(overtime, "player-1", 0); err != nil || score != 0 { t.Fatalf("overtime loser score = %v, %v", score, err) } if score, err := ScoreForPlayer(overtime, "player-2", 1); err != nil || score != 1 { t.Fatalf("overtime winner score = %v, %v", score, err) } abandon := MatchOutcome{Team0Score: 0, Team1Score: 10, Abandoners: map[string]bool{"player-1": true}} if score, err := ScoreForPlayer(abandon, "player-1", 0); err != nil || score != 0 { t.Fatalf("abandoner score = %v, %v", score, err) } if _, err := ScoreForPlayer(draw, "", 0); err == nil { t.Fatal("empty player accepted") } } func TestUpdateRatingMatchesCanonicalGlicko2Example(t *testing.T) { current := Rating{Value: 1500, RD: 200, Volatility: 0.06} opponents := []Opponent{ {PlayerID: "a", Rating: Rating{Value: 1400, RD: 30, Volatility: 0.06}, Score: 1, Weight: 1}, {PlayerID: "b", Rating: Rating{Value: 1550, RD: 100, Volatility: 0.06}, Score: 0, Weight: 1}, {PlayerID: "c", Rating: Rating{Value: 1700, RD: 300, Volatility: 0.06}, Score: 0, Weight: 1}, } updated, err := UpdateRating(current, opponents, time.Unix(100000, 0)) if err != nil { t.Fatal(err) } if math.Abs(updated.Value-1464.06) > 0.1 || math.Abs(updated.RD-151.52) > 0.1 || math.Abs(updated.Volatility-0.05999) > 0.0001 { t.Fatalf("canonical vector mismatch: %+v", updated) } } func TestRatingInactivityRaisesRDWithoutChangingRating(t *testing.T) { now := time.Unix(100000, 0) current := Rating{Value: 1600, RD: 100, Volatility: 0.06, LastRatedAt: now} updated, err := UpdateRating(current, nil, now.Add(48*time.Hour+time.Hour)) if err != nil { t.Fatal(err) } if updated.Value != current.Value || updated.RD <= current.RD || updated.RD > GlickoInitialRD { t.Fatalf("bad inactivity update: %+v", updated) } } func TestOpponentWeightHelpersAreExactAndDeterministic(t *testing.T) { opponents := []Opponent{{PlayerID: "c", Rating: Rating{Value: 1700}}, {PlayerID: "a", Rating: Rating{Value: 1400}}, {PlayerID: "b", Rating: Rating{Value: 1550}}} ranked, err := RankedOpponents(opponents) if err != nil { t.Fatal(err) } if ranked[0].PlayerID != "a" || ranked[0].Weight != 1.0/3.0 { t.Fatalf("ranked weighting/order wrong: %+v", ranked) } reordered, err := RankedOpponents([]Opponent{opponents[1], opponents[0], opponents[2]}) if err != nil { t.Fatal(err) } for i := range ranked { if ranked[i].PlayerID != reordered[i].PlayerID { t.Fatal("input order changed opponent order") } } casual, err := CasualOpponents(opponents[:2]) if err != nil { t.Fatal(err) } if casual[0].Weight != 0.5 || casual[1].Weight != 0.5 { t.Fatalf("casual weighting wrong: %+v", casual) } } func TestRatingRejectsInvalidStateAndBadScore(t *testing.T) { _, err := UpdateRating(Rating{Value: 1500, RD: 0, Volatility: 0.06}, nil, time.Now()) if err == nil { t.Fatal("accepted zero RD") } _, err = UpdateRating(Rating{Value: 1500, RD: 200, Volatility: 0.06}, []Opponent{{Rating: Rating{Value: 1500, RD: 100, Volatility: 0.06}, Weight: 1, Score: 2}}, time.Now()) if err == nil { t.Fatal("accepted score outside [0,1]") } }