Files
oslstats/internal/handlers/fixture_result_validation.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)
}