From 24b960f9cde88e394a0e5a6a35b69f5cdef9489a Mon Sep 17 00:00:00 2001 From: jedibob5 Date: Fri, 13 Mar 2026 17:10:04 -0500 Subject: [PATCH 1/4] Add AI redshirting endpoint --- controller/AdminController.go | 7 +++++++ main.go | 1 + managers/PlayerManager.go | 15 +++++++++++++++ managers/SyncManager.go | 29 +++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/controller/AdminController.go b/controller/AdminController.go index eb8e96b..30c8809 100644 --- a/controller/AdminController.go +++ b/controller/AdminController.go @@ -135,6 +135,13 @@ func SyncAIBoards(w http.ResponseWriter, r *http.Request) { fmt.Println(w, "Team Ranks successfully generated.") } +func AllocateAIRedshirts(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + seasonID := vars["seasonID"] + managers.AllocateAIRedshirts(seasonID) + http.ResponseWriter(w).Write([]byte("Redshirts allocated to AI teams.")) +} + func RunTheGames(w http.ResponseWriter, r *http.Request) { managers.RunTheGames() fmt.Println(w, "Games for current week are set to run.") diff --git a/main.go b/main.go index 15fa42b..7a688ec 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,7 @@ func handleRequests() http.Handler { apiRouter.HandleFunc("/admin/recruiting/class/size", controller.GetRecruitingClassSizeForTeams).Methods("GET") apiRouter.HandleFunc("/admin/ai/fill/boards", controller.FillAIBoards).Methods("GET") apiRouter.HandleFunc("/admin/ai/sync/boards", controller.SyncAIBoards).Methods("GET") + apiRouter.HandleFunc("/admin/ai/apply/redshirts/{seasonID}", controller.AllocateAIRedshirts).Methods("GET") // apiRouter.HandleFunc("/admin/fix/affinities", controller.RecalibrateCrootProfiles).Methods("GET") apiRouter.HandleFunc("/admin/fix/recruit/points", controller.RecalibrateRecruitPoints).Methods("GET") apiRouter.HandleFunc("/admin/run/the/games/", controller.RunTheGames).Methods("GET") diff --git a/managers/PlayerManager.go b/managers/PlayerManager.go index 4265404..d22444b 100644 --- a/managers/PlayerManager.go +++ b/managers/PlayerManager.go @@ -110,6 +110,21 @@ func GetAllCollegePlayersByTeamIdWithoutRedshirts(TeamID string) []structs.Colle return CollegePlayers } +func GetRedshirtEligiblePlayersByTeamId(TeamID string) []structs.CollegePlayer { + db := dbprovider.GetInstance().GetDB() + + var CollegePlayers []structs.CollegePlayer + + db.Order("overall desc"). + Where("team_id = ?", TeamID). + Where("is_redshirting = ?", false). + Where("is_redshirt = ?", false). + Where("has_graduated = ?", false). + Find(&CollegePlayers) + + return CollegePlayers +} + func GetCollegePlayerByCollegePlayerId(CollegePlayerId string) structs.CollegePlayer { db := dbprovider.GetInstance().GetDB() diff --git a/managers/SyncManager.go b/managers/SyncManager.go index c3faeb3..d22be9a 100644 --- a/managers/SyncManager.go +++ b/managers/SyncManager.go @@ -7,6 +7,7 @@ import ( "math/rand" "sort" "strconv" + "strings" "time" "github.com/CalebRose/SimFBA/dbprovider" @@ -1191,6 +1192,34 @@ func getRecruitingOdds(ts structs.Timestamp, croot structs.Recruit, team structs } } +func AllocateAIRedshirts(seasonId string) { + allAICollegeTeams := GetAllAvailableCollegeTeams() + seasonStatsMap := GetCFBPlayerSeasonStatMapBySeason(seasonId, "2") + + for _, team := range allAICollegeTeams { + log.Printf("\nAllocating redshirts for %s\n", team.TeamAbbr) + redshirtTargets := GetRedshirtEligiblePlayersByTeamId(strconv.Itoa(int(team.ID))) + + redshirtCount := 0 + redshirts := make([]string, 20) + for _, target := range redshirtTargets { + if redshirtCount >= 20 { + break + } + + playerSeasonStats := seasonStatsMap[uint(target.PlayerID)] + + // Redshirt top 20 players either without any snaps or with a long-term injury + if playerSeasonStats.GamesPlayed == 0 || (target.InjuryType != "" && target.WeeksOfRecovery >= 10) { + redshirts[redshirtCount] = fmt.Sprintf("%s %s %s %s, GamesPlayed: %d, Injury Weeks: %d\n", team.TeamAbbr, target.Position, target.FirstName, target.LastName, playerSeasonStats.GamesPlayed, target.WeeksOfRecovery) + SetRedshirtStatusForPlayer(strconv.Itoa(target.TeamID)) + redshirtCount++ + } + } + log.Printf("Redshirts for %s:\n%s\n", team.TeamAbbr, strings.Join(redshirts, "")) + } +} + func getCoachMap() map[uint]structs.CollegeCoach { coachMap := make(map[uint]structs.CollegeCoach) From c41e30abe6207f41a7fe5fc118daaa0f2890e792 Mon Sep 17 00:00:00 2001 From: jedibob5 Date: Fri, 13 Mar 2026 17:16:32 -0500 Subject: [PATCH 2/4] add usage comment --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index 7a688ec..890cf47 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,7 @@ func handleRequests() http.Handler { apiRouter.HandleFunc("/admin/recruiting/class/size", controller.GetRecruitingClassSizeForTeams).Methods("GET") apiRouter.HandleFunc("/admin/ai/fill/boards", controller.FillAIBoards).Methods("GET") apiRouter.HandleFunc("/admin/ai/sync/boards", controller.SyncAIBoards).Methods("GET") + // This endpoint should be used right before the redshirt deadline. apiRouter.HandleFunc("/admin/ai/apply/redshirts/{seasonID}", controller.AllocateAIRedshirts).Methods("GET") // apiRouter.HandleFunc("/admin/fix/affinities", controller.RecalibrateCrootProfiles).Methods("GET") apiRouter.HandleFunc("/admin/fix/recruit/points", controller.RecalibrateRecruitPoints).Methods("GET") From 63998737bfb093b4b631d1634f49a9a4725edb01 Mon Sep 17 00:00:00 2001 From: jedibob5 Date: Tue, 24 Mar 2026 19:07:35 -0500 Subject: [PATCH 3/4] Update redshirt logic to check against position minimums --- managers/PlayerManager.go | 15 ------------ managers/SyncManager.go | 48 +++++++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/managers/PlayerManager.go b/managers/PlayerManager.go index d22444b..4265404 100644 --- a/managers/PlayerManager.go +++ b/managers/PlayerManager.go @@ -110,21 +110,6 @@ func GetAllCollegePlayersByTeamIdWithoutRedshirts(TeamID string) []structs.Colle return CollegePlayers } -func GetRedshirtEligiblePlayersByTeamId(TeamID string) []structs.CollegePlayer { - db := dbprovider.GetInstance().GetDB() - - var CollegePlayers []structs.CollegePlayer - - db.Order("overall desc"). - Where("team_id = ?", TeamID). - Where("is_redshirting = ?", false). - Where("is_redshirt = ?", false). - Where("has_graduated = ?", false). - Find(&CollegePlayers) - - return CollegePlayers -} - func GetCollegePlayerByCollegePlayerId(CollegePlayerId string) structs.CollegePlayer { db := dbprovider.GetInstance().GetDB() diff --git a/managers/SyncManager.go b/managers/SyncManager.go index d22be9a..dfdc048 100644 --- a/managers/SyncManager.go +++ b/managers/SyncManager.go @@ -1198,7 +1198,8 @@ func AllocateAIRedshirts(seasonId string) { for _, team := range allAICollegeTeams { log.Printf("\nAllocating redshirts for %s\n", team.TeamAbbr) - redshirtTargets := GetRedshirtEligiblePlayersByTeamId(strconv.Itoa(int(team.ID))) + redshirtTargets := GetAllCollegePlayersByTeamId(strconv.Itoa(int(team.ID))) + positionCountMap := getPositionCounts(redshirtTargets) redshirtCount := 0 redshirts := make([]string, 20) @@ -1209,10 +1210,19 @@ func AllocateAIRedshirts(seasonId string) { playerSeasonStats := seasonStatsMap[uint(target.PlayerID)] - // Redshirt top 20 players either without any snaps or with a long-term injury - if playerSeasonStats.GamesPlayed == 0 || (target.InjuryType != "" && target.WeeksOfRecovery >= 10) { + /* + * Redshirt top 20 players that have never been redshirted and either have no snaps or a long-term injury this season. + * Also skips players if redshirting would put the team below that position minimum. + * GetAllCollegePlayersByTeamId returns players sorted by OVR by default. + */ + if (playerSeasonStats.GamesPlayed == 0 || + (target.InjuryType != "" && target.WeeksOfRecovery >= 10)) && + !target.IsRedshirt && !target.IsRedshirting && + isAboveMinPositionCount(target.Position, positionCountMap) { + redshirts[redshirtCount] = fmt.Sprintf("%s %s %s %s, GamesPlayed: %d, Injury Weeks: %d\n", team.TeamAbbr, target.Position, target.FirstName, target.LastName, playerSeasonStats.GamesPlayed, target.WeeksOfRecovery) - SetRedshirtStatusForPlayer(strconv.Itoa(target.TeamID)) + //SetRedshirtStatusForPlayer(strconv.Itoa(target.TeamID)) + positionCountMap[target.Position] -= 1 redshirtCount++ } } @@ -1220,6 +1230,36 @@ func AllocateAIRedshirts(seasonId string) { } } +func getPositionCounts(players []structs.CollegePlayer) map[string]int { + counts := make(map[string]int) + for _, player := range players { + counts[player.Position]++ + } + return counts +} + +func isAboveMinPositionCount(position string, positionCountMap map[string]int) bool { + minPositionThreshold := 0; + + switch position { + case "ATH": + minPositionThreshold = 0 + case "P", "K": + minPositionThreshold = 1 + case "QB", "RB", "FB", "TE", "FS", "SS", "C": + minPositionThreshold = 3 + case "OT", "OG", "DE", "DT", "OLB", "ILB": + minPositionThreshold = 4 + case "WR", "CB": + minPositionThreshold = 5 + default: + panic(fmt.Sprintf("Invalid position %s found in redshirt logic", position)) + } + + positionCount := positionCountMap[position] + return positionCount > minPositionThreshold +} + func getCoachMap() map[uint]structs.CollegeCoach { coachMap := make(map[uint]structs.CollegeCoach) From 040367652ff53f9a8b43fba3adf011ff391c92c9 Mon Sep 17 00:00:00 2001 From: jedibob5 Date: Tue, 24 Mar 2026 19:08:10 -0500 Subject: [PATCH 4/4] uncomment actual redshirt action from testing --- managers/SyncManager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/managers/SyncManager.go b/managers/SyncManager.go index dfdc048..a938e6b 100644 --- a/managers/SyncManager.go +++ b/managers/SyncManager.go @@ -1221,7 +1221,7 @@ func AllocateAIRedshirts(seasonId string) { isAboveMinPositionCount(target.Position, positionCountMap) { redshirts[redshirtCount] = fmt.Sprintf("%s %s %s %s, GamesPlayed: %d, Injury Weeks: %d\n", team.TeamAbbr, target.Position, target.FirstName, target.LastName, playerSeasonStats.GamesPlayed, target.WeeksOfRecovery) - //SetRedshirtStatusForPlayer(strconv.Itoa(target.TeamID)) + SetRedshirtStatusForPlayer(strconv.Itoa(target.TeamID)) positionCountMap[target.Position] -= 1 redshirtCount++ }