added log file uploading and match results
This commit is contained in:
187
internal/handlers/fixture_result_validation.go
Normal file
187
internal/handlers/fixture_result_validation.go
Normal file
@@ -0,0 +1,187 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user