Skip to content

Commit

Permalink
Start working on football shootout
Browse files Browse the repository at this point in the history
  • Loading branch information
dumbmatter committed Apr 12, 2024
1 parent 360d013 commit bf1a0c9
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 4 deletions.
20 changes: 19 additions & 1 deletion src/ui/util/processLiveGameEvents.football.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,14 @@ export const getText = (event: PlayByPlayEvent, numPeriods: number) => {
text = "Two-point conversion failed";
} else if (event.type === "turnoverOnDowns") {
text = <span className="text-danger">Turnover on downs</span>;
} else if (event.type === "shootoutStart") {
text = `The game will now be decided by a three-point shootout with ${event.rounds} rounds!`;
} else if (event.type === "shootoutShot") {
text = `${playersByPid![event.pid].name} ${event.made ? "made" : "missed"} a ${
event.yds
} yard field goal`;
} else if (event.type === "shootoutTie") {
text = `The shootout is tied! Teams will alternate kicks until there is a winner`;
} else {
throw new Error(`No text for "${event.type}"`);
}
Expand Down Expand Up @@ -527,6 +535,12 @@ const processLiveGameEvents = ({
if ((e as any).clock !== undefined) {
boxScore.time = formatClock((e as any).clock);
}
} else if (e.type === "shootoutStart") {
boxScore.shootout = true;
boxScore.teams[0].sPts = 0;
boxScore.teams[0].sAtt = 0;
boxScore.teams[1].sPts = 0;
boxScore.teams[1].sAtt = 0;
}

const addNewPlay = ({
Expand Down Expand Up @@ -721,7 +735,11 @@ const processLiveGameEvents = ({
boxScore.time = formatClock(e.clock);
stop = true;
t = actualT;
textOnly = e.type === "twoMinuteWarning" || e.type === "gameOver";
textOnly =
e.type === "twoMinuteWarning" ||
e.type === "gameOver" ||
e.type === "shootoutStart" ||
e.type === "shootoutTie";

play.texts.push(text);
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/views/Settings/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2249,7 +2249,7 @@ export const settings: Setting[] = (
basketball:
"For basketball, that means a three-point contest! This setting specifies the # of shots your best shooter will get. If the game is still tied after both teams go, then it's repeated until someone wins.",
football:
"For football, that means a field goal contest! This setting specifies the number of 50 yard field goals each team will attempt. If it's still tied after both teams go, then additional rounds will be played until there is a winner, with each round moving a little closer in case both kickers are injured or something crazy like that.",
"For football, that means a field goal contest! This setting specifies the number of 50 yard field goals each team will attempt. If it's still tied after that, then additional rounds will be played until there is a winner.",
hockey:
"For hockey, that means a penalty shootout. This setting specifies the number of players from each team who will take turns attempting penalty shots. If it's still tied after that, then additional rounds will be played until there is a winner.",
})}
Expand Down
17 changes: 17 additions & 0 deletions src/worker/core/GameSim.football/PlayByPlayLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,23 @@ type PlayByPlayEventInput =
type: "turnoverOnDowns";
clock: number;
t: TeamNum;
}
| {
type: "shootoutStart";
rounds: number;
clock: number;
}
| {
type: "shootoutShot";
t: TeamNum;
pid: number;
made: boolean;
yds: number;
clock: number;
}
| {
type: "shootoutTie";
clock: number;
};

export type PlayByPlayEvent =
Expand Down
101 changes: 99 additions & 2 deletions src/worker/core/GameSim.football/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { PHASE } from "../../../common";

const teamNums: [TeamNum, TeamNum] = [0, 1];

const FIELD_GOAL_DISTANCE_YARDS_ADDED_FROM_SCRIMMAGE = 17;

/**
* Convert energy into fatigue, which can be multiplied by a rating to get a fatigue-adjusted value.
*
Expand Down Expand Up @@ -202,6 +204,8 @@ class GameSim extends GameSimBase {
numOvertimes += 1;
}

this.doShootout();

this.playByPlay.logEvent({
type: "gameOver",
clock: this.clock,
Expand Down Expand Up @@ -259,6 +263,97 @@ class GameSim extends GameSimBase {
return out;
}

doShootoutShot(t: TeamNum) {
this.o = t;
this.d = t === 0 ? 1 : 0;

this.updatePlayersOnField("fieldGoal");

const distance = 50;

const p = this.getTopPlayerOnField(this.o, "K");
this.scrimmage = distance + FIELD_GOAL_DISTANCE_YARDS_ADDED_FROM_SCRIMMAGE;

// Don't let it ever be 0% or 100%
const probMake = helpers.bound(this.probMadeFieldGoal(p), 0.01, 0.99);

const made = Math.random() < probMake;

this.recordStat(t, undefined, "sAtt");
if (made) {
this.recordStat(t, undefined, "sPts");
}

this.playByPlay.logEvent({
type: "shootoutShot",
t: t,
pid: p.id,
made,
yds: distance,
clock: this.clock,
});
}

doShootout() {
if (
this.shootoutRounds <= 0 ||
this.team[0].stat.pts !== this.team[1].stat.pts
) {
return;
}

this.shootout = true;
this.clock = 1; // So fast-forward to end of period stops before the shootout
this.team[0].stat.sPts = 0;
this.team[0].stat.sAtt = 0;
this.team[1].stat.sPts = 0;
this.team[1].stat.sAtt = 0;

this.playByPlay.logEvent({
type: "shootoutStart",
rounds: this.shootoutRounds,
clock: this.clock,
});

const reversedTeamNums = [1, 0] as const;

for (let i = 0; i < this.shootoutRounds; i++) {
for (const t of reversedTeamNums) {
this.doShootoutShot(t);

// Short circuit if result is already decided
const t2 = t === 0 ? 1 : 0;
const minPts = this.team[t].stat.sPts;
const maxPts = minPts + this.shootoutRounds - i - 1;
const minPtsOther = this.team[t2].stat.sPts;
const maxPtsOther =
minPtsOther + this.shootoutRounds - i - (t === 0 ? 1 : 0);
console.log(i, t, minPts, maxPts, minPtsOther, maxPtsOther);
if (minPts > maxPtsOther) {
// Already clinched a win even without the remaining shots
break;
}
if (maxPts < minPtsOther) {
// Can't possibly win, so just give up
break;
}
}
}

if (this.team[0].stat.sPts === this.team[1].stat.sPts) {
this.playByPlay.logEvent({
type: "shootoutTie",
clock: this.clock,
});

while (this.team[0].stat.sPts === this.team[1].stat.sPts) {
for (const t of reversedTeamNums) {
this.doShootoutShot(t);
}
}
}
}

isFirstPeriodAfterHalftime(quarter: number) {
return this.numPeriods % 2 === 0 && quarter === this.numPeriods / 2 + 1;
}
Expand Down Expand Up @@ -1333,7 +1428,8 @@ class GameSim extends GameSimBase {
? kickerInput
: this.team[this.o].depth.K.find(p => !p.injured);
let baseProb = 0;
let distance = 100 - this.scrimmage + 17;
let distance =
100 - this.scrimmage + FIELD_GOAL_DISTANCE_YARDS_ADDED_FROM_SCRIMMAGE;

if (!kicker) {
// Would take an absurd amount of injuries to get here, but technically possible
Expand Down Expand Up @@ -1450,7 +1546,8 @@ class GameSim extends GameSimBase {
}
}

const distance = 100 - this.scrimmage + 17;
const distance =
100 - this.scrimmage + FIELD_GOAL_DISTANCE_YARDS_ADDED_FROM_SCRIMMAGE;
const kicker = this.getTopPlayerOnField(this.o, "K");
const made = Math.random() < this.probMadeFieldGoal(kicker);
const dt = extraPoint ? 0 : random.randInt(4, 6);
Expand Down

0 comments on commit bf1a0c9

Please sign in to comment.