@johnmorrisdotca/kyogijo-stadiums 0.61.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +4 -3
- package/catalogue.json +1 -1
- package/dist/catalogue.data.js +1 -1
- package/dist/feeds-entry.d.ts +7 -0
- package/dist/feeds-entry.js +6 -0
- package/dist/feeds.core.d.ts +55 -0
- package/dist/feeds.core.js +21 -0
- package/dist/feeds.d.ts +26 -0
- package/dist/feeds.dom.d.ts +1 -0
- package/dist/feeds.dom.js +34 -0
- package/dist/feeds.fetch.d.ts +62 -0
- package/dist/feeds.fetch.js +177 -0
- package/dist/feeds.js +211 -0
- package/dist/feeds.nhl.d.ts +28 -0
- package/dist/feeds.nhl.js +132 -0
- package/dist/judging.mount.js +1 -1
- package/dist/mount-entry.d.ts +1 -0
- package/dist/mount.d.ts +4 -3
- package/dist/mount.js +3 -2
- package/dist/replay-entry.d.ts +7 -3
- package/dist/replay-entry.js +3 -1
- package/dist/replay.board.d.ts +75 -4
- package/dist/replay.board.js +113 -13
- package/dist/replay.d.ts +1 -1
- package/dist/replay.deadtime.d.ts +58 -0
- package/dist/replay.deadtime.js +169 -0
- package/dist/replay.feed.d.ts +54 -0
- package/dist/replay.feed.js +17 -0
- package/dist/replay.hockey.d.ts +143 -0
- package/dist/replay.hockey.js +276 -0
- package/dist/replay.js +12 -6
- package/dist/replay.types.d.ts +41 -0
- package/dist/strings.core.d.ts +1 -0
- package/dist/strings.en.js +1 -1
- package/dist/strings.ja.js +1 -1
- package/dist/turn-entry.d.ts +1 -0
- package/dist/turn.dom.d.ts +3 -1
- package/dist/turn.dom.js +5 -4
- package/dist/turn.replay.board.d.ts +1 -1
- package/dist/turn.replay.board.js +20 -3
- package/dist/venue.boards.d.ts +6 -0
- package/dist/venue.boards.js +20 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -1
package/dist/replay.board.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// What a scoreboard shows of a replayed game at a time: the score, the period and the game clock, worked out from the same timeline the replay plays (its goals and its periods). Pure: a timeline and a time
|
|
2
2
|
// give the same board every time, so a seek, a step and a period's button all land on the right figures at once, and a replay that goes back finds the score as it was. The 3D view draws it on the scoreboard
|
|
3
3
|
// hung over the surface (`turn.replay.ts`); a page that draws its own board reads it the same way.
|
|
4
|
+
import { deadSeconds, stoppageAt } from "./replay.deadtime.js";
|
|
4
5
|
import { replayEvents, replayInfo } from "./replay.js";
|
|
5
6
|
const isNum = (value) => typeof value === "number" && Number.isFinite(value);
|
|
6
7
|
const two = (n) => String(n).padStart(2, "0");
|
|
@@ -19,9 +20,106 @@ export function replayPeriods(timeline) {
|
|
|
19
20
|
return [...given].sort((a, b) => a.start - b.start).map((p, i) => ({ ...p, number: isNum(p.number) ? p.number : i + 1 }));
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* The game clock at a time `t` on the replay's clock: the period it is in (the last that has begun; the first, before any has) and how much of it has been played. The game clock runs only while play is
|
|
24
|
+
* on: it stands still in each of the timeline's `stoppages`, while the replay's clock moves on through it. A period's `length` is spread over the seconds of it that are not in a stoppage, so a game
|
|
25
|
+
* re-timed to minutes reads like the real one. Pure.
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { replayGameClock } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
29
|
+
* const timeline = {
|
|
30
|
+
* frames: [{ t: 0, objects: [] }, { t: 70, objects: [] }],
|
|
31
|
+
* periods: [{ start: 0, end: 70, length: 1200 }],
|
|
32
|
+
* stoppages: [{ start: 30, end: 40, reason: "goal" }],
|
|
33
|
+
* };
|
|
34
|
+
* const at = (t: number) => { const c = replayGameClock(timeline, t); return `${c.played} ${c.running}`; };
|
|
35
|
+
* console.log(at(15), at(30), at(35), at(40), at(55));
|
|
36
|
+
* // 300 true 600 false 600 false 600 true 900 true
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function replayGameClock(timeline, t) {
|
|
40
|
+
const periods = replayPeriods(timeline);
|
|
41
|
+
let at = periods[0];
|
|
42
|
+
for (const p of periods)
|
|
43
|
+
if (p.start <= t)
|
|
44
|
+
at = p;
|
|
45
|
+
const live = at.end - at.start - deadSeconds(timeline, at.start, at.end);
|
|
46
|
+
const into = Math.min(at.end, Math.max(at.start, t));
|
|
47
|
+
const gone = into - at.start - deadSeconds(timeline, at.start, into);
|
|
48
|
+
const share = live > 0 ? Math.min(1, Math.max(0, gone / live)) : 0;
|
|
49
|
+
const length = isNum(at.length) && at.length > 0 ? at.length : live;
|
|
50
|
+
const stoppage = stoppageAt(timeline, t);
|
|
51
|
+
const played = share * length;
|
|
52
|
+
const running = stoppage === undefined && t >= at.start && t < at.end;
|
|
53
|
+
return { ...(at.number === undefined ? {} : { period: at.number }), played, left: length - played, running, ...(stoppage === undefined ? {} : { stoppage }) };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The game time at `t` on the replay's clock: the seconds of game clock played since the first period began (the lengths of the periods before, and what has been played of this one). It stands still in a
|
|
57
|
+
* stoppage, as the game clock does: a penalty's two minutes are two minutes of it.
|
|
58
|
+
*/
|
|
59
|
+
export function replayGameTime(timeline, t) {
|
|
60
|
+
const periods = replayPeriods(timeline);
|
|
61
|
+
let before = 0;
|
|
62
|
+
for (const p of periods) {
|
|
63
|
+
if (p.end > t)
|
|
64
|
+
break;
|
|
65
|
+
const next = periods[periods.indexOf(p) + 1];
|
|
66
|
+
if (next === undefined || next.start > t)
|
|
67
|
+
break;
|
|
68
|
+
before += isNum(p.length) && p.length > 0 ? p.length : p.end - p.start - deadSeconds(timeline, p.start, p.end);
|
|
69
|
+
}
|
|
70
|
+
return before + replayGameClock(timeline, t).played;
|
|
71
|
+
}
|
|
72
|
+
/** The penalties of a timeline, each checked: a side, a start and minutes that are numbers. Kept for the next call. */
|
|
73
|
+
const PENALTIES = new WeakMap();
|
|
74
|
+
export function replayPenalties(timeline) {
|
|
75
|
+
const hit = PENALTIES.get(timeline);
|
|
76
|
+
if (hit !== undefined)
|
|
77
|
+
return hit;
|
|
78
|
+
const given = Array.isArray(timeline.penalties) ? timeline.penalties : [];
|
|
79
|
+
const out = given.filter((p) => typeof p === "object" && p !== null && (p.team === "home" || p.team === "away") && isNum(p.start) && isNum(p.minutes) && p.minutes > 0 && (p.end === undefined || (isNum(p.end) && p.end >= p.start))).map((p) => ({ ...p }));
|
|
80
|
+
PENALTIES.set(timeline, out);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The penalties being served at `t` (the player is in the box: from the call to his `end`) with the game seconds left of each, and each side's skaters on the ice: five, less one for each penalty that
|
|
85
|
+
* makes it play short, never fewer than three. A penalty's time left is the game clock from now to its `end` (it runs only while the game clock does), or, with no end or one a power-play goal
|
|
86
|
+
* brought early (`endedBy: "goal"`), its minutes from the drop after the call. Pure.
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { penaltiesAt } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
90
|
+
* const timeline = {
|
|
91
|
+
* frames: [{ t: 0, objects: [] }, { t: 60, objects: [] }],
|
|
92
|
+
* periods: [{ start: 0, end: 60, length: 1200 }],
|
|
93
|
+
* penalties: [{ team: "away" as const, by: "a4", start: 10, end: 16, minutes: 2 }],
|
|
94
|
+
* };
|
|
95
|
+
* const at = penaltiesAt(timeline, 12);
|
|
96
|
+
* console.log(at.strength, at.served.map((s) => `${s.penalty.by} ${s.left}`), penaltiesAt(timeline, 16).strength);
|
|
97
|
+
* // [ 5, 4 ] [ 'a4 80' ] [ 5, 5 ]
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function penaltiesAt(timeline, t) {
|
|
101
|
+
const served = [];
|
|
102
|
+
const short = { home: 0, away: 0 };
|
|
103
|
+
const now = replayGameTime(timeline, t);
|
|
104
|
+
for (const p of replayPenalties(timeline)) {
|
|
105
|
+
if (p.start > t || (p.end !== undefined && p.end <= t))
|
|
106
|
+
continue;
|
|
107
|
+
const full = p.minutes * 60;
|
|
108
|
+
// Its time left: to its end on the game clock (a penalty that waited for a side two short begins its count later), or, with no end or one a power-play goal brought early, its minutes from the call.
|
|
109
|
+
const left = p.end !== undefined && p.endedBy !== "goal" ? Math.min(full, replayGameTime(timeline, p.end) - now) : full - (now - replayGameTime(timeline, p.start));
|
|
110
|
+
if (left <= 0 && p.end === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
served.push({ penalty: p, left: Math.max(0, left) });
|
|
113
|
+
if (p.short !== false)
|
|
114
|
+
short[p.team] += 1;
|
|
115
|
+
}
|
|
116
|
+
return { served, strength: [Math.max(3, 5 - short.home), Math.max(3, 5 - short.away)] };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The scoreboard of a replay at a time `t`, in seconds on the timeline's clock. The score is the goals at or before it; the period and the clock are the game clock's (`replayGameClock`): the clock runs
|
|
120
|
+
* through the period from its full length to nothing, or from nothing up when the timeline counts `up`, and stands still in a stoppage. A period with a `length` of its own runs its clock through that many
|
|
121
|
+
* seconds however long it lasts on the timeline, so a game re-timed to minutes can read like the real one. A timeline with `penalties` adds each side's `strength` and the penalties being served,
|
|
122
|
+
* with their time left (`penaltiesAt`).
|
|
25
123
|
* @example
|
|
26
124
|
* ```ts
|
|
27
125
|
* import { replayBoard } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
@@ -48,15 +146,17 @@ export function replayBoard(timeline, t) {
|
|
|
48
146
|
if (e.kind === "goal" && (e.team === "home" || e.team === "away"))
|
|
49
147
|
score[e.team === "home" ? 0 : 1] += 1;
|
|
50
148
|
}
|
|
51
|
-
const
|
|
52
|
-
let at = periods[0];
|
|
53
|
-
for (const p of periods)
|
|
54
|
-
if (p.start <= t)
|
|
55
|
-
at = p;
|
|
56
|
-
const span = at.end - at.start;
|
|
57
|
-
const share = span > 0 ? Math.min(1, Math.max(0, (t - at.start) / span)) : 0;
|
|
58
|
-
const length = isNum(at.length) && at.length > 0 ? at.length : span;
|
|
149
|
+
const game = replayGameClock(timeline, t);
|
|
59
150
|
// Down: the time left, rounded up, so that the full length shows until the period has moved and nothing shows only at its end. Up: the whole seconds gone.
|
|
60
|
-
const clock = timeline.clock === "up" ? boardClock(
|
|
61
|
-
|
|
151
|
+
const clock = timeline.clock === "up" ? boardClock(game.played) : boardClock(Math.ceil(game.left - 1e-9));
|
|
152
|
+
const board = game.period === undefined ? { score, clock } : { score, period: game.period, clock };
|
|
153
|
+
if (replayPenalties(timeline).length > 0) {
|
|
154
|
+
const { served, strength } = penaltiesAt(timeline, t);
|
|
155
|
+
board.strength = strength;
|
|
156
|
+
board.penalties = served
|
|
157
|
+
.map((s) => ({ team: s.penalty.team, left: boardClock(Math.ceil(s.left - 1e-9)), short: s.penalty.short !== false, seconds: s.left }))
|
|
158
|
+
.sort((a, b) => Number(b.short) - Number(a.short) || a.seconds - b.seconds)
|
|
159
|
+
.map(({ team, left, short }) => ({ team, left, short }));
|
|
160
|
+
}
|
|
161
|
+
return board;
|
|
62
162
|
}
|
package/dist/replay.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare function interpolate(timeline: ReplayTimeline, t: number, options
|
|
|
44
44
|
}): ReplayObject[];
|
|
45
45
|
/**
|
|
46
46
|
* The path each object took over the last `seconds` before `t`, oldest place first and ending at its place at `t`: the frames in between, and the places at the two ends.
|
|
47
|
-
* Only objects that are there at `t` have a trail. `teams` and `ids` choose whose; all by default.
|
|
47
|
+
* Only objects that are there at `t` have a trail, and it begins after the last frame the object was not in (a dead puck out of sight). `teams` and `ids` choose whose; all by default.
|
|
48
48
|
* @example
|
|
49
49
|
* ```ts
|
|
50
50
|
* import { trails } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReplayObject, ReplayStoppage, ReplayTimeline } from "./replay.types.ts";
|
|
2
|
+
type P = readonly [number, number];
|
|
3
|
+
/**
|
|
4
|
+
* The stoppages of a timeline, in order, each checked: a `start` and an `end` that are numbers, the end not before the start. Ones that overlap are joined. Read once and kept.
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { replayStoppages } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
8
|
+
* const frames = [{ t: 0, objects: [] }];
|
|
9
|
+
* console.log(replayStoppages({ frames, stoppages: [{ start: 9, end: 12 }, { start: 2, end: 5, reason: "goal" }, { start: 4, end: 6 }, { start: 8, end: 7 }] }));
|
|
10
|
+
* // [ { start: 2, end: 6, reason: 'goal' }, { start: 9, end: 12 } ]
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function replayStoppages(timeline: ReplayTimeline): readonly ReplayStoppage[];
|
|
14
|
+
/** The stoppage a time is in (from its start, up to but not at its end, when play starts again), or `undefined` while play is on. */
|
|
15
|
+
export declare function stoppageAt(timeline: ReplayTimeline, t: number): ReplayStoppage | undefined;
|
|
16
|
+
/** The seconds of a span of the timeline's clock that were dead: the parts of it in stoppages. */
|
|
17
|
+
export declare function deadSeconds(timeline: ReplayTimeline, from: number, to: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* The restarts that went by while the clock moved from `from` to `to` (`(from, to]`): the stoppages whose end, the drop of the puck, is in it. What a page plays the drop's sound on, as it plays the
|
|
20
|
+
* horn on a goal of `eventsBetween`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function restartsBetween(timeline: ReplayTimeline, from: number, to: number): readonly ReplayStoppage[];
|
|
23
|
+
/** How a sport's dead time goes: the speed the players go at and how long each part lasts, by the reason for the stoppage. */
|
|
24
|
+
export interface DeadTimeRules {
|
|
25
|
+
/** The fastest a player goes to his place, in the timeline's units a second. */
|
|
26
|
+
speed: number;
|
|
27
|
+
/** Seconds after play stops before anyone heads for the restart: a goal is celebrated, a whistle is a moment. */
|
|
28
|
+
pause(reason: string): number;
|
|
29
|
+
/** Seconds after play stops that the dead ball or puck is still seen where it stopped; then it is out of sight until the restart. */
|
|
30
|
+
shown(reason: string): number;
|
|
31
|
+
/** The shortest time the players take to get to their places, and how long they stand in them before the restart. */
|
|
32
|
+
least: number;
|
|
33
|
+
hold: number;
|
|
34
|
+
/** Where a player who comes off goes, and one who comes on comes from (the bench's gate), or `undefined` for none: then they only go and come at the restart. */
|
|
35
|
+
bench?(object: ReplayObject): P | undefined;
|
|
36
|
+
/** Is it the ball or the puck: dead in the stoppage, out of sight until the restart. */
|
|
37
|
+
dead(object: ReplayObject): boolean;
|
|
38
|
+
}
|
|
39
|
+
/** A stoppage to write into a timeline: play stops at `t`; the timeline's frames from `until` on (the players in their places, the puck on the dot) are the restart. */
|
|
40
|
+
export interface DeadStop {
|
|
41
|
+
t: number;
|
|
42
|
+
/** The time on the timeline given of the restart's frame: `t` and `until` are moved apart by the dead time, what was between them is left out, and the instant between them becomes part of the stoppage. 0.04 seconds after `t` by default. */
|
|
43
|
+
until?: number;
|
|
44
|
+
reason: string;
|
|
45
|
+
/** Where play starts again. */
|
|
46
|
+
x?: number;
|
|
47
|
+
y?: number;
|
|
48
|
+
team?: "home" | "away";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Write stoppages into a timeline. The timeline given has each stoppage as an instant: the play up to `t`, then at `until` the restart (the players in their places, the puck on the dot). Each is
|
|
52
|
+
* opened into the dead time the rules give: the puck stays where it stopped for a moment and is then out of sight, the players stand a moment (a goal is celebrated) and then go to their places at the
|
|
53
|
+
* rules' speed or slower (all getting there together, the farthest at the rules' speed), a player going off goes to the bench and the one coming on comes from it, and they stand in their places a
|
|
54
|
+
* moment before the restart. Everything after is moved on by the dead time; the periods with it (each keeps its length on the game clock), the events, the stoppages already there and the
|
|
55
|
+
* penalties. Pure.
|
|
56
|
+
*/
|
|
57
|
+
export declare function addDeadTime(timeline: ReplayTimeline, stops: readonly DeadStop[], rules: DeadTimeRules): ReplayTimeline;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Dead time: the stoppages of a replay. A stoppage stops the game clock (the board's) while the replay's own clock moves on; in it the ball or the puck is dead (it stays where play stopped,
|
|
2
|
+
// then is out of sight until the restart) and the players go to their places for the restart at a speed a player would. The functions here are pure and know no sport: a sport's rule set
|
|
3
|
+
// (`replay.hockey.ts` for the rink) says where play starts again, how long each part of the dead time lasts and how fast the players go, and `addDeadTime` writes the stoppages into a timeline.
|
|
4
|
+
import { interpolate } from "./replay.js";
|
|
5
|
+
const isNum = (value) => typeof value === "number" && Number.isFinite(value);
|
|
6
|
+
const CLEAN = new WeakMap();
|
|
7
|
+
/**
|
|
8
|
+
* The stoppages of a timeline, in order, each checked: a `start` and an `end` that are numbers, the end not before the start. Ones that overlap are joined. Read once and kept.
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { replayStoppages } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
12
|
+
* const frames = [{ t: 0, objects: [] }];
|
|
13
|
+
* console.log(replayStoppages({ frames, stoppages: [{ start: 9, end: 12 }, { start: 2, end: 5, reason: "goal" }, { start: 4, end: 6 }, { start: 8, end: 7 }] }));
|
|
14
|
+
* // [ { start: 2, end: 6, reason: 'goal' }, { start: 9, end: 12 } ]
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function replayStoppages(timeline) {
|
|
18
|
+
const hit = CLEAN.get(timeline);
|
|
19
|
+
if (hit !== undefined)
|
|
20
|
+
return hit;
|
|
21
|
+
const given = Array.isArray(timeline.stoppages) ? timeline.stoppages : [];
|
|
22
|
+
const sorted = given
|
|
23
|
+
.filter((s) => typeof s === "object" && s !== null && isNum(s.start) && isNum(s.end) && s.end >= s.start)
|
|
24
|
+
.map((s) => ({ ...s }))
|
|
25
|
+
.sort((a, b) => a.start - b.start);
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const s of sorted) {
|
|
28
|
+
const last = out[out.length - 1];
|
|
29
|
+
if (last !== undefined && s.start <= last.end)
|
|
30
|
+
last.end = Math.max(last.end, s.end);
|
|
31
|
+
else
|
|
32
|
+
out.push(s);
|
|
33
|
+
}
|
|
34
|
+
CLEAN.set(timeline, out);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** The stoppage a time is in (from its start, up to but not at its end, when play starts again), or `undefined` while play is on. */
|
|
38
|
+
export function stoppageAt(timeline, t) {
|
|
39
|
+
for (const s of replayStoppages(timeline)) {
|
|
40
|
+
if (s.start > t)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (t < s.end)
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** The seconds of a span of the timeline's clock that were dead: the parts of it in stoppages. */
|
|
48
|
+
export function deadSeconds(timeline, from, to) {
|
|
49
|
+
let dead = 0;
|
|
50
|
+
for (const s of replayStoppages(timeline)) {
|
|
51
|
+
if (s.start >= to)
|
|
52
|
+
break;
|
|
53
|
+
dead += Math.max(0, Math.min(to, s.end) - Math.max(from, s.start));
|
|
54
|
+
}
|
|
55
|
+
return dead;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The restarts that went by while the clock moved from `from` to `to` (`(from, to]`): the stoppages whose end, the drop of the puck, is in it. What a page plays the drop's sound on, as it plays the
|
|
59
|
+
* horn on a goal of `eventsBetween`.
|
|
60
|
+
*/
|
|
61
|
+
export function restartsBetween(timeline, from, to) {
|
|
62
|
+
return replayStoppages(timeline).filter((s) => s.end > from && s.end <= to);
|
|
63
|
+
}
|
|
64
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
65
|
+
const round1 = (n) => Math.round(n * 10) / 10;
|
|
66
|
+
const ceil2 = (n) => Math.ceil(n * 100 - 1e-6) / 100;
|
|
67
|
+
const dist = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
68
|
+
const placeOn = (path, t) => {
|
|
69
|
+
if (t < path.from || t >= path.until)
|
|
70
|
+
return undefined;
|
|
71
|
+
let at = path.legs[0].a;
|
|
72
|
+
for (const leg of path.legs) {
|
|
73
|
+
if (t < leg.t0)
|
|
74
|
+
break;
|
|
75
|
+
if (t >= leg.t1)
|
|
76
|
+
at = leg.b;
|
|
77
|
+
else {
|
|
78
|
+
const f = leg.t1 > leg.t0 ? (t - leg.t0) / (leg.t1 - leg.t0) : 1;
|
|
79
|
+
return [leg.a[0] + (leg.b[0] - leg.a[0]) * f, leg.a[1] + (leg.b[1] - leg.a[1]) * f];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return at;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Write stoppages into a timeline. The timeline given has each stoppage as an instant: the play up to `t`, then at `until` the restart (the players in their places, the puck on the dot). Each is
|
|
86
|
+
* opened into the dead time the rules give: the puck stays where it stopped for a moment and is then out of sight, the players stand a moment (a goal is celebrated) and then go to their places at the
|
|
87
|
+
* rules' speed or slower (all getting there together, the farthest at the rules' speed), a player going off goes to the bench and the one coming on comes from it, and they stand in their places a
|
|
88
|
+
* moment before the restart. Everything after is moved on by the dead time; the periods with it (each keeps its length on the game clock), the events, the stoppages already there and the
|
|
89
|
+
* penalties. Pure.
|
|
90
|
+
*/
|
|
91
|
+
export function addDeadTime(timeline, stops, rules) {
|
|
92
|
+
// Each period's length on the game clock, before the dead time stretches it: a period that had none keeps the live seconds it had.
|
|
93
|
+
const periods = timeline.periods?.map((p) => ({ ...p, length: isNum(p.length) ? p.length : p.end - p.start - deadSeconds(timeline, p.start, p.end) }));
|
|
94
|
+
let frames = [...timeline.frames];
|
|
95
|
+
let events = [...(timeline.events ?? [])];
|
|
96
|
+
let stoppages = [...replayStoppages(timeline)];
|
|
97
|
+
// The last first, so that a stoppage does not move the ones before it.
|
|
98
|
+
const order = [...stops].filter((s) => isNum(s.t)).sort((a, b) => b.t - a.t);
|
|
99
|
+
let pers = periods;
|
|
100
|
+
let penalties = timeline.penalties?.map((p) => ({ ...p }));
|
|
101
|
+
for (const stop of order) {
|
|
102
|
+
const t0 = stop.t;
|
|
103
|
+
const u = isNum(stop.until) && stop.until > t0 ? stop.until : t0 + 0.04;
|
|
104
|
+
const view = { ...timeline, frames, events: [] };
|
|
105
|
+
const at = new Map(interpolate(view, t0).map((o) => [o.id, o]));
|
|
106
|
+
const next = new Map(interpolate(view, u).map((o) => [o.id, o]));
|
|
107
|
+
const pause = round2(Math.max(0, rules.pause(stop.reason)));
|
|
108
|
+
const shown = round2(Math.max(0, rules.shown(stop.reason)));
|
|
109
|
+
// The players: who stays on, who goes off and who comes on, and how far each goes.
|
|
110
|
+
const stay = [];
|
|
111
|
+
const off = [];
|
|
112
|
+
const on = [];
|
|
113
|
+
for (const [id, o] of at) {
|
|
114
|
+
if (rules.dead(o))
|
|
115
|
+
continue;
|
|
116
|
+
const b = next.get(id);
|
|
117
|
+
if (b !== undefined)
|
|
118
|
+
stay.push([o, [o.x, o.y], [b.x, b.y]]);
|
|
119
|
+
else
|
|
120
|
+
off.push([o, [o.x, o.y], rules.bench?.(o)]);
|
|
121
|
+
}
|
|
122
|
+
for (const [id, o] of next)
|
|
123
|
+
if (!rules.dead(o) && !at.has(id))
|
|
124
|
+
on.push([o, rules.bench?.(o), [o.x, o.y]]);
|
|
125
|
+
const leave = Math.max(0, ...off.map(([, a, b]) => (b === undefined ? 0 : dist(a, b) / rules.speed)));
|
|
126
|
+
const swap = off.length > 0 || on.length > 0 ? ceil2(pause + leave) : pause;
|
|
127
|
+
const come = Math.max(0, ...on.map(([, a, b]) => (a === undefined ? 0 : dist(a, b) / rules.speed)));
|
|
128
|
+
const skate = Math.max(...stay.map(([, a, b]) => dist(a, b) / rules.speed), 0);
|
|
129
|
+
const arrive = ceil2(Math.max(pause + Math.max(rules.least, skate), on.length > 0 ? round2(swap + 0.04) + come : 0));
|
|
130
|
+
const total = round2(Math.max(arrive + rules.hold, shown + 1));
|
|
131
|
+
// The paths.
|
|
132
|
+
const paths = [];
|
|
133
|
+
for (const [object, a, b] of stay)
|
|
134
|
+
paths.push({ object, from: 0, until: Infinity, legs: [{ t0: 0, t1: pause, a, b: a }, { t0: pause, t1: arrive, a, b }] });
|
|
135
|
+
for (const [object, a, b] of off)
|
|
136
|
+
paths.push({ object, from: 0, until: b === undefined ? swap : round2(swap + 0.04), legs: [{ t0: 0, t1: pause, a, b: a }, { t0: pause, t1: swap, a, b: b ?? a }] });
|
|
137
|
+
for (const [object, a, b] of on)
|
|
138
|
+
paths.push({ object, from: a === undefined ? arrive : round2(swap + 0.04), until: Infinity, legs: [{ t0: round2(swap + 0.04), t1: arrive, a: a ?? b, b }] });
|
|
139
|
+
const deadOnes = [...at.values()].filter((o) => rules.dead(o));
|
|
140
|
+
const changes = off.length > 0 || on.length > 0 ? [swap, round2(swap + 0.04)] : [];
|
|
141
|
+
const moments = [...new Set([0, pause, shown, ...changes, arrive].map(round2))].filter((m) => m >= 0 && m < total).sort((a, b) => a - b);
|
|
142
|
+
const inserted = moments.map((m) => {
|
|
143
|
+
const objects = [];
|
|
144
|
+
for (const path of paths) {
|
|
145
|
+
const p = placeOn(path, m);
|
|
146
|
+
if (p !== undefined)
|
|
147
|
+
objects.push({ ...path.object, x: round1(p[0]), y: round1(p[1]) });
|
|
148
|
+
}
|
|
149
|
+
// The dead puck: where it stopped, and out of sight from `shown` on.
|
|
150
|
+
if (m < shown)
|
|
151
|
+
for (const o of deadOnes)
|
|
152
|
+
objects.push({ ...o });
|
|
153
|
+
return { t: round2(t0 + m), objects };
|
|
154
|
+
});
|
|
155
|
+
// The restart: the frame given at `until`, and everything after it, moved on.
|
|
156
|
+
const shift = round2(t0 + total - u);
|
|
157
|
+
const move = (t) => (t >= u ? round2(t + shift) : t > t0 ? t0 : t);
|
|
158
|
+
const restart = { t: round2(t0 + total), objects: [...next.values()] };
|
|
159
|
+
frames = [...frames.filter((f) => f.t < t0), ...inserted, restart, ...frames.filter((f) => f.t > u).map((f) => ({ ...f, t: move(f.t) }))];
|
|
160
|
+
events = events.map((e) => ({ ...e, t: move(e.t), ...(isNum(e.until) ? { until: move(e.until) } : {}) }));
|
|
161
|
+
stoppages = [
|
|
162
|
+
...stoppages.map((s) => ({ ...s, start: move(s.start), end: move(s.end) })),
|
|
163
|
+
{ start: t0, end: round2(t0 + total), reason: stop.reason, ...(isNum(stop.x) && isNum(stop.y) ? { x: stop.x, y: stop.y } : {}), ...(stop.team === undefined ? {} : { team: stop.team }) },
|
|
164
|
+
].sort((a, b) => a.start - b.start);
|
|
165
|
+
pers = pers?.map((p) => ({ ...p, start: move(p.start), end: move(p.end) }));
|
|
166
|
+
penalties = penalties?.map((p) => ({ ...p, start: move(p.start), ...(isNum(p.end) ? { end: move(p.end) } : {}) }));
|
|
167
|
+
}
|
|
168
|
+
return { ...timeline, frames, events: events.sort((a, b) => a.t - b.t), stoppages, ...(pers === undefined ? {} : { periods: pers }), ...(penalties === undefined ? {} : { penalties }) };
|
|
169
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FeedFormat, FeedGame } from "./feeds.core.ts";
|
|
2
|
+
import type { FeedError } from "./feeds.core.ts";
|
|
3
|
+
import type { FeedFollowState } from "./feeds.fetch.ts";
|
|
4
|
+
import type { KyogijoReplay } from "./mount.ts";
|
|
5
|
+
import type { ReplaySettings, ReplayTimeline } from "./replay.types.ts";
|
|
6
|
+
/** What a feed played from an address can be told: its format, the replay's settings, and for a test the function that fetches. */
|
|
7
|
+
export interface FeedReplayOptions {
|
|
8
|
+
format?: FeedFormat;
|
|
9
|
+
settings?: ReplaySettings;
|
|
10
|
+
base?: string;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
}
|
|
14
|
+
/** A replay of a feed: the replay's controller, and the game the feed was read into. */
|
|
15
|
+
export type FeedReplay = KyogijoReplay & {
|
|
16
|
+
feed: FeedGame;
|
|
17
|
+
};
|
|
18
|
+
/** What a live follow can be told, beyond `FeedReplayOptions`. */
|
|
19
|
+
export interface ReplayFollowOptions extends FeedReplayOptions {
|
|
20
|
+
/** Seconds between asks: 15 by default, never less than 10. */
|
|
21
|
+
every?: number;
|
|
22
|
+
/** Each new answer, after it is played. */
|
|
23
|
+
onGame?: (game: FeedGame, replay: KyogijoReplay) => void;
|
|
24
|
+
onError?: (error: FeedError) => void;
|
|
25
|
+
onState?: (state: FeedFollowState) => void;
|
|
26
|
+
/** The document whose visibility it follows: the page's by default. */
|
|
27
|
+
doc?: Document;
|
|
28
|
+
}
|
|
29
|
+
/** A replay that follows a feed live: the replay playing now (a new one on each answer that changed), the follow's state, and `stop`, which stops asking and leaves the replay. */
|
|
30
|
+
export interface ReplayFollow {
|
|
31
|
+
replay(): KyogijoReplay | undefined;
|
|
32
|
+
state(): FeedFollowState;
|
|
33
|
+
stop(): void;
|
|
34
|
+
}
|
|
35
|
+
/** A mount's or a view's `replay`: play a timeline, or a feed from an address. */
|
|
36
|
+
export interface ReplayCall {
|
|
37
|
+
(timeline: ReplayTimeline, settings?: ReplaySettings): KyogijoReplay;
|
|
38
|
+
/**
|
|
39
|
+
* Fetch a feed from an address, by the reader's browser (no server of ours, no cookie, nothing kept), read it (`readFeed`: kyogijo's open timeline, or an NHL-style play-by-play) and play it. Rejects with a
|
|
40
|
+
* `FeedError` that says what was wrong: a server that does not let the page read it (`cors`), a status (`http`), an answer that is not a feed (`format`).
|
|
41
|
+
*/
|
|
42
|
+
fromUrl(url: string, options?: FeedReplayOptions): Promise<FeedReplay>;
|
|
43
|
+
/** Follow a feed live: ask it every 15 seconds or more while the page is shown, play each answer that changed from where the replay was (or at its newest moment, when it was there), and stop when the game is over or the server says no. */
|
|
44
|
+
follow(url: string, options?: ReplayFollowOptions): ReplayFollow;
|
|
45
|
+
}
|
|
46
|
+
/** What `feeds.dom.ts` lends a `replay`. */
|
|
47
|
+
export interface FeedKit {
|
|
48
|
+
fromUrl(play: (timeline: ReplayTimeline, settings?: ReplaySettings) => KyogijoReplay, url: string, options?: FeedReplayOptions): Promise<FeedReplay>;
|
|
49
|
+
follow(play: (timeline: ReplayTimeline, settings?: ReplaySettings) => KyogijoReplay, url: string, options?: ReplayFollowOptions): ReplayFollow;
|
|
50
|
+
}
|
|
51
|
+
/** Put the feeds' code in: called once by `feeds.dom.ts`. */
|
|
52
|
+
export declare function useFeedKit(next: FeedKit): void;
|
|
53
|
+
/** A `replay` with `fromUrl` and `follow` on it. */
|
|
54
|
+
export declare function withFeeds(play: (timeline: ReplayTimeline, settings?: ReplaySettings) => KyogijoReplay): ReplayCall;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
let kit;
|
|
2
|
+
/** Put the feeds' code in: called once by `feeds.dom.ts`. */
|
|
3
|
+
export function useFeedKit(next) {
|
|
4
|
+
kit = next;
|
|
5
|
+
}
|
|
6
|
+
const need = () => {
|
|
7
|
+
if (kit === undefined)
|
|
8
|
+
throw new Error("the feeds are not loaded: import \"@johnmorrisdotca/kyogijo-stadiums/feeds\" once, before replay.fromUrl or replay.follow");
|
|
9
|
+
return kit;
|
|
10
|
+
};
|
|
11
|
+
/** A `replay` with `fromUrl` and `follow` on it. */
|
|
12
|
+
export function withFeeds(play) {
|
|
13
|
+
return Object.assign(play, {
|
|
14
|
+
fromUrl: (url, options) => need().fromUrl(play, url, options),
|
|
15
|
+
follow: (url, options) => need().follow(play, url, options),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { DeadTimeRules } from "./replay.deadtime.ts";
|
|
2
|
+
/** Why play stopped, in hockey: each says where it starts again. */
|
|
3
|
+
export type HockeyStop = "goal" | "save" | "frozen" | "icing" | "offside" | "penalty" | "whistle" | "period-start" | "period-end";
|
|
4
|
+
/** The nine face-off spots, feet from centre ice (NHL Rule 1.9): centre, the four in the neutral zone (5 feet outside the blue lines, 44 feet apart) and the four in the end zones (20 feet out from the goal lines, 44 feet apart). */
|
|
5
|
+
export declare const HOCKEY_DOTS: readonly (readonly [number, number])[];
|
|
6
|
+
/** The blue lines are 25 feet either side of centre: past one, play is in an end zone. */
|
|
7
|
+
export declare const HOCKEY_BLUE_LINE = 25;
|
|
8
|
+
/** Is a place one of the nine face-off spots (to a tenth of a foot)? */
|
|
9
|
+
export declare const isHockeyDot: (x: number, y: number) => boolean;
|
|
10
|
+
/** Where play stopped and why, to say where it starts again. */
|
|
11
|
+
export interface HockeyStopAt {
|
|
12
|
+
reason: HockeyStop | string;
|
|
13
|
+
/** Where play stopped: the puck's place, or for a save the place the shot came from. Centre ice when not given. */
|
|
14
|
+
x?: number;
|
|
15
|
+
y?: number;
|
|
16
|
+
/** For icing, offside and a penalty: the end the side it was called against defends, as the sign of its goal line's x (-1 for the left end, 1 for the right). */
|
|
17
|
+
defends?: 1 | -1;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The face-off spot play starts again at after a stoppage, by NHL rule: centre ice after a goal and at the start of a period; after a save or a frozen puck, the end-zone spot of that end on the side
|
|
21
|
+
* the puck was (the nearest spot in the zone); after icing and after a penalty, an end-zone spot of the end the offending side defends (Rules 81 and 76); after offside, the
|
|
22
|
+
* neutral-zone spot just outside the zone the offending side attacks (Rule 83); after any other whistle, the nearest spot in the zone where play stopped (an end-zone spot in an end, a neutral-zone
|
|
23
|
+
* spot between the blue lines, never centre ice). An end-zone spot's side is the side of the rink the puck was on.
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { hockeyFaceoffDot } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
27
|
+
* console.log(hockeyFaceoffDot({ reason: "goal", x: 88, y: 2 }).join(), hockeyFaceoffDot({ reason: "save", x: 60, y: -18 }).join());
|
|
28
|
+
* console.log(hockeyFaceoffDot({ reason: "icing", x: 80, y: 30, defends: -1 }).join(), hockeyFaceoffDot({ reason: "offside", x: 26, y: 5, defends: -1 }).join());
|
|
29
|
+
* // 0,0 69,-22
|
|
30
|
+
* // -69,22 20,22
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function hockeyFaceoffDot(stop: HockeyStopAt): readonly [number, number];
|
|
34
|
+
/** The places of a face-off's line-up. */
|
|
35
|
+
export type HockeyRole = "C" | "LW" | "RW" | "LD" | "RD" | "G";
|
|
36
|
+
/**
|
|
37
|
+
* Where a side lines up for a face-off at a spot (NHL Rule 76), as `[x, y]` in feet from centre ice: `attacks` is the end it attacks (1 for the right). The centre stands on the dot a step back on his
|
|
38
|
+
* own side; the wingers on their own side of the circle's hash marks, outside it; the defence behind them (at the blue line when the spot is in the end the side attacks, in front of its own net when it
|
|
39
|
+
* is in its own end, a stride back from the wingers at centre and in the neutral zone); the goaltender in the crease. A left winger is on the left of a player facing the end his side attacks.
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { hockeyFormation } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
43
|
+
* const at = hockeyFormation([0, 0], 1);
|
|
44
|
+
* console.log(at.C.join(), at.LW.join(), at.G.join());
|
|
45
|
+
* // -2.5,0 -5,17 -85,0
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function hockeyFormation(dot: readonly [number, number], attacks: 1 | -1): Record<HockeyRole, [number, number]>;
|
|
49
|
+
/** The speeds and seconds of hockey's dead time in a replay, and where each side's bench is. */
|
|
50
|
+
export declare const HOCKEY_DEAD_TIME: {
|
|
51
|
+
/** The fastest a player glides to his place, feet a second (about 12 miles an hour: a glide, not a sprint). */
|
|
52
|
+
readonly speed: 18;
|
|
53
|
+
/** Seconds before the players head for the face-off: a goal is celebrated. */
|
|
54
|
+
readonly pause: {
|
|
55
|
+
readonly goal: 2.5;
|
|
56
|
+
readonly other: 0.6;
|
|
57
|
+
};
|
|
58
|
+
/** Seconds the dead puck is seen where it stopped (in the net after a goal), then out of sight until the drop. */
|
|
59
|
+
readonly shown: {
|
|
60
|
+
readonly goal: 2.5;
|
|
61
|
+
readonly other: 1.2;
|
|
62
|
+
};
|
|
63
|
+
/** The least time the players take to their places, and the seconds they stand in them before the puck is dropped. */
|
|
64
|
+
readonly least: 2;
|
|
65
|
+
readonly hold: 1;
|
|
66
|
+
/** The line-up before the opening face-off of a period, seconds: the clock reads the period's full time until the drop. */
|
|
67
|
+
readonly lineUp: 1.5;
|
|
68
|
+
/** The players' benches' gates, on the side of negative y (the side the benches are on, as the arenas draw them): the home side's nearer the left end, the away side's the right. */
|
|
69
|
+
readonly bench: Record<"home" | "away", readonly [number, number]>;
|
|
70
|
+
};
|
|
71
|
+
/** Hockey's dead time as `addDeadTime` reads it: the puck is the dead one, and a player going off or coming on goes by his side's bench. */
|
|
72
|
+
export declare const HOCKEY_RULES: DeadTimeRules;
|
|
73
|
+
/**
|
|
74
|
+
* The stoppage a play of an NHL-style play-by-play is (`typeDescKey` and, for a `stoppage`, `details.reason`): `goal`, `penalty`, the rule set's word for the stoppage's reason, or `undefined` for a
|
|
75
|
+
* play that does not stop play (a shot, a hit, a delayed penalty, which play goes on through until the side it is against touches the puck).
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* import { hockeyStopOf } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
79
|
+
* console.log(hockeyStopOf("stoppage", "icing"), hockeyStopOf("stoppage", "puck-in-netting"), hockeyStopOf("goal"), hockeyStopOf("delayed-penalty"));
|
|
80
|
+
* // icing whistle goal undefined
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function hockeyStopOf(typeDescKey: string, reason?: string): HockeyStop | undefined;
|
|
84
|
+
/** The kinds of penalty, and their minutes on the game clock (NHL Rules 16 to 21: a minor 2, a double minor 4, a major 5, a misconduct 10; a bench minor is a minor served by a player the coach names). */
|
|
85
|
+
export declare const HOCKEY_PENALTY_MINUTES: {
|
|
86
|
+
readonly minor: 2;
|
|
87
|
+
readonly "bench-minor": 2;
|
|
88
|
+
readonly "double-minor": 4;
|
|
89
|
+
readonly major: 5;
|
|
90
|
+
readonly misconduct: 10;
|
|
91
|
+
};
|
|
92
|
+
export type HockeyPenaltyKind = keyof typeof HOCKEY_PENALTY_MINUTES;
|
|
93
|
+
/**
|
|
94
|
+
* The kind of a penalty of an NHL-style play-by-play (`details.typeCode` and `details.duration` of a `penalty`): `MIN` a minor (4 minutes of it a double minor), `BEN` a bench minor, `MAJ` a major,
|
|
95
|
+
* `MIS` and `GMIS` a misconduct; else by its minutes. `undefined` for a penalty shot or a match penalty, which this rule set does not play.
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* import { hockeyPenaltyKind } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
99
|
+
* console.log(hockeyPenaltyKind("MIN", 2), hockeyPenaltyKind("MIN", 4), hockeyPenaltyKind("BEN", 2), hockeyPenaltyKind("MAJ", 5), hockeyPenaltyKind("MIS", 10), hockeyPenaltyKind("PS", 0));
|
|
100
|
+
* // minor double-minor bench-minor major misconduct undefined
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function hockeyPenaltyKind(typeCode?: string, duration?: number): HockeyPenaltyKind | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Where a side's penalty box is, the seat its penalized player sits on, in the `nhl-api` frame: across the ice from the players' benches (NHL Rule 1: the two penalty boxes on the side opposite the
|
|
106
|
+
* benches, one each side of the off-ice officials' box at centre), the home side's on the left, behind the boards. A stand-in for the rink's own figures (the benches and boxes of the rink, KYOGIJO-
|
|
107
|
+
* STADIUMS-47) until they are in the package: then this reads them, and nothing that calls it changes.
|
|
108
|
+
*/
|
|
109
|
+
export declare function hockeyPenaltyBox(team: "home" | "away"): readonly [number, number];
|
|
110
|
+
/** A call of a penalty, on the game clock: when (seconds of game played since the start), against which side, what kind. */
|
|
111
|
+
export interface HockeyCall {
|
|
112
|
+
team: "home" | "away";
|
|
113
|
+
kind: HockeyPenaltyKind;
|
|
114
|
+
at: number;
|
|
115
|
+
}
|
|
116
|
+
/** When a penalty's time begins and ends on the game clock, and whether its side plays short. */
|
|
117
|
+
export interface HockeyServed {
|
|
118
|
+
/** When its time begins: the call, or, for a side already two short, when one of those ends (it never plays with fewer than three skaters). */
|
|
119
|
+
from: number;
|
|
120
|
+
/** When the player leaves the box: his time ran out, or a power-play goal ended his minor. */
|
|
121
|
+
to: number;
|
|
122
|
+
short: boolean;
|
|
123
|
+
/** Ended by a power-play goal. */
|
|
124
|
+
byGoal: boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* How a game's penalties are served, by NHL rule, all on the game clock (seconds played since the start): each penalty's player sits from the call; a minor is 2 minutes, a double minor 4, a major 5 and a
|
|
128
|
+
* misconduct 10; his side plays a skater short (not for a misconduct, served by a substitute, nor for coincidental penalties, called on both sides at once and as many each, which leave the strength
|
|
129
|
+
* even); a side is never more than two short, so a third penalty's time waits for one of the first two to end; a goal by the side with more skaters ends the scored-on side's minor with the least time
|
|
130
|
+
* left (a double minor loses the part it is in; a major is served in full). Pure.
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { hockeyPenaltyEnds } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
134
|
+
* const calls = [{ team: "away" as const, kind: "minor" as const, at: 100 }, { team: "home" as const, kind: "major" as const, at: 400 }];
|
|
135
|
+
* const goals = [{ team: "home" as const, at: 160 }, { team: "away" as const, at: 500 }];
|
|
136
|
+
* console.log(hockeyPenaltyEnds(calls, goals).map((s) => `${s.from}-${s.to} ${s.short} ${s.byGoal}`).join(", "));
|
|
137
|
+
* // 100-160 true true, 400-700 true false
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export declare function hockeyPenaltyEnds(calls: readonly HockeyCall[], goals: readonly {
|
|
141
|
+
team: "home" | "away";
|
|
142
|
+
at: number;
|
|
143
|
+
}[]): HockeyServed[];
|