From 416b76a62d484ff3b3dd7ae0d760b83e85381f64 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 3 Mar 2026 23:43:07 -0600 Subject: [PATCH 1/7] init: Add timeout validation tests and enhance doTimeout logic --- .../core/GameSim.football/index.test.ts | 47 ++++++++++++++++++- src/worker/core/GameSim.football/index.ts | 24 +++++++--- src/worker/core/GameSim.football/types.ts | 13 +++++ tools/lib/server.ts | 1 + 4 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/worker/core/GameSim.football/index.test.ts b/src/worker/core/GameSim.football/index.test.ts index e7367cd0b4..d98d1f44b7 100644 --- a/src/worker/core/GameSim.football/index.test.ts +++ b/src/worker/core/GameSim.football/index.test.ts @@ -1,4 +1,4 @@ -import { assert, beforeAll, test } from "vitest"; +import { assert, beforeAll, describe, test } from "vitest"; import GameSim from "./index.ts"; import { player, team } from "../index.ts"; import loadTeams from "../game/loadTeams.ts"; @@ -7,6 +7,7 @@ import testHelpers from "../../../test/helpers.ts"; import Play from "./Play.ts"; import { DEFAULT_LEVEL } from "../../../common/budgetLevels.ts"; import { range } from "../../../common/utils.ts"; +import type { teamPlayType } from "./types.ts"; export const genTwoTeams = async () => { testHelpers.resetG(); @@ -63,6 +64,50 @@ test("kick a field goal when down 2 at the end of the game and there is little t assert.strictEqual(game.getPlayType(), "fieldGoalLate"); }); +describe("doTimeout", () => { + test("doesn't allow timeouts if team is out of timeouts", async () => { + const game = await initGameSim(); + game.timeouts = [0, 3]; + game.awaitingKickoff = undefined; + game.doTimeout(0, true, "fieldGoal"); + assert.strictEqual(game.timeouts[0], 0); + game.doTimeout(1, true, "fieldGoal"); + assert.strictEqual(game.timeouts[1], 2); + }); + + test("doesn't allow timeouts if a team is currently kicking an extra point", async () => { + const game = await initGameSim(); + game.timeouts = [3, 3]; + game.awaitingAfterTouchdown = true; + game.doTimeout(0, true, "extraPoint"); + game.doTimeout(1, true, "extraPoint"); + assert.strictEqual(game.timeouts[0], 3); + assert.strictEqual(game.timeouts[1], 3); + }); + + test("doesn't allow timeouts if the play is a kickoff", async () => { + // Test both offense and defense + const game = await initGameSim(); + game.timeouts = [3, 3]; + game.awaitingKickoff = 0; + game.doTimeout(0, true, "kickoff"); + assert.strictEqual(game.timeouts[0], 3); + game.doTimeout(1, true, "kickoff"); + assert.strictEqual(game.timeouts[1], 3); + }); + + test("doesn't allow timeouts if team is waiting for kickoff", async () => { + const game = await initGameSim(); + game.timeouts = [3, 3]; + game.overtimes = 1; + game.awaitingKickoff = 0; + const playType: teamPlayType = "kickoff"; + game.doTimeout(0, true, playType); + assert.strictEqual(game.timeouts[0], 3); + game.doTimeout(1, true, playType); + assert.strictEqual(game.timeouts[1], 3); + }); +}); test("kick a field goal on 4th down to take the lead late in the game", async () => { const game = await initGameSim(); game.probMadeFieldGoal = () => 0.75; diff --git a/src/worker/core/GameSim.football/index.ts b/src/worker/core/GameSim.football/index.ts index 7d3c1f328a..c3f3e611e5 100644 --- a/src/worker/core/GameSim.football/index.ts +++ b/src/worker/core/GameSim.football/index.ts @@ -15,6 +15,7 @@ import type { PlayersOnField, TeamGameSim, Formation, + teamPlayType, } from "./types.ts"; import getInjuryRate from "../GameSim.basketball/getInjuryRate.ts"; import Play, { @@ -609,7 +610,7 @@ class GameSim extends GameSimBase { ); } - getPlayType() { + getPlayType(): teamPlayType { if (this.awaitingKickoff !== undefined) { return Math.random() < this.probOnside() ? "onsideKick" : "kickoff"; } @@ -973,9 +974,9 @@ class GameSim extends GameSimBase { if (clockAtEndOfPlay > 0 && !twoMinuteWarningHappening) { // Timeouts - small chance at any time if (Math.random() < 0.01) { - this.doTimeout(this.o, false); + this.doTimeout(this.o, false, playType); } else if (Math.random() < 0.003) { - this.doTimeout(this.d, false); + this.doTimeout(this.d, false, playType); } // Timeouts - late in game when clock is running @@ -989,19 +990,19 @@ class GameSim extends GameSimBase { if (diff > 0) { // If offense is winning, defense uses timeouts when near the end if (this.clock < 2.5) { - this.doTimeout(this.d, true); + this.doTimeout(this.d, true, playType); } } else { if (this.clock < 1.5) { // If offense is losing or tied, offense uses timeouts when even nearer the end - this.doTimeout(this.o, true); + this.doTimeout(this.o, true, playType); } } } } else { // Before halftime, less aggressive and don't care about score if (this.clock < 1.5) { - this.doTimeout(this.o, true); + this.doTimeout(this.o, true, playType); } } } @@ -1305,7 +1306,16 @@ class GameSim extends GameSimBase { this.updateTeamCompositeRatings(); } - doTimeout(t: TeamNum, toStopClock: boolean) { + doTimeout(t: TeamNum, toStopClock: boolean, playType: teamPlayType) { + if (playType === "kickoff" || playType === "punt") { + return; + } + if (playType === "extraPoint" || playType === "twoPointConversion") { + return; + } + if (this.awaitingKickoff) { + return; + } if (this.timeouts[t] <= 0) { return; } diff --git a/src/worker/core/GameSim.football/types.ts b/src/worker/core/GameSim.football/types.ts index 65a1a3f920..e945c83867 100644 --- a/src/worker/core/GameSim.football/types.ts +++ b/src/worker/core/GameSim.football/types.ts @@ -61,3 +61,16 @@ export type Formation = { off: Partial>; def: Partial>; }; + +// TODO: maybe change the naming of this to be explicit to actions teams can take during their possession (mostly) +export type teamPlayType = + | "fieldGoal" + | "punt" + | "pass" + | "run" + | "extraPoint" + | "kickoff" + | "onsideKick" + | "kneel" + | "twoPointConversion" + | "fieldGoalLate"; diff --git a/tools/lib/server.ts b/tools/lib/server.ts index d122962e02..356449d92a 100644 --- a/tools/lib/server.ts +++ b/tools/lib/server.ts @@ -114,6 +114,7 @@ export const startServer = async (exposeToNetwork: boolean) => { return new Promise((resolve) => { server.listen(port, exposeToNetwork ? "0.0.0.0" : "localhost", () => { console.log(`Local: http://localhost:${port}`); + console.log("Worker console: chrome://inspect/#workers"); if (exposeToNetwork) { console.log(`Network: http://${getIpAddress()}:${port}`); } else { From 3385e64a211dc473f26c880f2412c8ec5445baf4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 4 Mar 2026 00:39:36 -0600 Subject: [PATCH 2/7] fix: remove ability to call timeouts first play of quarter --- src/worker/core/GameSim.football/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/worker/core/GameSim.football/index.ts b/src/worker/core/GameSim.football/index.ts index c3f3e611e5..ef722f38ef 100644 --- a/src/worker/core/GameSim.football/index.ts +++ b/src/worker/core/GameSim.football/index.ts @@ -129,6 +129,7 @@ class GameSim extends GameSimBase { ? g.get("footballOvertimePlayoffs", "current") : g.get("footballOvertime", "current"); + startOfNewPeriod = false; constructor({ gid, day, @@ -391,6 +392,7 @@ class GameSim extends GameSimBase { this.playUntimedPossession ) { this.simPlay(); + this.startOfNewPeriod = false; } // Who gets the ball after halftime? @@ -409,6 +411,9 @@ class GameSim extends GameSimBase { } quarter += 1; + // Once quarter is locked, set clock running to false until next play ran + this.isClockRunning = false; + this.startOfNewPeriod = true; this.team[0].stat.ptsQtrs.push(0); this.team[1].stat.ptsQtrs.push(0); @@ -1316,6 +1321,12 @@ class GameSim extends GameSimBase { if (this.awaitingKickoff) { return; } + // If first play of a quarter, not allowed to call timeout since ball is technically dead + // If clock is at 0, + if (!this.isClockRunning && this.startOfNewPeriod) { + console.log("start of quarter, can't call timeout"); + return; + } if (this.timeouts[t] <= 0) { return; } From a7030ca5036ed412902e0a9bec21fa0481bf7b8b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 8 Mar 2026 16:28:06 -0500 Subject: [PATCH 3/7] more bare bones work for timeouts --- src/worker/core/GameSim.football/index.ts | 130 ++++++++++++++++++++-- 1 file changed, 118 insertions(+), 12 deletions(-) diff --git a/src/worker/core/GameSim.football/index.ts b/src/worker/core/GameSim.football/index.ts index ef722f38ef..b9af6646b2 100644 --- a/src/worker/core/GameSim.football/index.ts +++ b/src/worker/core/GameSim.football/index.ts @@ -615,7 +615,7 @@ class GameSim extends GameSimBase { ); } - getPlayType(): teamPlayType { + getPlayType() { if (this.awaitingKickoff !== undefined) { return Math.random() < this.probOnside() ? "onsideKick" : "kickoff"; } @@ -1311,22 +1311,128 @@ class GameSim extends GameSimBase { this.updateTeamCompositeRatings(); } - doTimeout(t: TeamNum, toStopClock: boolean, playType: teamPlayType) { - if (playType === "kickoff" || playType === "punt") { - return; + /** + * This function determines whether a team should call a timeout, and if so, the reasoning behind it. + * + * If no timeout is called, return undefined. + * If a timeout is called to stop the clock, return "toStopClock". + * If a timeout is called for another reason, return "other". + * Stopping the clock supercedes reasons for calling a timeout. + * @param t + * @param playType + * @returns + */ + shouldCallTimeout( + t: TeamNum, + teamHasPossession: boolean, + playType: ReturnType, + twoMinuteWarning: boolean, + quarter: number, + distance: number, + timeRemaining: number, + currentDown: number, + yardsToGo: number, + ): undefined | "toStopClock" | "other" { + // Timeout cannot be called + if (playType === "kickoff" || playType === "onsideKick") { + return undefined; } if (playType === "extraPoint" || playType === "twoPointConversion") { - return; + return undefined; } - if (this.awaitingKickoff) { - return; + // TODO: ADD THIS VARIABLE IN: First play of quarter cannot be interrupted + if (this.awaitingFirstPlayOfQuarter) { + return undefined; } - // If first play of a quarter, not allowed to call timeout since ball is technically dead - // If clock is at 0, - if (!this.isClockRunning && this.startOfNewPeriod) { - console.log("start of quarter, can't call timeout"); - return; + if (this.timeouts[t] <= 0) { + return undefined; + } + + const randomChanceTimeout = Math.random(); + const RED_ZONE_DISTANCE = 20; + const diff = this.team[t].stat.pts - this.team[1 - t].stat.pts; + const ONE_SCORE_GAME = Math.abs(diff) <= 8; + const inFinalPeriod = quarter >= this.numPeriods; + const inPeriodBeforeHalftime = quarter === Math.ceil(this.numPeriods / 2); + const clockRunning = this.isClockRunning; + + // BEFORE TWO MINUTE WARNING + // Small random chance of timeout due to: + // - playcall confusion + // - substitution issues + // - avoiding delay of game + if (!twoMinuteWarning && !ONE_SCORE_GAME) { + const OFFENSE_TIMEOUT_PROBABILITY = 0.01; + const DEFENSE_TIMEOUT_PROBABILITY = 0.003; + + const prob = teamHasPossession + ? OFFENSE_TIMEOUT_PROBABILITY + : DEFENSE_TIMEOUT_PROBABILITY; + + return randomChanceTimeout < prob ? "other" : undefined; + } + + // RED ZONE PLAYCALL TIMEOUT + // Offense may want correct playcall near endzone + if (teamHasPossession && distance <= RED_ZONE_DISTANCE && ONE_SCORE_GAME) { + if (currentDown >= 3 && randomChanceTimeout < 0.08) { + return "other"; + } + } + + // 4TH DOWN DECISION TIMEOUT + if (teamHasPossession && currentDown === 4) { + if (randomChanceTimeout < 0.15) { + return "other"; + } } + + // GOAL LINE DEFENSIVE TIMEOUT + if (!teamHasPossession && distance <= 5 && ONE_SCORE_GAME) { + if (randomChanceTimeout < 0.07) { + return "other"; + } + } + + // ------------------------- + // CLOCK MANAGEMENT + // ------------------------- + + if (inFinalPeriod) { + if (clockRunning) { + // Defense losing stops clock + if (!teamHasPossession && diff < 0) { + if (timeRemaining < 2.5) { + return "toStopClock"; + } + } + + // Offense losing/tied saves time + if (teamHasPossession && diff <= 0) { + if (timeRemaining < 1.5) { + return "toStopClock"; + } + } + } + } + + // ------------------------- + // END OF HALF FIELD GOAL ICING + // ------------------------- + + if (inPeriodBeforeHalftime) { + if ( + playType === "fieldGoal" && + !teamHasPossession && + timeRemaining < 0.5 + ) { + return randomChanceTimeout < 0.8 ? "other" : undefined; + } + } + + return undefined; + } + doTimeout(t: TeamNum, toStopClock: boolean) { if (this.timeouts[t] <= 0) { return; } From f6e3aeeb8851c3b7d27b55f423850b2819241064 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 9 Mar 2026 00:26:43 -0500 Subject: [PATCH 4/7] comments --- src/worker/core/GameSim.football/index.ts | 63 ++++++++++++++--------- src/worker/core/GameSim.football/types.ts | 13 ----- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/worker/core/GameSim.football/index.ts b/src/worker/core/GameSim.football/index.ts index b9af6646b2..4047f6b75b 100644 --- a/src/worker/core/GameSim.football/index.ts +++ b/src/worker/core/GameSim.football/index.ts @@ -15,7 +15,6 @@ import type { PlayersOnField, TeamGameSim, Formation, - teamPlayType, } from "./types.ts"; import getInjuryRate from "../GameSim.basketball/getInjuryRate.ts"; import Play, { @@ -29,7 +28,6 @@ import { PHASE, STARTING_NUM_TIMEOUTS } from "../../../common/index.ts"; import type { TeamNum } from "../../../common/types.ts"; const teamNums: [TeamNum, TeamNum] = [0, 1]; - const FIELD_GOAL_DISTANCE_YARDS_ADDED_FROM_SCRIMMAGE = 17; const ESTIMATED_SECONDS_PER_KNEEL = 42; @@ -66,6 +64,7 @@ const fatigue = (energy: number, injured: boolean): number => { return energy; }; +export type TeamPlayType = ReturnType; class GameSim extends GameSimBase { team: [TeamGameSim, TeamGameSim]; @@ -101,7 +100,7 @@ class GameSim extends GameSimBase { awaitingAfterTouchdown = false; awaitingAfterSafety = false; - + awaitingFirstPlayOfQuarter = false; awaitingKickoff: TeamNum | undefined; lastHalfAwaitingKickoff: TeamNum; @@ -116,6 +115,8 @@ class GameSim extends GameSimBase { twoMinuteWarningHappened = false; currentPlay: Play; + // TODO: Why do we need to keep track of the prior play? + priorPlay: PlayEvent; lngTracker: LngTracker; @@ -979,9 +980,9 @@ class GameSim extends GameSimBase { if (clockAtEndOfPlay > 0 && !twoMinuteWarningHappening) { // Timeouts - small chance at any time if (Math.random() < 0.01) { - this.doTimeout(this.o, false, playType); + this.doTimeout(this.o, false); } else if (Math.random() < 0.003) { - this.doTimeout(this.d, false, playType); + this.doTimeout(this.d, false); } // Timeouts - late in game when clock is running @@ -995,19 +996,19 @@ class GameSim extends GameSimBase { if (diff > 0) { // If offense is winning, defense uses timeouts when near the end if (this.clock < 2.5) { - this.doTimeout(this.d, true, playType); + this.doTimeout(this.d, true); } } else { if (this.clock < 1.5) { // If offense is losing or tied, offense uses timeouts when even nearer the end - this.doTimeout(this.o, true, playType); + this.doTimeout(this.o, true); } } } } else { // Before halftime, less aggressive and don't care about score if (this.clock < 1.5) { - this.doTimeout(this.o, true, playType); + this.doTimeout(this.o, true); } } } @@ -1048,6 +1049,7 @@ class GameSim extends GameSimBase { dtClockRunning = helpers.bound(clockAtEndOfPlay - 2, 0, Infinity); } + // TODO: Modify this, to call the decrement clock function // Clock dt += dtClockRunning; this.clock -= dt; @@ -1347,7 +1349,7 @@ class GameSim extends GameSimBase { if (this.timeouts[t] <= 0) { return undefined; } - + let finalDecision = undefined; const randomChanceTimeout = Math.random(); const RED_ZONE_DISTANCE = 20; const diff = this.team[t].stat.pts - this.team[1 - t].stat.pts; @@ -1355,7 +1357,29 @@ class GameSim extends GameSimBase { const inFinalPeriod = quarter >= this.numPeriods; const inPeriodBeforeHalftime = quarter === Math.ceil(this.numPeriods / 2); const clockRunning = this.isClockRunning; + const redZonePlayCallTimeout = () => { + if (currentDown >= 3 && chance < 0.08) { + return "other"; + } + return undefined; + }; + const fourthDownDecisionTimeout = () => { + if (currentDown === 4 && chance < 0.15) { + return "other"; + } + return undefined; + }; + const iceFieldGoalTimeout = () => { + if ( + (playType === "fieldGoalLate" || "fieldGoal") && + !teamHasPossession && + timeRemaining < 0.5 + ) { + return randomChanceTimeout < 0.8 ? "other" : undefined; + } + }; + const clockManagementTimeout = () => {}; // BEFORE TWO MINUTE WARNING // Small random chance of timeout due to: // - playcall confusion @@ -1363,6 +1387,7 @@ class GameSim extends GameSimBase { // - avoiding delay of game if (!twoMinuteWarning && !ONE_SCORE_GAME) { const OFFENSE_TIMEOUT_PROBABILITY = 0.01; + // - Include ONE_SCORE_GAME variable here? Otherwise we cannot reach the other flows. const DEFENSE_TIMEOUT_PROBABILITY = 0.003; const prob = teamHasPossession @@ -1375,9 +1400,7 @@ class GameSim extends GameSimBase { // RED ZONE PLAYCALL TIMEOUT // Offense may want correct playcall near endzone if (teamHasPossession && distance <= RED_ZONE_DISTANCE && ONE_SCORE_GAME) { - if (currentDown >= 3 && randomChanceTimeout < 0.08) { - return "other"; - } + redZonePlayCallTimeout(randomChanceTimeout); } // 4TH DOWN DECISION TIMEOUT @@ -1394,9 +1417,7 @@ class GameSim extends GameSimBase { } } - // ------------------------- // CLOCK MANAGEMENT - // ------------------------- if (inFinalPeriod) { if (clockRunning) { @@ -1416,21 +1437,11 @@ class GameSim extends GameSimBase { } } - // ------------------------- // END OF HALF FIELD GOAL ICING - // ------------------------- if (inPeriodBeforeHalftime) { - if ( - playType === "fieldGoal" && - !teamHasPossession && - timeRemaining < 0.5 - ) { - return randomChanceTimeout < 0.8 ? "other" : undefined; - } } - - return undefined; + return finalDecision; } doTimeout(t: TeamNum, toStopClock: boolean) { if (this.timeouts[t] <= 0) { @@ -2124,6 +2135,7 @@ class GameSim extends GameSimBase { qb, defender: p, ydsReturn: yds, + // TODO: THIS IS WHAT WE ARE LOOKING FOR, A POSSESSION CHANGE! }); let fumble = false; @@ -2879,6 +2891,7 @@ class GameSim extends GameSimBase { power : undefined; return random.choice(players, weightFunc); + // If there is an injury, we can call an injury timeout if possible } // Pass undefined as p for some team-only stats diff --git a/src/worker/core/GameSim.football/types.ts b/src/worker/core/GameSim.football/types.ts index e945c83867..65a1a3f920 100644 --- a/src/worker/core/GameSim.football/types.ts +++ b/src/worker/core/GameSim.football/types.ts @@ -61,16 +61,3 @@ export type Formation = { off: Partial>; def: Partial>; }; - -// TODO: maybe change the naming of this to be explicit to actions teams can take during their possession (mostly) -export type teamPlayType = - | "fieldGoal" - | "punt" - | "pass" - | "run" - | "extraPoint" - | "kickoff" - | "onsideKick" - | "kneel" - | "twoPointConversion" - | "fieldGoalLate"; From 5b0c4d3fd319bf7c06bbedb6eef1c52cd28074f1 Mon Sep 17 00:00:00 2001 From: jabapo Date: Mon, 9 Mar 2026 02:45:52 -0400 Subject: [PATCH 5/7] seperate into helper functions, track start of quarter --- src/worker/core/GameSim.football/index.ts | 101 +++++++++++++++------- 1 file changed, 72 insertions(+), 29 deletions(-) diff --git a/src/worker/core/GameSim.football/index.ts b/src/worker/core/GameSim.football/index.ts index 4047f6b75b..fb6cc36de5 100644 --- a/src/worker/core/GameSim.football/index.ts +++ b/src/worker/core/GameSim.football/index.ts @@ -115,8 +115,6 @@ class GameSim extends GameSimBase { twoMinuteWarningHappened = false; currentPlay: Play; - // TODO: Why do we need to keep track of the prior play? - priorPlay: PlayEvent; lngTracker: LngTracker; @@ -130,7 +128,6 @@ class GameSim extends GameSimBase { ? g.get("footballOvertimePlayoffs", "current") : g.get("footballOvertime", "current"); - startOfNewPeriod = false; constructor({ gid, day, @@ -393,7 +390,7 @@ class GameSim extends GameSimBase { this.playUntimedPossession ) { this.simPlay(); - this.startOfNewPeriod = false; + this.awaitingFirstPlayOfQuarter = false; } // Who gets the ball after halftime? @@ -414,7 +411,7 @@ class GameSim extends GameSimBase { quarter += 1; // Once quarter is locked, set clock running to false until next play ran this.isClockRunning = false; - this.startOfNewPeriod = true; + this.awaitingFirstPlayOfQuarter = true; this.team[0].stat.ptsQtrs.push(0); this.team[1].stat.ptsQtrs.push(0); @@ -1342,7 +1339,6 @@ class GameSim extends GameSimBase { if (playType === "extraPoint" || playType === "twoPointConversion") { return undefined; } - // TODO: ADD THIS VARIABLE IN: First play of quarter cannot be interrupted if (this.awaitingFirstPlayOfQuarter) { return undefined; } @@ -1352,34 +1348,86 @@ class GameSim extends GameSimBase { let finalDecision = undefined; const randomChanceTimeout = Math.random(); const RED_ZONE_DISTANCE = 20; - const diff = this.team[t].stat.pts - this.team[1 - t].stat.pts; + // TODO: FIND THIS OUT. Its ball position - scrimmage, not sure if this is fully correct + const distanceToGoalLine = 100 - this.scrimmage; + const diff = this.team[this.o].stat.pts - this.team[this.d].stat.pts; const ONE_SCORE_GAME = Math.abs(diff) <= 8; const inFinalPeriod = quarter >= this.numPeriods; const inPeriodBeforeHalftime = quarter === Math.ceil(this.numPeriods / 2); const clockRunning = this.isClockRunning; + // Redzone playcall timeout. Different percentages based on down, defense, offense, and score. + // If score is close, timeout is more likely, especially for offense. const redZonePlayCallTimeout = () => { - if (currentDown >= 3 && chance < 0.08) { - return "other"; + const OFFENSE_TIMEOUT_PROBABILITY = 0.08; + const DEFENSE_TIMEOUT_PROBABILITY = 0.05; + // TODO: Make timeout probability 2x higher if game is close for offense, 1.5x for defense + if (currentDown >= 3) { + if (yardsToGo <= 5) { + if (teamHasPossession) { + return randomChanceTimeout < OFFENSE_TIMEOUT_PROBABILITY + ? "other" + : undefined; + } else { + return randomChanceTimeout < DEFENSE_TIMEOUT_PROBABILITY + ? "other" + : undefined; + } + } else if (yardsToGo <= 10) { + if (teamHasPossession) { + return randomChanceTimeout < OFFENSE_TIMEOUT_PROBABILITY * 0.5 + ? "other" + : undefined; + } else { + return randomChanceTimeout < DEFENSE_TIMEOUT_PROBABILITY * 0.5 + ? "other" + : undefined; + } + } } return undefined; }; const fourthDownDecisionTimeout = () => { - if (currentDown === 4 && chance < 0.15) { - return "other"; + const OFFENSE_TIMEOUT_PROBABILITY = 0.15; + const DEFENSE_TIMEOUT_PROBABILITY = 0.1; + if (currentDown === 4 && teamHasPossession && yardsToGo <= 3) { + return randomChanceTimeout < 0.15 ? "other" : undefined; } return undefined; }; const iceFieldGoalTimeout = () => { + const DEFENSE_TIMEOUT_PROBABILITY = 0.8; if ( (playType === "fieldGoalLate" || "fieldGoal") && !teamHasPossession && timeRemaining < 0.5 ) { - return randomChanceTimeout < 0.8 ? "other" : undefined; + return randomChanceTimeout < DEFENSE_TIMEOUT_PROBABILITY + ? "other" + : undefined; + } + return undefined; + }; + + const clockManagementTimeout = () => { + // If game is a blowout, don't stop clock + if (Math.abs(diff) > 24) { + return undefined; + } + // Defense losing stops clock + if (!teamHasPossession && diff > 0) { + if (timeRemaining < 2.5) { + return "toStopClock"; + } + } + + // Offense losing/tied saves time + if (teamHasPossession && diff <= 0) { + if (timeRemaining < 1.5) { + return "toStopClock"; + } } }; - const clockManagementTimeout = () => {}; // BEFORE TWO MINUTE WARNING // Small random chance of timeout due to: // - playcall confusion @@ -1393,7 +1441,7 @@ class GameSim extends GameSimBase { const prob = teamHasPossession ? OFFENSE_TIMEOUT_PROBABILITY : DEFENSE_TIMEOUT_PROBABILITY; - + // Include scenarios here return randomChanceTimeout < prob ? "other" : undefined; } @@ -1417,22 +1465,17 @@ class GameSim extends GameSimBase { } } - // CLOCK MANAGEMENT - - if (inFinalPeriod) { - if (clockRunning) { - // Defense losing stops clock - if (!teamHasPossession && diff < 0) { - if (timeRemaining < 2.5) { - return "toStopClock"; - } + // IF we are in a close game (within 24 points, which could plausibly be overcome), timeouts are more common + if (inFinalPeriod && twoMinuteWarning) { + if (Math.abs(diff) <= 24) { + if (clockRunning) { + clockManagementTimeout(); } - - // Offense losing/tied saves time - if (teamHasPossession && diff <= 0) { - if (timeRemaining < 1.5) { - return "toStopClock"; - } + if (ONE_SCORE_GAME) { + // If a close game, we have to check these scenarios for if a team should call a timeout + iceFieldGoalTimeout(); + redZonePlayCallTimeout(); + fourthDownDecisionTimeout(); } } } From 82e56e0a79c3ac4361909243e541befc62242474 Mon Sep 17 00:00:00 2001 From: jabapo Date: Mon, 9 Mar 2026 19:46:17 -0400 Subject: [PATCH 6/7] comments --- src/worker/core/GameSim.football/index.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/worker/core/GameSim.football/index.ts b/src/worker/core/GameSim.football/index.ts index fb6cc36de5..389f83ba89 100644 --- a/src/worker/core/GameSim.football/index.ts +++ b/src/worker/core/GameSim.football/index.ts @@ -1395,10 +1395,11 @@ class GameSim extends GameSimBase { return undefined; }; + // Icing usually happens if a team has more than 1 timeout. Does not usually happen with only 1 timeout. const iceFieldGoalTimeout = () => { const DEFENSE_TIMEOUT_PROBABILITY = 0.8; if ( - (playType === "fieldGoalLate" || "fieldGoal") && + (playType === "fieldGoalLate" || playType === "fieldGoal") && !teamHasPossession && timeRemaining < 0.5 ) { @@ -1409,6 +1410,19 @@ class GameSim extends GameSimBase { return undefined; }; + // Offenses usually call a timeout when the amount of time is < 10 second, and they have the option to stop the clock, to win off of a field goal. + // This scenario we need to check IF teamHasPossession, AND timeRemaining is low, AND the playType is a field goal attempt, let time trickle down to 4 seconds, and then call a timeout. + const prepareForFieldGoalTimeout = () => { + if ( + (playType === "fieldGoalLate" && + teamHasPossession && + timeRemaining < 0.5) || + (playType === "fieldGoal" && teamHasPossession && timeRemaining < 0.1) + ) { + return randomChanceTimeout < 0.9 ? "other" : undefined; + } + }; + const clockManagementTimeout = () => { // If game is a blowout, don't stop clock if (Math.abs(diff) > 24) { From 8ab6fda20acbe914ee0f5629946ae01e5506e5e2 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 10 Mar 2026 00:05:16 -0500 Subject: [PATCH 7/7] more tweaked version --- src/worker/core/GameSim.football/index.ts | 118 ++++++++++++++-------- 1 file changed, 77 insertions(+), 41 deletions(-) diff --git a/src/worker/core/GameSim.football/index.ts b/src/worker/core/GameSim.football/index.ts index 389f83ba89..cbc0e3a93a 100644 --- a/src/worker/core/GameSim.football/index.ts +++ b/src/worker/core/GameSim.football/index.ts @@ -1358,6 +1358,9 @@ class GameSim extends GameSimBase { // Redzone playcall timeout. Different percentages based on down, defense, offense, and score. // If score is close, timeout is more likely, especially for offense. const redZonePlayCallTimeout = () => { + if (distance > RED_ZONE_DISTANCE) { + return undefined; + } const OFFENSE_TIMEOUT_PROBABILITY = 0.08; const DEFENSE_TIMEOUT_PROBABILITY = 0.05; // TODO: Make timeout probability 2x higher if game is close for offense, 1.5x for defense @@ -1386,41 +1389,82 @@ class GameSim extends GameSimBase { } return undefined; }; + + // A team on fourth down is more likely to call a timeout, especially the offense. + // Or, if they are trying to draw an offsides or something? Maybe dont have to calculate for that const fourthDownDecisionTimeout = () => { + if (currentDown !== 4) { + return undefined; + } const OFFENSE_TIMEOUT_PROBABILITY = 0.15; const DEFENSE_TIMEOUT_PROBABILITY = 0.1; - if (currentDown === 4 && teamHasPossession && yardsToGo <= 3) { - return randomChanceTimeout < 0.15 ? "other" : undefined; + if (teamHasPossession) { + if (yardsToGo <= 2) { + return randomChanceTimeout < OFFENSE_TIMEOUT_PROBABILITY + ? "other" + : undefined; + } else { + return randomChanceTimeout < OFFENSE_TIMEOUT_PROBABILITY * 1.5 + ? "other" + : undefined; + } + } else { + if (yardsToGo <= 2) { + return randomChanceTimeout < DEFENSE_TIMEOUT_PROBABILITY * 1.5 + ? "other" + : undefined; + } else { + return randomChanceTimeout < DEFENSE_TIMEOUT_PROBABILITY * 0.5 + ? "other" + : undefined; + } } - return undefined; }; // Icing usually happens if a team has more than 1 timeout. Does not usually happen with only 1 timeout. + // Do this only if teams are within 24 points const iceFieldGoalTimeout = () => { - const DEFENSE_TIMEOUT_PROBABILITY = 0.8; + const DEFENSE_TIMEOUT_PROBABILITY = 0.9; if ( (playType === "fieldGoalLate" || playType === "fieldGoal") && !teamHasPossession && timeRemaining < 0.5 ) { - return randomChanceTimeout < DEFENSE_TIMEOUT_PROBABILITY - ? "other" - : undefined; + if (this.timeouts[t] <= 1) { + return randomChanceTimeout < DEFENSE_TIMEOUT_PROBABILITY * 0.05 + ? "other" + : undefined; + } else { + return randomChanceTimeout < DEFENSE_TIMEOUT_PROBABILITY + ? "other" + : undefined; + } } return undefined; }; // Offenses usually call a timeout when the amount of time is < 10 second, and they have the option to stop the clock, to win off of a field goal. // This scenario we need to check IF teamHasPossession, AND timeRemaining is low, AND the playType is a field goal attempt, let time trickle down to 4 seconds, and then call a timeout. + // Or actually, we shouldn't be looking if playType is field goal + // Do this in 2nd quarter and 4th quarter, no need to do it otherwise const prepareForFieldGoalTimeout = () => { + if (!inPeriodBeforeHalftime && !inFinalPeriod) { + return undefined; + } + if (!teamHasPossession) { + return undefined; + } + const OFFENSE_TIMEOUT_PROBABILITY = 0.9; if ( - (playType === "fieldGoalLate" && - teamHasPossession && - timeRemaining < 0.5) || - (playType === "fieldGoal" && teamHasPossession && timeRemaining < 0.1) + (playType === "fieldGoalLate" || playType === "fieldGoal") && + teamHasPossession && + timeRemaining < 0.5 ) { - return randomChanceTimeout < 0.9 ? "other" : undefined; + return randomChanceTimeout < OFFENSE_TIMEOUT_PROBABILITY + ? "other" + : undefined; } + return undefined; }; const clockManagementTimeout = () => { @@ -1447,7 +1491,7 @@ class GameSim extends GameSimBase { // - playcall confusion // - substitution issues // - avoiding delay of game - if (!twoMinuteWarning && !ONE_SCORE_GAME) { + if (!inFinalPeriod && !inPeriodBeforeHalftime) { const OFFENSE_TIMEOUT_PROBABILITY = 0.01; // - Include ONE_SCORE_GAME variable here? Otherwise we cannot reach the other flows. const DEFENSE_TIMEOUT_PROBABILITY = 0.003; @@ -1456,37 +1500,33 @@ class GameSim extends GameSimBase { ? OFFENSE_TIMEOUT_PROBABILITY : DEFENSE_TIMEOUT_PROBABILITY; // Include scenarios here + redZonePlayCallTimeout(); + fourthDownDecisionTimeout(); return randomChanceTimeout < prob ? "other" : undefined; - } - - // RED ZONE PLAYCALL TIMEOUT - // Offense may want correct playcall near endzone - if (teamHasPossession && distance <= RED_ZONE_DISTANCE && ONE_SCORE_GAME) { - redZonePlayCallTimeout(randomChanceTimeout); - } - - // 4TH DOWN DECISION TIMEOUT - if (teamHasPossession && currentDown === 4) { - if (randomChanceTimeout < 0.15) { - return "other"; - } - } - - // GOAL LINE DEFENSIVE TIMEOUT - if (!teamHasPossession && distance <= 5 && ONE_SCORE_GAME) { - if (randomChanceTimeout < 0.07) { - return "other"; + } else { + // IF we are in a close game (within 24 points, which could plausibly be overcome), timeouts are more common + if (inFinalPeriod && twoMinuteWarning) { + if (Math.abs(diff) <= 24) { + if (clockRunning) { + clockManagementTimeout(); + } + if (ONE_SCORE_GAME) { + // If a close game, we have to check these scenarios for if a team should call a timeout + iceFieldGoalTimeout(); + redZonePlayCallTimeout(); + prepareForFieldGoalTimeout(); + fourthDownDecisionTimeout(); + } + } } - } - - // IF we are in a close game (within 24 points, which could plausibly be overcome), timeouts are more common - if (inFinalPeriod && twoMinuteWarning) { - if (Math.abs(diff) <= 24) { + if (inPeriodBeforeHalftime) { if (clockRunning) { clockManagementTimeout(); + prepareForFieldGoalTimeout(); } if (ONE_SCORE_GAME) { // If a close game, we have to check these scenarios for if a team should call a timeout + prepareForFieldGoalTimeout(); iceFieldGoalTimeout(); redZonePlayCallTimeout(); fourthDownDecisionTimeout(); @@ -1494,10 +1534,6 @@ class GameSim extends GameSimBase { } } - // END OF HALF FIELD GOAL ICING - - if (inPeriodBeforeHalftime) { - } return finalDecision; } doTimeout(t: TeamNum, toStopClock: boolean) {