diff --git a/TODO b/TODO index 0ec64d4282..0c57e9f6e2 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,4 @@ -reduce number of bye weeks - see how baseball schedule is divided into series +FBGM kick FG in overtime when losing by TD? https://old.reddit.com/r/Football_GM/comments/1av0fev/overtime_logic_may_need_a_tweak/ FBGM snap counts https://discord.com/channels/@me/778760871911751700/1206814062621491233 - by position? diff --git a/src/worker/core/season/groupScheduleCompact.ts b/src/worker/core/season/groupScheduleCompact.ts new file mode 100644 index 0000000000..4d9b5b8224 --- /dev/null +++ b/src/worker/core/season/groupScheduleCompact.ts @@ -0,0 +1,88 @@ +import { orderBy } from "../../../common/utils"; +import { random } from "../../util"; + +const groupScheduleCompact = (tids: [number, number][]) => { + const dailyMatchups: [number, number][][] = []; + + const matchups = [...tids]; + random.shuffle(matchups); + + const remainingMatchups = new Set(matchups); + + const allTids = Array.from(new Set(tids.flat())); + + while (remainingMatchups.size > 0) { + const tidsUsed = new Set(); + + const matchupsToday: [number, number][] = []; + dailyMatchups.push(matchupsToday); + + // First look at games from teams with the most games left, otherwise they might be left until the end and take up entire days with one team + const numGamesLeftByTid: Record = {}; + for (const matchup of remainingMatchups) { + for (const tid of matchup) { + if (numGamesLeftByTid[tid] === undefined) { + numGamesLeftByTid[tid] = 1; + } else { + numGamesLeftByTid[tid] += 1; + } + } + } + const remainingMatchupsArray = orderBy( + Array.from(remainingMatchups), + matchup => { + return Math.min(...matchup.map(tid => numGamesLeftByTid[tid] ?? 0)); + }, + "desc", + ); + + for (const matchup of remainingMatchupsArray) { + if (!tidsUsed.has(matchup[0]) && !tidsUsed.has(matchup[1])) { + matchupsToday.push(matchup); + remainingMatchups.delete(matchup); + tidsUsed.add(matchup[0]); + tidsUsed.add(matchup[1]); + + if (tidsUsed.size === allTids.length) { + break; + } + } + } + } + + // Start on 2nd to last day, see what we can move to the last day. Keep repeating, going further back each time. This is to make the end of season schedule less "jagged" (fewer teams that end the season early) + for ( + let startIndex = dailyMatchups.length - 2; + startIndex >= 0; + startIndex-- + ) { + for (let i = startIndex; i < dailyMatchups.length - 1; i++) { + const today = dailyMatchups[i]; + const tomorrow = dailyMatchups[i + 1]; + + const tidsTomorrow = new Set(tomorrow.flat()); + + const toRemove = []; + for (let k = 0; k < today.length; k++) { + const matchup = today[k]; + if (!tidsTomorrow.has(matchup[0]) && !tidsTomorrow.has(matchup[1])) { + tomorrow.push(matchup); + toRemove.push(k); + } + } + + // Remove from end, so indexes don't change + toRemove.reverse(); + for (const index of toRemove) { + today.splice(index, 1); + } + } + } + + // Some jaggedness remains, so just randomize it + random.shuffle(dailyMatchups); + + return dailyMatchups.flat(); +}; + +export default groupScheduleCompact; diff --git a/src/worker/core/season/newScheduleGood.ts b/src/worker/core/season/newScheduleGood.ts index b919b9d4db..74eb9b92fb 100644 --- a/src/worker/core/season/newScheduleGood.ts +++ b/src/worker/core/season/newScheduleGood.ts @@ -3,6 +3,8 @@ import { groupByUnique, orderBy, range } from "../../../common/utils"; import type { Div, GameAttributesLeague } from "../../../common/types"; import { TOO_MANY_TEAMS_TOO_SLOW } from "./getInitialNumGamesConfDivSettings"; import groupScheduleSeries from "./groupScheduleSeries"; +import { isSport } from "../../../common"; +import groupScheduleCompact from "./groupScheduleCompact"; type MyTeam = { seasonAttrs: { @@ -753,6 +755,9 @@ const newSchedule = ( if (Object.hasOwn(g, "groupScheduleSeries") && g.get("groupScheduleSeries")) { // Group schedule into series tids = groupScheduleSeries(tids); + } else if (isSport("football")) { + // For football, ideally we'd have explicit bye weeks, but failing that we should at least make the schedule as compact as possible. Whereas the code below makes it only somewhat compact. + tids = groupScheduleCompact(tids); } else { // Order the schedule so that it takes fewer days to play random.shuffle(tids);