sorarebuddy/db/player.go
2024-05-23 08:18:54 +04:00

367 lines
18 KiB
Go

package db
import (
"context"
"time"
"git.lehouerou.net/laurent/sorare/types"
"github.com/pkg/errors"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"github.com/uptrace/bun"
"git.lehouerou.net/laurent/sorarebuddy/model"
)
type PlayerRepository struct {
*Repository[model.Player]
}
func NewPlayerRepository(db *bun.DB) *PlayerRepository {
return &PlayerRepository{
Repository: NewRepository[model.Player](db, []string{"slug"}),
}
}
func (r *PlayerRepository) SearchByDisplayName(
ctx context.Context,
displayName string,
limit int,
) ([]model.Player, error) {
var players []model.Player
err := r.db.NewSelect().
Model(&players).
Relation("Team").
Relation("DomesticLeague").
Relation("DomesticLeague.Zone").
Where("f_unaccent(player.display_name) ILIKE ?", "%"+displayName+"%").
Limit(limit).
Scan(ctx)
return players, err
}
func (r *PlayerRepository) GetOne(ctx context.Context, slug string) (model.Player, error) {
var player model.Player
err := r.db.NewSelect().
Model(&player).
Where("player.slug = ?", slug).
Relation("CardSupply").
Relation("ClubMembership").
Relation("ClubMembership.Team").
Relation("ClubMembership.Team.Country").
Relation("ClubMembership.Team.DomesticLeague").
Relation("ClubMembership.Team.DomesticLeague.Zone").
Relation("Country").
Relation("Team").
Relation("DomesticLeague").
Relation("DomesticLeague.Zone").
Scan(ctx)
return player, err
}
func (r *PlayerRepository) GetMany(ctx context.Context, slugs ...string) ([]model.Player, error) {
var players []model.Player
err := r.db.NewSelect().
Model(&players).
Where("player.slug IN (?)", bun.In(slugs)).
Relation("CardSupply").
Relation("ClubMembership").
Relation("Country").
Relation("Team").
Relation("DomesticLeague").
Relation("DomesticLeague.Zone").
Scan(ctx)
return players, err
}
func (r *PlayerRepository) GetAll(ctx context.Context) ([]model.Player, error) {
var players []model.Player
err := r.db.NewSelect().
Model(&players).
Scan(ctx)
return players, err
}
func (r *PlayerRepository) GetPlayerSlugsNotInDb(ctx context.Context, playerSlugs []string) ([]string, error) {
var players []model.Player
err := r.db.NewSelect().
Model(&players).
Where("slug IN (?)", bun.In(playerSlugs)).
Scan(ctx)
if err != nil {
return nil, errors.Wrap(err, "getting players not in db")
}
diff, _ := lo.Difference(playerSlugs, lo.Map(players, func(p model.Player, index int) string { return p.Slug }))
return diff, nil
}
type SingleRankingsOptions struct {
StartDate time.Time `json:"startDate"`
Position types.Position `json:"position"`
Competitions []string `json:"competitions"`
Zones []int `json:"zones"`
OnlyClubGames bool `json:"onlyClubGames"`
OnlyStarting bool `json:"onlyStarting"`
MinGameCount int `json:"minGameCount"`
MinTeamGamesPlayedPercentage int `json:"minTeamGamesPlayedPercentage"`
Order string `json:"order"`
Limit int `json:"limit"`
Rarity types.Rarity `json:"rarity"`
U23 bool `json:"u23"`
MinTotalMinutes int `json:"minTotalMinutes"`
MinAge int `json:"minAge"`
MaxAge int `json:"maxAge"`
HasGameInNextGw bool `json:"hasGameInNextGw"`
}
type SingleRanking struct {
PlayerSlug string `bun:"player_slug" json:"playerSlug"`
TeamSlug string `bun:"team_slug" json:"teamSlug"`
AvgTeamGoalsFor decimal.Decimal `bun:"avg_team_goals_for" json:"avgTeamGoalsFor"`
AvgTeamGoalsAgainst decimal.Decimal `bun:"avg_team_goals_against" json:"avgTeamGoalsAgainst"`
TotalAvgScore decimal.Decimal `bun:"total_avg_score" json:"totalAvgScore"`
DecisiveAvgScore decimal.Decimal `bun:"decisive_avg_score" json:"decisiveAvgScore"`
AllAroundAvgScore decimal.Decimal `bun:"all_around_avg_score" json:"allAroundAvgScore"`
MinScore decimal.Decimal `bun:"min_score" json:"minScore"`
MaxScore decimal.Decimal `bun:"max_score" json:"maxScore"`
TotalStddevScore decimal.Decimal `bun:"total_stddev_score" json:"totalStddevScore"`
GameUsedInStatsCount int `bun:"game_used_in_stats_count" json:"gameUsedInStatsCount"`
GameStartedCount int `bun:"game_started_count" json:"gameStartedCount"`
GameBenchedPlayedCount int `bun:"game_benched_played_count" json:"gameBenchedPlayedCount"`
GameBenchedUnplayedCount int `bun:"game_benched_unplayed_count" json:"gameBenchedUnplayedCount"`
TeamsGameCount int `bun:"teams_game_count" json:"teamsGameCount"`
TotalPossibleMinutes int `bun:"total_possible_minutes" json:"totalPossibleMinutes"`
PercentageMinutesPlayed decimal.Decimal `bun:"percentage_minutes_played" json:"percentageMinutesPlayed"`
StackPlayPercentage decimal.Decimal `bun:"stack_play_percentage" json:"stackPlayPercentage"`
Floor decimal.Decimal `bun:"floor" json:"floor"`
MoreThan80Score int `bun:"more_than_80_score" json:"moreThan80Score"`
MoreThan70Score int `bun:"more_than_70_score" json:"moreThan70Score"`
MoreThan60Score int `bun:"more_than_60_score" json:"moreThan60Score"`
MoreThan50Score int `bun:"more_than_50_score" json:"moreThan50Score"`
DecisiveCount int `bun:"decisive_count" json:"decisiveCount"`
MoreThan40AA int `bun:"more_than_40_aa" json:"moreThan40AA"`
MoreThan30AA int `bun:"more_than_30_aa" json:"moreThan30AA"`
MoreThan20AA int `bun:"more_than_20_aa" json:"moreThan20AA"`
MoreThan10AA int `bun:"more_than_10_aa" json:"moreThan10AA"`
MoreThan80ScorePercentage decimal.Decimal `bun:"more_than_80_score_percentage" json:"moreThan80ScorePercentage"`
MoreThan70ScorePercentage decimal.Decimal `bun:"more_than_70_score_percentage" json:"moreThan70ScorePercentage"`
MoreThan60ScorePercentage decimal.Decimal `bun:"more_than_60_score_percentage" json:"moreThan60ScorePercentage"`
MoreThan50ScorePercentage decimal.Decimal `bun:"more_than_50_score_percentage" json:"moreThan50ScorePercentage"`
DecisiveCountPercentage decimal.Decimal `bun:"decisive_percentage" json:"decisivePercentage"`
MoreThan40AAPercentage decimal.Decimal `bun:"more_than_40_aa_percentage" json:"moreThan40AAPercentage"`
MoreThan30AAPercentage decimal.Decimal `bun:"more_than_30_aa_percentage" json:"moreThan30AAPercentage"`
MoreThan20AAPercentage decimal.Decimal `bun:"more_than_20_aa_percentage" json:"moreThan20AAPercentage"`
MoreThan10AAPercentage decimal.Decimal `bun:"more_than_10_aa_percentage" json:"moreThan10AAPercentage"`
TotalMinutes decimal.Decimal `bun:"total_minutes" json:"totalMinutes"`
TotalAA decimal.Decimal `bun:"total_aa" json:"totalAA"`
AAPerMin decimal.Decimal `bun:"aa_per_min" json:"aaPerMin"`
Scores []*decimal.Decimal `bun:"scores,array" json:"scores"`
Player model.Player `bun:"-" json:"player"`
}
func CalculateCutoffBirthdate(age int) time.Time {
// Get the current time.
now := time.Now()
// Subtract the age from the current year.
// Also subtract 6 months to reverse the original SQL operation.
birthYear := now.Year() - age
if now.Month() < 7 { // If before July, subtract an additional year.
birthYear--
}
// Construct the birth date using the calculated birth year.
// The month and day are set to the same as the current date for simplicity.
// Adjust these values as needed for your specific requirements.
birthDate := time.Date(birthYear, now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
return birthDate
}
func (r *PlayerRepository) SingleRankings(ctx context.Context, opts SingleRankingsOptions) ([]SingleRanking, error) {
minAgeBirthdate := CalculateCutoffBirthdate(opts.MinAge)
maxAgeBirthdate := CalculateCutoffBirthdate(opts.MaxAge)
// Return the list of team slugs for teams that have games in the currently opened game week.
NextGWGameCountRequest := r.db.NewSelect().
ColumnExpr("t.slug").
TableExpr("teams AS t").
Join("JOIN games AS g ON g.away_team_slug = t.slug OR g.home_team_slug = t.slug").
Join("JOIN fixtures AS f ON f.slug = g.fixture_slug").
Where("f.state = 'opened'").
Group("t.slug")
FilteredGames := r.db.NewSelect().
ColumnExpr("g.*").
TableExpr("games AS g").
Where("g.date >= ?", opts.StartDate)
if opts.OnlyClubGames {
FilteredGames.Join("JOIN competitions AS c ON c.slug = g.competition_slug")
FilteredGames.Where("c.competition_type = 'CLUB'")
}
// Return for each player the sum of supplies over all seasons
PlayersSupplies := r.db.NewSelect().
ColumnExpr("cs.player_slug").
ColumnExpr("SUM(cs.limited) AS limited").
ColumnExpr("SUM(cs.rare) AS rare").
ColumnExpr("SUM(cs.super_rare) AS super_rare").
ColumnExpr("SUM(cs.unique) AS unique").
TableExpr("card_supplies AS cs").
Group("player_slug")
FilteredPlayers := r.db.NewSelect().
ColumnExpr("p.*").
TableExpr("players AS p").
Join("JOIN \"Supplies\" AS s ON s.player_slug = p.slug").
Join("JOIN competitions AS dc ON dc.slug = p.domestic_league_slug").
Where("p.field_position = ?", opts.Position).
Where("p.birth_date >= ?", maxAgeBirthdate).
Where("p.birth_date <= ?", minAgeBirthdate).
Where("dc.zone_id IS NOT NULL")
if opts.U23 {
FilteredPlayers.Where("p.birth_date >= ?", CalculateCutoffBirthdate(23))
}
if len(opts.Competitions) > 0 {
FilteredPlayers.Where("dc.slug IN (?)", bun.In(opts.Competitions))
}
if len(opts.Zones) > 0 {
FilteredPlayers.Where("dc.zone_id IN (?)", bun.In(opts.Zones))
}
if opts.HasGameInNextGw {
FilteredPlayers.Join("JOIN \"NextGWGameCount\" AS ngc ON ngc.slug = p.team_slug")
}
PlayerPossibleGameCount := r.db.NewSelect().
ColumnExpr("p.slug").
ColumnExpr("COUNT(DISTINCT g.id) AS total_game_count").
ColumnExpr("SUM(g.minutes) AS total_minutes").
ColumnExpr("ARRAY_AGG(COALESCE(fs.score, -1) ORDER BY g.date DESC) AS scores").
ColumnExpr("COUNT(DISTINCT CASE WHEN gp.status = 'starting' THEN gp.game_id ELSE NULL END) AS game_started_count").
ColumnExpr("COUNT(DISTINCT CASE WHEN gp.status = 'bench' AND fs.minutes_played > 0 THEN gp.game_id ELSE NULL END) AS game_benched_played_count").
ColumnExpr("COUNT(DISTINCT CASE WHEN gp.status = 'bench' AND fs.minutes_played = 0 THEN gp.game_id ELSE NULL END) AS game_benched_unplayed_count").
ColumnExpr("SUM(fs.minutes_played) * 100 / SUM(g.minutes) AS percentage_minutes_played").
TableExpr("\"FilteredPlayers\" AS p").
Join("JOIN memberships AS m ON m.player_slug = p.slug AND m.start_date <= now() AND (m.end_date IS NULL OR m.end_date >= ?)", opts.StartDate).
Join("JOIN games AS g ON (g.away_team_slug = m.team_slug OR g.home_team_slug = m.team_slug) AND g.date >= GREATEST(m.start_date, ?) AND (g.date <= m.end_date OR m.end_date IS NULL) AND g.date < DATE(now())", opts.StartDate).
Join("LEFT JOIN game_players AS gp ON gp.player_slug = p.slug AND gp.game_id = g.id").
Join("LEFT JOIN game_player_scores AS fs ON fs.player_slug = p.slug AND fs.game_id = g.id").
Group("p.slug")
if opts.OnlyClubGames {
PlayerPossibleGameCount.Where("m.membership_type = 'club'")
}
// Return the list of games played by players filtered out with params
gamePlayersRequest := r.db.NewSelect().
ColumnExpr("gp.player_slug").
ColumnExpr("gp.game_id").
ColumnExpr("gp.team_slug").
ColumnExpr("CASE WHEN gp.team_slug = g.home_team_slug THEN g.home_goals ELSE g.away_goals END as team_goals_for").
ColumnExpr("CASE WHEN gp.team_slug = g.home_team_slug THEN g.away_goals ELSE g.home_goals END as team_goals_against").
ColumnExpr("CASE WHEN gp.team_slug = g.home_team_slug THEN 'H' ELSE 'A' END AS home_away").
TableExpr("\"FilteredGames\" AS g").
Join("JOIN game_players AS gp ON g.id = gp.game_id").
Join("JOIN \"FilteredPlayers\" AS p ON p.slug = gp.player_slug")
if opts.OnlyStarting {
gamePlayersRequest.Where("gp.status = 'starting'")
}
scoresRequest := r.db.NewSelect().
ColumnExpr("gp.player_slug").
ColumnExpr("round(AVG(gp.team_goals_for), 2) AS avg_team_goals_for").
ColumnExpr("round(AVG(gp.team_goals_against), 2) AS avg_team_goals_against").
ColumnExpr("round(AVG(fs.score), 2) AS total_avg_score").
ColumnExpr("round(AVG(fs.decisive_score), 2) AS decisive_avg_score").
ColumnExpr("round(AVG(fs.all_around_score), 2) AS all_around_avg_score").
ColumnExpr("min(fs.score) as min_score").
ColumnExpr("max(fs.score) as max_score").
ColumnExpr("COALESCE(round(stddev(fs.score),2), 0) AS total_stddev_score").
ColumnExpr("COUNT(*) AS game_used_in_stats_count").
ColumnExpr("COUNT(CASE WHEN fs.score > 80 THEN 1 ELSE NULL END) AS more_than_80_score").
ColumnExpr("COUNT(CASE WHEN fs.score > 70 THEN 1 ELSE NULL END) AS more_than_70_score").
ColumnExpr("COUNT(CASE WHEN fs.score > 60 THEN 1 ELSE NULL END) AS more_than_60_score").
ColumnExpr("COUNT(CASE WHEN fs.score > 50 THEN 1 ELSE NULL END) AS more_than_50_score").
ColumnExpr("COUNT(CASE WHEN fs.decisive_score > 60 THEN 1 ELSE NULL END) AS decisive_count").
ColumnExpr("COUNT(CASE WHEN fs.all_around_score > 40 THEN 1 ELSE NULL END) AS more_than_40_aa").
ColumnExpr("COUNT(CASE WHEN fs.all_around_score > 30 THEN 1 ELSE NULL END) AS more_than_30_aa").
ColumnExpr("COUNT(CASE WHEN fs.all_around_score > 20 THEN 1 ELSE NULL END) AS more_than_20_aa").
ColumnExpr("COUNT(CASE WHEN fs.all_around_score > 10 THEN 1 ELSE NULL END) AS more_than_10_aa").
ColumnExpr("SUM(fs.minutes_played) as total_minutes").
ColumnExpr("SUM(fs.all_around_score) as total_aa").
ColumnExpr("COALESCE(ROUND(SUM(fs.all_around_score) / NULLIF(SUM(fs.minutes_played), 0), 3), 0) AS aa_per_min").
TableExpr("\"FilteredGamePlayers\" AS gp").
Join("JOIN game_player_scores AS fs ON gp.player_slug = fs.player_slug AND gp.game_id = fs.game_id").
Group("gp.player_slug")
filteredScoresRequest := r.db.NewSelect().
ColumnExpr("s.*").
ColumnExpr("ROUND(((s.game_used_in_stats_count::float / ppgc.total_game_count::float) * 100)::numeric, 2) AS stack_play_percentage").
ColumnExpr("ROUND(((s.more_than_80_score::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_80_score_percentage").
ColumnExpr("ROUND(((s.more_than_70_score::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_70_score_percentage").
ColumnExpr("ROUND(((s.more_than_60_score::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_60_score_percentage").
ColumnExpr("ROUND(((s.more_than_50_score::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_50_score_percentage").
ColumnExpr("ROUND(((s.decisive_count::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS decisive_percentage").
ColumnExpr("ROUND(((s.more_than_40_aa::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_40_aa_percentage").
ColumnExpr("ROUND(((s.more_than_30_aa::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_30_aa_percentage").
ColumnExpr("ROUND(((s.more_than_20_aa::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_20_aa_percentage").
ColumnExpr("ROUND(((s.more_than_10_aa::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_10_aa_percentage").
ColumnExpr("ppgc.game_started_count").
ColumnExpr("ppgc.game_benched_played_count").
ColumnExpr("ppgc.game_benched_unplayed_count").
ColumnExpr("ppgc.total_game_count AS teams_game_count").
ColumnExpr("ppgc.scores").
ColumnExpr("ppgc.total_minutes AS total_possible_minutes").
ColumnExpr("ppgc.percentage_minutes_played").
TableExpr("\"Scores\" AS s").
Join("JOIN \"PlayerPossibleGameCount\" AS ppgc ON ppgc.slug = s.player_slug").
Where("s.game_used_in_stats_count >= ?", opts.MinGameCount).
Where("s.total_minutes >= ?", opts.MinTotalMinutes)
mainRequest := r.db.NewSelect().
With("Supplies", PlayersSupplies).
With("NextGWGameCount", NextGWGameCountRequest).
With("FilteredGames", FilteredGames).
With("FilteredPlayers", FilteredPlayers).
With("PlayerPossibleGameCount", PlayerPossibleGameCount).
With("FilteredGamePlayers", gamePlayersRequest).
With("Scores", scoresRequest).
With("FilteredScores", filteredScoresRequest).
ColumnExpr("fs.*").
TableExpr("\"FilteredScores\" AS fs").
Where("fs.stack_play_percentage >= ?", opts.MinTeamGamesPlayedPercentage).
Order(opts.Order).
Limit(opts.Limit)
var res []SingleRanking
err := mainRequest.Scan(ctx, &res)
if err != nil {
return nil, errors.Wrap(err, "selecting single rankings")
}
playerSlugs := lo.Map(res, func(r SingleRanking, _ int) string {
return r.PlayerSlug
})
if len(playerSlugs) == 0 {
return res, nil
}
players, err := r.GetMany(ctx, playerSlugs...)
if err != nil {
return nil, errors.Wrap(err, "selecting players")
}
for i, re := range res {
for _, p := range players {
if re.PlayerSlug == p.Slug {
res[i].Player = p
break
}
}
}
return res, nil
}