Skip to content

Commit bf1a0c9

Browse files
committed
Start working on football shootout
1 parent 360d013 commit bf1a0c9

File tree

4 files changed

+136
-4
lines changed

4 files changed

+136
-4
lines changed

src/ui/util/processLiveGameEvents.football.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,14 @@ export const getText = (event: PlayByPlayEvent, numPeriods: number) => {
436436
text = "Two-point conversion failed";
437437
} else if (event.type === "turnoverOnDowns") {
438438
text = <span className="text-danger">Turnover on downs</span>;
439+
} else if (event.type === "shootoutStart") {
440+
text = `The game will now be decided by a three-point shootout with ${event.rounds} rounds!`;
441+
} else if (event.type === "shootoutShot") {
442+
text = `${playersByPid![event.pid].name} ${event.made ? "made" : "missed"} a ${
443+
event.yds
444+
} yard field goal`;
445+
} else if (event.type === "shootoutTie") {
446+
text = `The shootout is tied! Teams will alternate kicks until there is a winner`;
439447
} else {
440448
throw new Error(`No text for "${event.type}"`);
441449
}
@@ -527,6 +535,12 @@ const processLiveGameEvents = ({
527535
if ((e as any).clock !== undefined) {
528536
boxScore.time = formatClock((e as any).clock);
529537
}
538+
} else if (e.type === "shootoutStart") {
539+
boxScore.shootout = true;
540+
boxScore.teams[0].sPts = 0;
541+
boxScore.teams[0].sAtt = 0;
542+
boxScore.teams[1].sPts = 0;
543+
boxScore.teams[1].sAtt = 0;
530544
}
531545

532546
const addNewPlay = ({
@@ -721,7 +735,11 @@ const processLiveGameEvents = ({
721735
boxScore.time = formatClock(e.clock);
722736
stop = true;
723737
t = actualT;
724-
textOnly = e.type === "twoMinuteWarning" || e.type === "gameOver";
738+
textOnly =
739+
e.type === "twoMinuteWarning" ||
740+
e.type === "gameOver" ||
741+
e.type === "shootoutStart" ||
742+
e.type === "shootoutTie";
725743

726744
play.texts.push(text);
727745
}

src/ui/views/Settings/settings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2249,7 +2249,7 @@ export const settings: Setting[] = (
22492249
basketball:
22502250
"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.",
22512251
football:
2252-
"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.",
2252+
"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.",
22532253
hockey:
22542254
"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.",
22552255
})}

src/worker/core/GameSim.football/PlayByPlayLogger.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,23 @@ type PlayByPlayEventInput =
231231
type: "turnoverOnDowns";
232232
clock: number;
233233
t: TeamNum;
234+
}
235+
| {
236+
type: "shootoutStart";
237+
rounds: number;
238+
clock: number;
239+
}
240+
| {
241+
type: "shootoutShot";
242+
t: TeamNum;
243+
pid: number;
244+
made: boolean;
245+
yds: number;
246+
clock: number;
247+
}
248+
| {
249+
type: "shootoutTie";
250+
clock: number;
234251
};
235252

236253
export type PlayByPlayEvent =

src/worker/core/GameSim.football/index.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { PHASE } from "../../../common";
2929

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

32+
const FIELD_GOAL_DISTANCE_YARDS_ADDED_FROM_SCRIMMAGE = 17;
33+
3234
/**
3335
* Convert energy into fatigue, which can be multiplied by a rating to get a fatigue-adjusted value.
3436
*
@@ -202,6 +204,8 @@ class GameSim extends GameSimBase {
202204
numOvertimes += 1;
203205
}
204206

207+
this.doShootout();
208+
205209
this.playByPlay.logEvent({
206210
type: "gameOver",
207211
clock: this.clock,
@@ -259,6 +263,97 @@ class GameSim extends GameSimBase {
259263
return out;
260264
}
261265

266+
doShootoutShot(t: TeamNum) {
267+
this.o = t;
268+
this.d = t === 0 ? 1 : 0;
269+
270+
this.updatePlayersOnField("fieldGoal");
271+
272+
const distance = 50;
273+
274+
const p = this.getTopPlayerOnField(this.o, "K");
275+
this.scrimmage = distance + FIELD_GOAL_DISTANCE_YARDS_ADDED_FROM_SCRIMMAGE;
276+
277+
// Don't let it ever be 0% or 100%
278+
const probMake = helpers.bound(this.probMadeFieldGoal(p), 0.01, 0.99);
279+
280+
const made = Math.random() < probMake;
281+
282+
this.recordStat(t, undefined, "sAtt");
283+
if (made) {
284+
this.recordStat(t, undefined, "sPts");
285+
}
286+
287+
this.playByPlay.logEvent({
288+
type: "shootoutShot",
289+
t: t,
290+
pid: p.id,
291+
made,
292+
yds: distance,
293+
clock: this.clock,
294+
});
295+
}
296+
297+
doShootout() {
298+
if (
299+
this.shootoutRounds <= 0 ||
300+
this.team[0].stat.pts !== this.team[1].stat.pts
301+
) {
302+
return;
303+
}
304+
305+
this.shootout = true;
306+
this.clock = 1; // So fast-forward to end of period stops before the shootout
307+
this.team[0].stat.sPts = 0;
308+
this.team[0].stat.sAtt = 0;
309+
this.team[1].stat.sPts = 0;
310+
this.team[1].stat.sAtt = 0;
311+
312+
this.playByPlay.logEvent({
313+
type: "shootoutStart",
314+
rounds: this.shootoutRounds,
315+
clock: this.clock,
316+
});
317+
318+
const reversedTeamNums = [1, 0] as const;
319+
320+
for (let i = 0; i < this.shootoutRounds; i++) {
321+
for (const t of reversedTeamNums) {
322+
this.doShootoutShot(t);
323+
324+
// Short circuit if result is already decided
325+
const t2 = t === 0 ? 1 : 0;
326+
const minPts = this.team[t].stat.sPts;
327+
const maxPts = minPts + this.shootoutRounds - i - 1;
328+
const minPtsOther = this.team[t2].stat.sPts;
329+
const maxPtsOther =
330+
minPtsOther + this.shootoutRounds - i - (t === 0 ? 1 : 0);
331+
console.log(i, t, minPts, maxPts, minPtsOther, maxPtsOther);
332+
if (minPts > maxPtsOther) {
333+
// Already clinched a win even without the remaining shots
334+
break;
335+
}
336+
if (maxPts < minPtsOther) {
337+
// Can't possibly win, so just give up
338+
break;
339+
}
340+
}
341+
}
342+
343+
if (this.team[0].stat.sPts === this.team[1].stat.sPts) {
344+
this.playByPlay.logEvent({
345+
type: "shootoutTie",
346+
clock: this.clock,
347+
});
348+
349+
while (this.team[0].stat.sPts === this.team[1].stat.sPts) {
350+
for (const t of reversedTeamNums) {
351+
this.doShootoutShot(t);
352+
}
353+
}
354+
}
355+
}
356+
262357
isFirstPeriodAfterHalftime(quarter: number) {
263358
return this.numPeriods % 2 === 0 && quarter === this.numPeriods / 2 + 1;
264359
}
@@ -1333,7 +1428,8 @@ class GameSim extends GameSimBase {
13331428
? kickerInput
13341429
: this.team[this.o].depth.K.find(p => !p.injured);
13351430
let baseProb = 0;
1336-
let distance = 100 - this.scrimmage + 17;
1431+
let distance =
1432+
100 - this.scrimmage + FIELD_GOAL_DISTANCE_YARDS_ADDED_FROM_SCRIMMAGE;
13371433

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

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

0 commit comments

Comments
 (0)