From 104030519f0175a56bbb8631a80fc2522a63b504 Mon Sep 17 00:00:00 2001 From: frag223 Date: Tue, 21 Jul 2026 16:56:01 +1000 Subject: [PATCH 1/3] adding independent function for statistics --- internal/games/statistics.go | 64 ++++++++++++++++ internal/games/statistics_test.go | 117 ++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 internal/games/statistics.go create mode 100644 internal/games/statistics_test.go diff --git a/internal/games/statistics.go b/internal/games/statistics.go new file mode 100644 index 0000000..29df451 --- /dev/null +++ b/internal/games/statistics.go @@ -0,0 +1,64 @@ +package games + +func TeamStatisticsForGames(teamID int64, ga []Game) TeamStatistics { + var ts TeamStatistics + + for _, g := range ga { + tmp := TeamStatisticsForGame(teamID, g) + + ts.Wins += tmp.Wins + ts.Losses += tmp.Losses + ts.Draws += tmp.Draws + ts.RoundsWon += tmp.RoundsWon + ts.RoundsLost += tmp.RoundsLost + } + + ts.GamesPlayed = len(ga) + + if ts.GamesPlayed == 0 { + return ts + } + + ts.WinRate = float64(ts.Wins) / float64(ts.GamesPlayed) + ts.RoundDifferential = ts.RoundsWon - ts.RoundsLost + ts.AverageRoundsWon = float64(ts.RoundsWon) / float64(ts.GamesPlayed) + ts.AverageRoundsLost = float64(ts.RoundsLost) / float64(ts.GamesPlayed) + + return ts +} + +func TeamStatisticsForGame(teamID int64, ga Game) TeamStatistics { + var ts TeamStatistics + + if ga.winner == nil { + ga.winner = new(int64) + } + + sideAWins := len(filterResultsByTeam(TeamA, ga.results)) + sideBWins := len(filterResultsByTeam(TeamB, ga.results)) + + switch *ga.winner { + case 0: + ts.Draws += 1 + case teamID: + ts.Wins += 1 + default: + ts.Losses += 1 + } + + teamSide := TeamA + if ga.teamB == teamID { + teamSide = TeamB + } + + switch teamSide { + case TeamA: + ts.RoundsWon += sideAWins + ts.RoundsLost += sideBWins + default: + ts.RoundsWon += sideBWins + ts.RoundsLost += sideAWins + } + + return ts +} diff --git a/internal/games/statistics_test.go b/internal/games/statistics_test.go new file mode 100644 index 0000000..d968464 --- /dev/null +++ b/internal/games/statistics_test.go @@ -0,0 +1,117 @@ +package games + +import ( + "testing" + + "github.com/code-gorilla-au/odize" +) + +func TestTeamStatisticsForGames(t *testing.T) { + group := odize.NewGroup(t, nil) + + teamID := int64(1) + otherTeamID := int64(2) + + err := group. + Test("should return zeroed statistics for empty games list", func(t *testing.T) { + stats := TeamStatisticsForGames(teamID, []Game{}) + odize.AssertEqual(t, 0, stats.GamesPlayed) + odize.AssertEqual(t, 0, stats.Wins) + odize.AssertEqual(t, 0, stats.Losses) + odize.AssertEqual(t, 0, stats.Draws) + odize.AssertEqual(t, 0.0, stats.WinRate) + odize.AssertEqual(t, 0, stats.RoundsWon) + odize.AssertEqual(t, 0, stats.RoundsLost) + odize.AssertEqual(t, 0, stats.RoundDifferential) + odize.AssertEqual(t, 0.0, stats.AverageRoundsWon) + odize.AssertEqual(t, 0.0, stats.AverageRoundsLost) + }). + Test("should calculate statistics correctly for a win", func(t *testing.T) { + game := Game{ + teamA: teamID, + teamB: otherTeamID, + winner: &teamID, + results: []RoundResult{ + {Outcome: TeamA, RemainingPlayers: 1}, + }, + } + stats := TeamStatisticsForGames(teamID, []Game{game}) + odize.AssertEqual(t, 1, stats.GamesPlayed) + odize.AssertEqual(t, 1, stats.Wins) + odize.AssertEqual(t, 0, stats.Losses) + odize.AssertEqual(t, 0, stats.Draws) + odize.AssertEqual(t, 1.0, stats.WinRate) + odize.AssertEqual(t, 1, stats.RoundsWon) + odize.AssertEqual(t, 0, stats.RoundsLost) + odize.AssertEqual(t, 1, stats.RoundDifferential) + odize.AssertEqual(t, 1.0, stats.AverageRoundsWon) + odize.AssertEqual(t, 0.0, stats.AverageRoundsLost) + }). + Test("should calculate statistics correctly for a loss", func(t *testing.T) { + game := Game{ + teamA: teamID, + teamB: otherTeamID, + winner: &otherTeamID, + results: []RoundResult{ + {Outcome: TeamB, RemainingPlayers: 1}, + }, + } + stats := TeamStatisticsForGames(teamID, []Game{game}) + odize.AssertEqual(t, 1, stats.GamesPlayed) + odize.AssertEqual(t, 0, stats.Wins) + odize.AssertEqual(t, 1, stats.Losses) + odize.AssertEqual(t, 0, stats.Draws) + odize.AssertEqual(t, 0.0, stats.WinRate) + odize.AssertEqual(t, 0, stats.RoundsWon) + odize.AssertEqual(t, 1, stats.RoundsLost) + odize.AssertEqual(t, -1, stats.RoundDifferential) + odize.AssertEqual(t, 0.0, stats.AverageRoundsWon) + odize.AssertEqual(t, 1.0, stats.AverageRoundsLost) + }). + Test("should calculate statistics correctly for a draw", func(t *testing.T) { + winner := int64(0) + game := Game{ + teamA: teamID, + teamB: otherTeamID, + winner: &winner, + results: []RoundResult{ + {Outcome: TeamA, RemainingPlayers: 1}, + }, + } + stats := TeamStatisticsForGames(teamID, []Game{game}) + odize.AssertEqual(t, 1, stats.GamesPlayed) + odize.AssertEqual(t, 0, stats.Wins) + odize.AssertEqual(t, 0, stats.Losses) + odize.AssertEqual(t, 1, stats.Draws) + odize.AssertEqual(t, 0.0, stats.WinRate) + odize.AssertEqual(t, 1, stats.RoundsWon) + odize.AssertEqual(t, 0, stats.RoundsLost) + odize.AssertEqual(t, 1, stats.RoundDifferential) + odize.AssertEqual(t, 1.0, stats.AverageRoundsWon) + odize.AssertEqual(t, 0.0, stats.AverageRoundsLost) + }). + Test("should handle nil winner as draw", func(t *testing.T) { + game := Game{ + teamA: teamID, + teamB: otherTeamID, + winner: nil, + results: []RoundResult{ + {Outcome: TeamA, RemainingPlayers: 1}, + }, + } + stats := TeamStatisticsForGames(teamID, []Game{game}) + odize.AssertEqual(t, 1, stats.GamesPlayed) + odize.AssertEqual(t, 0, stats.Wins) + odize.AssertEqual(t, 0, stats.Losses) + odize.AssertEqual(t, 1, stats.Draws) + odize.AssertEqual(t, 0.0, stats.WinRate) + odize.AssertEqual(t, 1, stats.RoundsWon) + odize.AssertEqual(t, 0, stats.RoundsLost) + odize.AssertEqual(t, 1, stats.RoundDifferential) + odize.AssertEqual(t, 1.0, stats.AverageRoundsWon) + odize.AssertEqual(t, 0.0, stats.AverageRoundsLost) + }). + Run() + + odize.AssertNoError(t, err) +} From 5475a6f055633f613c34217934bbc55f7b03edec Mon Sep 17 00:00:00 2001 From: frag223 Date: Tue, 21 Jul 2026 17:04:47 +1000 Subject: [PATCH 2/3] extract + replace for single way of calculating statistics --- internal/games/game.go | 15 ++++++++++ internal/games/service.go | 47 +++---------------------------- internal/games/statistics.go | 2 +- internal/games/statistics_test.go | 2 +- 4 files changed, 21 insertions(+), 45 deletions(-) diff --git a/internal/games/game.go b/internal/games/game.go index 00118dc..69ea4df 100644 --- a/internal/games/game.go +++ b/internal/games/game.go @@ -151,6 +151,21 @@ func fromGameModel(m database.Game) (Game, error) { }, nil } +func fromGameModels(models []database.Game) ([]Game, error) { + var results []Game + + for _, model := range models { + game, err := fromGameModel(model) + if err != nil { + return []Game{}, fmt.Errorf("failed to convert game model: %w", err) + } + + results = append(results, game) + } + + return results, nil +} + func toGameModel(g Game) (database.Game, error) { resolvedWinner := sql.NullInt64{} diff --git a/internal/games/service.go b/internal/games/service.go index d075251..ff40b3b 100644 --- a/internal/games/service.go +++ b/internal/games/service.go @@ -114,49 +114,10 @@ func (s *Service) GetTeamStatistics(ctx context.Context, teamID int64) (TeamStat return TeamStatistics{}, fmt.Errorf("listing completed games: %w", err) } - stats := TeamStatistics{} - for _, model := range models { - if err = s.processGameForStats(&stats, model, teamID); err != nil { - return TeamStatistics{}, err - } - } - - if stats.GamesPlayed == 0 { - return stats, nil - } - - stats.WinRate = (float64(stats.Wins) / float64(stats.GamesPlayed)) * 100 - stats.RoundDifferential = stats.RoundsWon - stats.RoundsLost - stats.AverageRoundsWon = float64(stats.RoundsWon) / float64(stats.GamesPlayed) - stats.AverageRoundsLost = float64(stats.RoundsLost) / float64(stats.GamesPlayed) - - return stats, nil -} - -func (s *Service) processGameForStats(stats *TeamStatistics, model database.Game, teamID int64) error { - stats.GamesPlayed++ - - switch { - case !model.Winner.Valid || model.Winner.Int64 == 0: - stats.Draws++ - case model.Winner.Int64 == teamID: - stats.Wins++ - default: - stats.Losses++ - } - - var results []RoundResult - if err := json.Unmarshal(model.ResultsLog, &results); err != nil { - return fmt.Errorf("parsing game %d results: %w", model.ID, err) - } - - teamRoundsWon := len(filterResultsByTeam(TeamA, results)) - teamRoundsLost := len(filterResultsByTeam(TeamB, results)) - if model.TeamB.Valid && model.TeamB.Int64 == teamID { - teamRoundsWon, teamRoundsLost = teamRoundsLost, teamRoundsWon + playedGames, err := fromGameModels(models) + if err != nil { + return TeamStatistics{}, fmt.Errorf("failed to convert game models: %w", err) } - stats.RoundsWon += teamRoundsWon - stats.RoundsLost += teamRoundsLost - return nil + return TeamStatisticsForGames(teamID, playedGames), nil } diff --git a/internal/games/statistics.go b/internal/games/statistics.go index 29df451..157f41d 100644 --- a/internal/games/statistics.go +++ b/internal/games/statistics.go @@ -19,7 +19,7 @@ func TeamStatisticsForGames(teamID int64, ga []Game) TeamStatistics { return ts } - ts.WinRate = float64(ts.Wins) / float64(ts.GamesPlayed) + ts.WinRate = float64(ts.Wins) / float64(ts.GamesPlayed) * 100 ts.RoundDifferential = ts.RoundsWon - ts.RoundsLost ts.AverageRoundsWon = float64(ts.RoundsWon) / float64(ts.GamesPlayed) ts.AverageRoundsLost = float64(ts.RoundsLost) / float64(ts.GamesPlayed) diff --git a/internal/games/statistics_test.go b/internal/games/statistics_test.go index d968464..e2876a0 100644 --- a/internal/games/statistics_test.go +++ b/internal/games/statistics_test.go @@ -40,7 +40,7 @@ func TestTeamStatisticsForGames(t *testing.T) { odize.AssertEqual(t, 1, stats.Wins) odize.AssertEqual(t, 0, stats.Losses) odize.AssertEqual(t, 0, stats.Draws) - odize.AssertEqual(t, 1.0, stats.WinRate) + odize.AssertEqual(t, 100.00, stats.WinRate) odize.AssertEqual(t, 1, stats.RoundsWon) odize.AssertEqual(t, 0, stats.RoundsLost) odize.AssertEqual(t, 1, stats.RoundDifferential) From 526b9ad5fd1b4524f9de26ae0c35717396f1a237 Mon Sep 17 00:00:00 2001 From: frag223 Date: Tue, 21 Jul 2026 17:23:12 +1000 Subject: [PATCH 3/3] adding method for torunament --- internal/database/game.sql.gen.go | 18 ++++++++++++++++++ internal/database/queries/game.sql | 6 ++++++ internal/tournaments/interfaces.go | 1 + 3 files changed, 25 insertions(+) diff --git a/internal/database/game.sql.gen.go b/internal/database/game.sql.gen.go index ebe898a..e22b655 100644 --- a/internal/database/game.sql.gen.go +++ b/internal/database/game.sql.gen.go @@ -213,6 +213,24 @@ func (q *Queries) ListCompletedGamesByTeam(ctx context.Context, arg ListComplete return items, nil } +const setStageStatus = `-- name: SetStageStatus :exec +update stages +set status = ? +where id = ? +and tournament_id = ? +` + +type SetStageStatusParams struct { + Status string + ID int64 + TournamentID sql.NullInt64 +} + +func (q *Queries) SetStageStatus(ctx context.Context, arg SetStageStatusParams) error { + _, err := q.db.ExecContext(ctx, setStageStatus, arg.Status, arg.ID, arg.TournamentID) + return err +} + const startGame = `-- name: StartGame :exec update games set status = 'running' diff --git a/internal/database/queries/game.sql b/internal/database/queries/game.sql index fd61796..b7f0e87 100644 --- a/internal/database/queries/game.sql +++ b/internal/database/queries/game.sql @@ -62,6 +62,12 @@ insert into stage_games (stage_id, game_id) values (?, ?) returning *; +-- name: SetStageStatus :exec +update stages +set status = ? +where id = ? +and tournament_id = ?; + -- name: UpdateStage :one update stages set name = ?, diff --git a/internal/tournaments/interfaces.go b/internal/tournaments/interfaces.go index 7bf46d4..5cd7e14 100644 --- a/internal/tournaments/interfaces.go +++ b/internal/tournaments/interfaces.go @@ -12,6 +12,7 @@ type Store interface { CreateStage(ctx context.Context, arg database.CreateStageParams) (database.Stage, error) CreateTournament(ctx context.Context, arg database.CreateTournamentParams) (database.Tournament, error) UpdateStage(ctx context.Context, arg database.UpdateStageParams) (database.Stage, error) + SetStageStatus(ctx context.Context, arg database.SetStageStatusParams) error } type TeamsService interface {