188 lines
5.3 KiB
Go
188 lines
5.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
|
|
"git.haelnorr.com/h/oslstats/internal/db"
|
|
"git.haelnorr.com/h/oslstats/pkg/slapshotapi"
|
|
"github.com/pkg/errors"
|
|
"github.com/uptrace/bun"
|
|
)
|
|
|
|
// PlayerLookupResult stores the resolved player info from a game_user_id lookup
|
|
type PlayerLookupResult struct {
|
|
Player *db.Player
|
|
TeamID int
|
|
Found bool
|
|
Unmapped bool // true if player not in system (potential free agent)
|
|
}
|
|
|
|
// MapGameUserIDsToPlayers creates a lookup map from game_user_id to resolved player info.
|
|
// It looks up players by their SlapID (which corresponds to game_user_id in match logs)
|
|
// and checks their team assignment in the given season/league.
|
|
func MapGameUserIDsToPlayers(
|
|
ctx context.Context,
|
|
tx bun.Tx,
|
|
gameUserIDs []string,
|
|
seasonID, leagueID int,
|
|
) (map[string]*PlayerLookupResult, error) {
|
|
result := make(map[string]*PlayerLookupResult, len(gameUserIDs))
|
|
|
|
// Initialize all as unmapped
|
|
for _, id := range gameUserIDs {
|
|
result[id] = &PlayerLookupResult{Unmapped: true}
|
|
}
|
|
|
|
if len(gameUserIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
// Get all players that have a slap_id matching any of the game_user_ids
|
|
// game_user_id in logs is a string representation of the slapshot player ID (uint32)
|
|
players := []*db.Player{}
|
|
err := tx.NewSelect().
|
|
Model(&players).
|
|
Where("p.slap_id::text IN (?)", bun.In(gameUserIDs)).
|
|
Relation("User").
|
|
Scan(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewSelect players")
|
|
}
|
|
|
|
// Build a map of slapID -> player
|
|
slapIDToPlayer := make(map[string]*db.Player, len(players))
|
|
playerIDs := make([]int, 0, len(players))
|
|
for _, p := range players {
|
|
if p.SlapID != nil {
|
|
key := slapIDStr(*p.SlapID)
|
|
slapIDToPlayer[key] = p
|
|
playerIDs = append(playerIDs, p.ID)
|
|
}
|
|
}
|
|
|
|
// Get team roster entries for these players in the given season/league
|
|
rosters := []*db.TeamRoster{}
|
|
if len(playerIDs) > 0 {
|
|
err = tx.NewSelect().
|
|
Model(&rosters).
|
|
Where("tr.season_id = ?", seasonID).
|
|
Where("tr.league_id = ?", leagueID).
|
|
Where("tr.player_id IN (?)", bun.In(playerIDs)).
|
|
Scan(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewSelect rosters")
|
|
}
|
|
}
|
|
|
|
// Build playerID -> teamID map
|
|
playerTeam := make(map[int]int, len(rosters))
|
|
for _, r := range rosters {
|
|
playerTeam[r.PlayerID] = r.TeamID
|
|
}
|
|
|
|
// Populate results
|
|
for _, id := range gameUserIDs {
|
|
player, found := slapIDToPlayer[id]
|
|
if !found {
|
|
continue // stays unmapped
|
|
}
|
|
teamID, onTeam := playerTeam[player.ID]
|
|
result[id] = &PlayerLookupResult{
|
|
Player: player,
|
|
TeamID: teamID,
|
|
Found: true,
|
|
Unmapped: !onTeam,
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// DetermineTeamOrientation validates that logs match fixture's team assignment
|
|
// by cross-checking player game_user_ids against registered rosters.
|
|
//
|
|
// Returns:
|
|
// - fixtureHomeIsLogsHome: true if fixture's home team maps to "home" in logs
|
|
// - unmappedPlayers: list of game_user_ids that couldn't be resolved
|
|
// - error: if orientation cannot be determined
|
|
func DetermineTeamOrientation(
|
|
ctx context.Context,
|
|
tx bun.Tx,
|
|
fixture *db.Fixture,
|
|
allPlayers []slapshotapi.Player,
|
|
playerLookup map[string]*PlayerLookupResult,
|
|
) (bool, []string, error) {
|
|
if fixture == nil {
|
|
return false, nil, errors.New("fixture cannot be nil")
|
|
}
|
|
|
|
unmapped := []string{}
|
|
|
|
// Count how many fixture-home-team players are on "home" vs "away" in logs
|
|
homeTeamOnHome := 0 // fixture home team players that are "home" in logs
|
|
homeTeamOnAway := 0 // fixture home team players that are "away" in logs
|
|
awayTeamOnHome := 0 // fixture away team players that are "home" in logs
|
|
awayTeamOnAway := 0 // fixture away team players that are "away" in logs
|
|
|
|
for _, p := range allPlayers {
|
|
lookup, exists := playerLookup[p.GameUserID]
|
|
if !exists || lookup.Unmapped {
|
|
unmapped = append(unmapped, p.GameUserID+" ("+p.Username+")")
|
|
continue
|
|
}
|
|
|
|
logTeam := p.Team // "home" or "away" in the log
|
|
|
|
switch lookup.TeamID {
|
|
case fixture.HomeTeamID:
|
|
if logTeam == "home" {
|
|
homeTeamOnHome++
|
|
} else {
|
|
homeTeamOnAway++
|
|
}
|
|
case fixture.AwayTeamID:
|
|
if logTeam == "home" {
|
|
awayTeamOnHome++
|
|
} else {
|
|
awayTeamOnAway++
|
|
}
|
|
default:
|
|
// Player is on a team but not one of the fixture teams
|
|
unmapped = append(unmapped, p.GameUserID+" ("+p.Username+")")
|
|
}
|
|
}
|
|
|
|
totalMapped := homeTeamOnHome + homeTeamOnAway + awayTeamOnHome + awayTeamOnAway
|
|
if totalMapped == 0 {
|
|
return false, unmapped, errors.New("no mapped players found, cannot determine team orientation")
|
|
}
|
|
|
|
// Calculate orientation: how many agree with "home=home" vs "home=away"
|
|
matchOrientation := homeTeamOnHome + awayTeamOnAway // logs match fixture orientation
|
|
reverseOrientation := homeTeamOnAway + awayTeamOnHome // logs are reversed
|
|
|
|
if matchOrientation == reverseOrientation {
|
|
return false, unmapped, errors.New("cannot determine team orientation: equal evidence for both orientations")
|
|
}
|
|
|
|
fixtureHomeIsLogsHome := matchOrientation > reverseOrientation
|
|
return fixtureHomeIsLogsHome, unmapped, nil
|
|
}
|
|
|
|
// FloatToIntPtr converts a *float64 to *int by truncating the decimal.
|
|
// Returns nil if input is nil.
|
|
func FloatToIntPtr(f *float64) *int {
|
|
if f == nil {
|
|
return nil
|
|
}
|
|
v := int(math.Round(*f))
|
|
return &v
|
|
}
|
|
|
|
// slapIDStr converts a uint32 SlapID to a string for map lookups
|
|
func slapIDStr(id uint32) string {
|
|
return fmt.Sprintf("%d", id)
|
|
}
|