@johnmorrisdotca/kyogijo-stadiums 0.4.0 → 0.5.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.
Files changed (244) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/NOTICE.md +4 -0
  3. package/README.md +71 -16
  4. package/dist/annotate.js +1 -1
  5. package/dist/athletics/describe.d.ts +8 -0
  6. package/dist/athletics/describe.js +41 -0
  7. package/dist/athletics/events.d.ts +17 -0
  8. package/dist/athletics/events.js +48 -0
  9. package/dist/athletics/facilities.d.ts +80 -0
  10. package/dist/athletics/facilities.js +176 -0
  11. package/dist/athletics/infield.d.ts +79 -0
  12. package/dist/athletics/infield.js +166 -0
  13. package/dist/athletics/layout.d.ts +13 -0
  14. package/dist/athletics/layout.js +98 -0
  15. package/dist/athletics/oval.d.ts +57 -0
  16. package/dist/athletics/oval.js +116 -0
  17. package/dist/athletics/plan.d.ts +97 -0
  18. package/dist/athletics/plan.js +355 -0
  19. package/dist/athletics/races.d.ts +102 -0
  20. package/dist/athletics/races.js +355 -0
  21. package/dist/athletics/steeplechase.d.ts +72 -0
  22. package/dist/athletics/steeplechase.js +196 -0
  23. package/dist/athletics/stencil.d.ts +13 -0
  24. package/dist/athletics/stencil.js +85 -0
  25. package/dist/athletics/track.d.ts +54 -0
  26. package/dist/athletics/track.js +105 -0
  27. package/dist/athletics/types.d.ts +77 -0
  28. package/dist/athletics/types.js +1 -0
  29. package/dist/athletics-entry.d.ts +17 -0
  30. package/dist/athletics-entry.js +11 -0
  31. package/dist/athletics.style.d.ts +16 -0
  32. package/dist/athletics.style.js +51 -0
  33. package/dist/atlas-entry.d.ts +4 -2
  34. package/dist/atlas-entry.js +3 -2
  35. package/dist/atlas.d.ts +83 -7
  36. package/dist/atlas.js +108 -8
  37. package/dist/charts-entry.d.ts +6 -1
  38. package/dist/charts-entry.js +3 -0
  39. package/dist/charts.athletics.d.ts +183 -0
  40. package/dist/charts.athletics.js +455 -0
  41. package/dist/charts.curling.heat.d.ts +24 -0
  42. package/dist/charts.curling.heat.js +116 -0
  43. package/dist/charts.curling.js +14 -3
  44. package/dist/charts.pool.d.ts +67 -0
  45. package/dist/charts.pool.js +76 -0
  46. package/dist/charts.skating.d.ts +204 -0
  47. package/dist/charts.skating.js +426 -0
  48. package/dist/charts.types.d.ts +53 -1
  49. package/dist/court.d.ts +85 -2
  50. package/dist/court.js +102 -7
  51. package/dist/court.more.d.ts +46 -0
  52. package/dist/court.more.js +219 -0
  53. package/dist/courts.style.d.ts +14 -2
  54. package/dist/courts.style.js +51 -4
  55. package/dist/crowd.d.ts +154 -0
  56. package/dist/crowd.draw.d.ts +17 -0
  57. package/dist/crowd.draw.js +198 -0
  58. package/dist/crowd.js +465 -0
  59. package/dist/crowd.park.d.ts +19 -0
  60. package/dist/crowd.park.js +153 -0
  61. package/dist/curling.stats.d.ts +271 -0
  62. package/dist/curling.stats.js +301 -0
  63. package/dist/draw.d.ts +16 -0
  64. package/dist/draw.js +318 -35
  65. package/dist/element-entry.d.ts +1 -1
  66. package/dist/element-entry.js +1 -1
  67. package/dist/element.d.ts +13 -2
  68. package/dist/element.js +77 -3
  69. package/dist/features.js +131 -0
  70. package/dist/fields.kit.d.ts +2 -2
  71. package/dist/fields.types.d.ts +18 -4
  72. package/dist/frames.providers.js +63 -8
  73. package/dist/geojson.js +3 -0
  74. package/dist/geometry-entry.d.ts +2 -1
  75. package/dist/geometry-entry.js +2 -1
  76. package/dist/geometry.d.ts +20 -0
  77. package/dist/geometry.js +78 -1
  78. package/dist/grounds.d.ts +4 -0
  79. package/dist/grounds.data.js +12 -0
  80. package/dist/hardwood.d.ts +31 -0
  81. package/dist/hardwood.js +157 -0
  82. package/dist/heat.js +4 -3
  83. package/dist/heat.types.d.ts +5 -0
  84. package/dist/heat.zones.d.ts +2 -2
  85. package/dist/heat.zones.js +154 -6
  86. package/dist/homeside.js +17 -0
  87. package/dist/index.d.ts +16 -2
  88. package/dist/index.js +10 -1
  89. package/dist/layers.d.ts +1 -1
  90. package/dist/layers.env.d.ts +3 -1
  91. package/dist/layers.env.js +4 -2
  92. package/dist/layers.js +4 -4
  93. package/dist/leagues/athletics.d.ts +63 -0
  94. package/dist/leagues/athletics.js +87 -0
  95. package/dist/leagues/courts.d.ts +269 -4
  96. package/dist/leagues/courts.js +155 -1
  97. package/dist/leagues/skating.d.ts +145 -0
  98. package/dist/leagues/skating.js +73 -0
  99. package/dist/leagues/sources.js +20 -0
  100. package/dist/leagues/types.d.ts +1 -1
  101. package/dist/leagues-entry.d.ts +4 -2
  102. package/dist/leagues-entry.js +2 -1
  103. package/dist/leagues.d.ts +14 -4
  104. package/dist/leagues.js +146 -5
  105. package/dist/look.d.ts +3 -3
  106. package/dist/look.js +39 -6
  107. package/dist/outline.d.ts +1 -1
  108. package/dist/outline.js +30 -2
  109. package/dist/pageant.d.ts +120 -0
  110. package/dist/pageant.draw.d.ts +21 -0
  111. package/dist/pageant.draw.js +171 -0
  112. package/dist/pageant.js +207 -0
  113. package/dist/pageant.style.d.ts +8 -0
  114. package/dist/pageant.style.js +42 -0
  115. package/dist/png.d.ts +8 -1
  116. package/dist/png.js +21 -1
  117. package/dist/pool.d.ts +176 -0
  118. package/dist/pool.figures.d.ts +304 -0
  119. package/dist/pool.figures.js +144 -0
  120. package/dist/pool.js +304 -0
  121. package/dist/pool.kit.d.ts +48 -0
  122. package/dist/pool.kit.js +141 -0
  123. package/dist/pool.labels.d.ts +20 -0
  124. package/dist/pool.labels.js +257 -0
  125. package/dist/pool.race.d.ts +65 -0
  126. package/dist/pool.race.js +140 -0
  127. package/dist/pool.replay.d.ts +46 -0
  128. package/dist/pool.replay.js +127 -0
  129. package/dist/pool.section.d.ts +13 -0
  130. package/dist/pool.section.js +83 -0
  131. package/dist/pool.style.d.ts +20 -0
  132. package/dist/pool.style.js +169 -0
  133. package/dist/pool.types.d.ts +145 -0
  134. package/dist/pool.types.js +3 -0
  135. package/dist/pool.words.d.ts +13 -0
  136. package/dist/pool.words.js +101 -0
  137. package/dist/ranges.names.js +47 -3
  138. package/dist/replay-entry.d.ts +3 -0
  139. package/dist/replay-entry.js +2 -0
  140. package/dist/restyle.js +1 -1
  141. package/dist/score.js +70 -5
  142. package/dist/sheet.d.ts +3 -0
  143. package/dist/sheet.js +7 -3
  144. package/dist/skating/describe.d.ts +7 -0
  145. package/dist/skating/describe.js +44 -0
  146. package/dist/skating/events.d.ts +24 -0
  147. package/dist/skating/events.js +47 -0
  148. package/dist/skating/figure.d.ts +24 -0
  149. package/dist/skating/figure.js +37 -0
  150. package/dist/skating/long.d.ts +54 -0
  151. package/dist/skating/long.js +111 -0
  152. package/dist/skating/parts.d.ts +43 -0
  153. package/dist/skating/parts.js +40 -0
  154. package/dist/skating/plan.d.ts +55 -0
  155. package/dist/skating/plan.js +197 -0
  156. package/dist/skating/short.d.ts +47 -0
  157. package/dist/skating/short.js +89 -0
  158. package/dist/skating/track.d.ts +66 -0
  159. package/dist/skating/track.js +94 -0
  160. package/dist/skating/types.d.ts +57 -0
  161. package/dist/skating/types.js +3 -0
  162. package/dist/skating-entry.d.ts +17 -0
  163. package/dist/skating-entry.js +11 -0
  164. package/dist/skating.style.d.ts +4 -0
  165. package/dist/skating.style.js +24 -0
  166. package/dist/strings.d.ts +296 -0
  167. package/dist/strings.js +328 -11
  168. package/dist/style.d.ts +2 -0
  169. package/dist/style.js +11 -1
  170. package/dist/surfaces/artistic.d.ts +9 -0
  171. package/dist/surfaces/artistic.js +94 -0
  172. package/dist/surfaces/athletics.d.ts +16 -0
  173. package/dist/surfaces/athletics.js +132 -0
  174. package/dist/surfaces/baseball.js +15 -0
  175. package/dist/surfaces/basketball.floor.d.ts +27 -0
  176. package/dist/surfaces/basketball.floor.js +83 -0
  177. package/dist/surfaces/basketball.gear.d.ts +27 -0
  178. package/dist/surfaces/basketball.gear.js +90 -0
  179. package/dist/surfaces/basketball.js +43 -21
  180. package/dist/surfaces/curling.d.ts +16 -0
  181. package/dist/surfaces/curling.fold.d.ts +67 -0
  182. package/dist/surfaces/curling.fold.js +107 -0
  183. package/dist/surfaces/curling.js +37 -3
  184. package/dist/surfaces/dive.d.ts +9 -0
  185. package/dist/surfaces/dive.js +128 -0
  186. package/dist/surfaces/field.d.ts +7 -2
  187. package/dist/surfaces/field.js +15 -9
  188. package/dist/surfaces/hockey.js +1 -0
  189. package/dist/surfaces/net.d.ts +2 -1
  190. package/dist/surfaces/net.js +160 -15
  191. package/dist/surfaces/polo.d.ts +12 -0
  192. package/dist/surfaces/polo.js +159 -0
  193. package/dist/surfaces/pool.d.ts +27 -0
  194. package/dist/surfaces/pool.js +198 -0
  195. package/dist/surfaces/skating.d.ts +6 -0
  196. package/dist/surfaces/skating.js +62 -0
  197. package/dist/surfaces/soccer.js +2 -1
  198. package/dist/surfaces/surface.d.ts +44 -0
  199. package/dist/surfaces/venue.d.ts +15 -0
  200. package/dist/surfaces/venue.js +95 -0
  201. package/dist/tenants.d.ts +77 -0
  202. package/dist/tenants.data.d.ts +8 -0
  203. package/dist/tenants.data.js +945 -0
  204. package/dist/tenants.js +46 -0
  205. package/dist/turn-entry.d.ts +12 -0
  206. package/dist/turn-entry.js +7 -0
  207. package/dist/turn.camera.d.ts +105 -0
  208. package/dist/turn.camera.js +168 -0
  209. package/dist/turn.dom.d.ts +40 -0
  210. package/dist/turn.dom.js +351 -0
  211. package/dist/turn.kit.d.ts +86 -0
  212. package/dist/turn.kit.js +110 -0
  213. package/dist/turn.markup.d.ts +48 -0
  214. package/dist/turn.markup.js +108 -0
  215. package/dist/turn.model.d.ts +78 -0
  216. package/dist/turn.model.js +91 -0
  217. package/dist/turn.style.d.ts +5 -0
  218. package/dist/turn.style.js +55 -0
  219. package/dist/types.d.ts +183 -18
  220. package/dist/units.d.ts +1 -1
  221. package/dist/units.js +15 -1
  222. package/dist/uprights.board.d.ts +17 -0
  223. package/dist/uprights.board.js +59 -0
  224. package/dist/uprights.d.ts +37 -0
  225. package/dist/uprights.fields.d.ts +32 -0
  226. package/dist/uprights.fields.js +172 -0
  227. package/dist/uprights.goal.d.ts +49 -0
  228. package/dist/uprights.goal.js +69 -0
  229. package/dist/uprights.js +160 -0
  230. package/dist/uprights.more.d.ts +7 -0
  231. package/dist/uprights.more.js +183 -0
  232. package/dist/uprights.net.d.ts +12 -0
  233. package/dist/uprights.net.js +109 -0
  234. package/dist/uprights.netkit.d.ts +28 -0
  235. package/dist/uprights.netkit.js +67 -0
  236. package/dist/uprights.pools.d.ts +8 -0
  237. package/dist/uprights.pools.js +190 -0
  238. package/dist/uprights.rinks.d.ts +27 -0
  239. package/dist/uprights.rinks.js +106 -0
  240. package/dist/uprights.track.d.ts +50 -0
  241. package/dist/uprights.track.js +246 -0
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +1 -1
  244. package/package.json +19 -2
@@ -0,0 +1,271 @@
1
+ import type { CurlingOptions, CurlingStone } from "./charts.types.ts";
2
+ /** A team of a game, as the stones and the board name them. */
3
+ export type CurlingSide = "home" | "away";
4
+ /** The free guards of a layout: how many each team has, and which stones they are. */
5
+ export interface CurlingGuards {
6
+ home: number;
7
+ away: number;
8
+ /** The places in the list given of the stones that are free guards. */
9
+ indices: number[];
10
+ /** A team that is not `home` or `away` is counted under its name. */
11
+ others: Record<string, number>;
12
+ }
13
+ /**
14
+ * The free guards of a layout. A free guard is a stone in play that is between the tee line and the hog line and is not in the house (the free guard zone, World Curling R6 and
15
+ * Curling Canada Rule 12): its centre is nearer the hog line than the tee line, and no part of it is within the 12-foot circle. Stones out of play are not counted. Places are feet
16
+ * from the button (in `units`), as `curlingScore` takes them.
17
+ * @example
18
+ * ```ts
19
+ * import { curlingFreeGuards } from "@johnmorrisdotca/kyogijo-stadiums/charts";
20
+ * const guards = curlingFreeGuards([
21
+ * { x: 0.2, y: -11, team: "home" },
22
+ * { x: -2, y: -16, team: "away" },
23
+ * { x: 0.5, y: 0.5, team: "home" },
24
+ * { x: 3, y: -19, team: "away" },
25
+ * ]);
26
+ * console.log(guards.home, guards.away, guards.indices);
27
+ * // 1 2 [ 0, 1, 3 ]
28
+ * ```
29
+ */
30
+ export declare function curlingFreeGuards(stones: readonly CurlingStone[], options?: {
31
+ units?: CurlingOptions["units"];
32
+ width?: number;
33
+ }): CurlingGuards;
34
+ /** The stones in play in each layout of an end, and the most there were. */
35
+ export interface CurlingInPlay {
36
+ /** Stones in play in each layout, in the order given (after each throw). */
37
+ counts: number[];
38
+ /** The most stones in play at any time of the end, and the first layout that had that many. */
39
+ max: number;
40
+ at: number;
41
+ }
42
+ /**
43
+ * The stones in play after each throw of an end, from the layout after each throw (`curlingScore`'s rules: a stone that touches a sideline, does not come to rest beyond the hog
44
+ * line or has wholly crossed the back line is out). The most stones in play is the measure of how crowded an end got.
45
+ * @example
46
+ * ```ts
47
+ * import { curlingInPlay } from "@johnmorrisdotca/kyogijo-stadiums/charts";
48
+ * const end = curlingInPlay([
49
+ * [{ x: 0, y: -12, team: "home" }],
50
+ * [{ x: 0, y: -12, team: "home" }, { x: 1, y: 0, team: "away" }],
51
+ * [{ x: 1, y: 0, team: "away" }],
52
+ * ]);
53
+ * console.log(end.counts, end.max, end.at);
54
+ * // [ 1, 2, 1 ] 2 1
55
+ * ```
56
+ */
57
+ export declare function curlingInPlay(layouts: readonly (readonly CurlingStone[])[], options?: {
58
+ units?: CurlingOptions["units"];
59
+ width?: number;
60
+ }): CurlingInPlay;
61
+ /** What a team scored in an end: one team scores in an end and the other has nothing, or neither does (a blank end). `hammer` is the team that threw last, when the record says. */
62
+ export interface CurlingEndScore {
63
+ home: number;
64
+ away: number;
65
+ hammer?: CurlingSide;
66
+ }
67
+ /** One end, worked out. */
68
+ export interface CurlingEndRecord {
69
+ /** The number of the end, from 1. */
70
+ end: number;
71
+ home: number;
72
+ away: number;
73
+ /** The team with the hammer, and the team that scored (`null` for a blank end). */
74
+ hammer: CurlingSide | null;
75
+ scoring: CurlingSide | null;
76
+ points: number;
77
+ blank: boolean;
78
+ /** The team without the hammer scored. */
79
+ steal: boolean;
80
+ /** The team with the hammer scored exactly one point (it was forced to take one). */
81
+ forced: boolean;
82
+ /** The score after the end. */
83
+ homeTotal: number;
84
+ awayTotal: number;
85
+ }
86
+ /** How a team did in the ends it had the hammer. */
87
+ export interface CurlingHammerStats {
88
+ /** Ends with the hammer. */
89
+ ends: number;
90
+ /** Ends in which the team scored, and its points in them. */
91
+ scored: number;
92
+ points: number;
93
+ /** Ends it blanked (to keep the hammer), ends it was stolen from, ends it scored exactly one, two or more, and three or more. */
94
+ blanked: number;
95
+ stolen: number;
96
+ forced: number;
97
+ multi: number;
98
+ big: number;
99
+ /** Points per end with the hammer, the measure of hammer efficiency. */
100
+ efficiency: number | null;
101
+ /** Percent of its hammer ends in which it scored, blanked, was held to one, and was stolen from. */
102
+ scoringRate: number | null;
103
+ blankRate: number | null;
104
+ forceRate: number | null;
105
+ stolenRate: number | null;
106
+ }
107
+ /** How a team did in the ends it had no hammer. */
108
+ export interface CurlingStealStats {
109
+ ends: number;
110
+ /** Ends in which it scored (stole), and its points in them. */
111
+ steals: number;
112
+ points: number;
113
+ /** Percent of its ends without the hammer in which it stole. */
114
+ stealRate: number | null;
115
+ }
116
+ /** One team's ends. */
117
+ export interface CurlingTeamEnds {
118
+ points: number;
119
+ /** Ends in which the team scored. */
120
+ endsScored: number;
121
+ withHammer: CurlingHammerStats;
122
+ withoutHammer: CurlingStealStats;
123
+ }
124
+ /** What `curlingEndStats` gives back. */
125
+ export interface CurlingEndStats {
126
+ ends: number;
127
+ /** Ends in which neither team scored. */
128
+ blanks: number;
129
+ records: CurlingEndRecord[];
130
+ home: CurlingTeamEnds;
131
+ away: CurlingTeamEnds;
132
+ }
133
+ /**
134
+ * The team with the hammer in each end. The team that scored in an end gives up the hammer: the other team has it in the next end. After a blank end the same team keeps it. An end
135
+ * that names its own `hammer` is taken as it says, and the rule goes on from there. `first` is the team with the hammer in the first end.
136
+ * @example
137
+ * ```ts
138
+ * import { curlingHammers } from "@johnmorrisdotca/kyogijo-stadiums/charts";
139
+ * console.log(curlingHammers([{ home: 2, away: 0 }, { home: 0, away: 0 }, { home: 0, away: 1 }, { home: 1, away: 0 }], "home"));
140
+ * // [ 'home', 'away', 'away', 'home' ]
141
+ * ```
142
+ */
143
+ export declare function curlingHammers(ends: readonly CurlingEndScore[], first: CurlingSide): CurlingSide[];
144
+ /**
145
+ * The ends of a game: for each end who scored and how many, who had the hammer, whether it was a blank end, a steal or a forced one; and for each team its hammer efficiency (points
146
+ * per end with the hammer), how often it scored, blanked, was held to one or was stolen from with the hammer, and how often it stole without it. Each end is what each team scored in it
147
+ * (one of them nothing: an end is scored by one team, so two scores in an end are an error), as a score board's `ends` give it; `hammer` is the team that threw last in the first end, or
148
+ * each end names its own.
149
+ * @throws TypeError when `ends` is not a list; RangeError when an end is not a whole number of points for each team, or both teams scored in it.
150
+ * @example
151
+ * ```ts
152
+ * import { curlingEndStats } from "@johnmorrisdotca/kyogijo-stadiums/charts";
153
+ * const game = curlingEndStats(
154
+ * [{ home: 2, away: 0 }, { home: 0, away: 1 }, { home: 0, away: 1 }, { home: 1, away: 0 }, { home: 0, away: 2 }],
155
+ * { hammer: "home" },
156
+ * );
157
+ * console.log(game.home.points, game.away.points, game.home.withHammer.efficiency, game.away.withoutHammer.steals);
158
+ * // 3 4 1 1
159
+ * ```
160
+ */
161
+ export declare function curlingEndStats(ends: readonly CurlingEndScore[], options?: {
162
+ hammer?: CurlingSide;
163
+ }): CurlingEndStats;
164
+ /** One end for the free guard analysis: the free guards each team had after the first four stones of the end (or whenever the caller counts them), and the points each scored. */
165
+ export interface CurlingGuardEnd {
166
+ hammerGuards: number;
167
+ otherGuards: number;
168
+ hammerPoints: number;
169
+ otherPoints: number;
170
+ }
171
+ /** What `curlingGuardScores` gives back. */
172
+ export interface CurlingGuardScores {
173
+ /** The names of the bands of guards: `0`, `1-2` and `3+` by default. */
174
+ bands: string[];
175
+ /** For each band of guards the hammer team had: the ends, and the average points the hammer team scored in them. */
176
+ hammer: {
177
+ band: string;
178
+ ends: number;
179
+ average: number | null;
180
+ }[];
181
+ /** The same for the guards the team without the hammer had and the points it scored. */
182
+ other: {
183
+ band: string;
184
+ ends: number;
185
+ average: number | null;
186
+ }[];
187
+ /** Every pair of bands, the hammer team's first: the ends, and the average points each team scored. */
188
+ cells: {
189
+ hammer: string;
190
+ other: string;
191
+ ends: number;
192
+ hammerAverage: number | null;
193
+ otherAverage: number | null;
194
+ }[];
195
+ }
196
+ /**
197
+ * How many points each team scored against how many free guards each had out: the free guard analysis (does the team with the hammer score more when it has put guards out, does the
198
+ * team without it score more when it has?). The guards are put in bands, `0`, `1-2` and `3+` by default (`bands` are the lowest number of each band). `blanks: false` leaves the
199
+ * blank ends out of the hammer team's averages, as a team that blanks an end has chosen to score nothing.
200
+ * @throws TypeError when `ends` is not a list; RangeError for a number of guards or points that is not a whole number, or bands that do not rise from 0.
201
+ * @example
202
+ * ```ts
203
+ * import { curlingGuardScores } from "@johnmorrisdotca/kyogijo-stadiums/charts";
204
+ * const table = curlingGuardScores([
205
+ * { hammerGuards: 0, otherGuards: 1, hammerPoints: 1, otherPoints: 0 },
206
+ * { hammerGuards: 2, otherGuards: 0, hammerPoints: 2, otherPoints: 0 },
207
+ * { hammerGuards: 1, otherGuards: 2, hammerPoints: 0, otherPoints: 1 },
208
+ * ]);
209
+ * console.log(table.bands.join(" "), table.hammer.map((b) => b.average ?? "-").join(" "));
210
+ * // 0 1-2 3+ 1 1 -
211
+ * ```
212
+ */
213
+ export declare function curlingGuardScores(ends: readonly CurlingGuardEnd[], options?: {
214
+ bands?: readonly number[];
215
+ blanks?: boolean;
216
+ }): CurlingGuardScores;
217
+ /** One shot, as a shot-by-shot summary gives it: who threw it, what kind it was, and how it was scored from 0 to 4 (4 is the perfect shot). `hit` says whether it was a hit, when the type does not. */
218
+ export interface CurlingShot {
219
+ team?: CurlingSide | (string & {});
220
+ /** The kind of shot, as the summary names it: `draw`, `take-out`, `guard`, `clearing`, `hit and roll`... */
221
+ type?: string;
222
+ /** How well it was played, 0 to 4 (World Curling's scale). A shot without a score is counted in the types and left out of the percentage. */
223
+ score?: number | null;
224
+ hit?: boolean;
225
+ }
226
+ /** The shot types that are hits: a stone thrown to take out another. Any other type is not a hit, unless the shot says `hit`. */
227
+ export declare const CURLING_HIT_TYPES: readonly string[];
228
+ /** True for a shot that is a hit: it says so, or its type is one of `CURLING_HIT_TYPES`. */
229
+ export declare function curlingIsHit(shot: CurlingShot): boolean;
230
+ /** A group of shots: how many, how many were scored, and the shot percentage. */
231
+ export interface CurlingShotLine {
232
+ shots: number;
233
+ /** Shots with a score, and the points they earned out of 4 each. */
234
+ scored: number;
235
+ points: number;
236
+ /** The shot percentage: the points as a percent of the most they could earn, 4 for each scored shot. */
237
+ percent: number | null;
238
+ hits: number;
239
+ /** Percent of the shots that were hits. */
240
+ hitRate: number | null;
241
+ }
242
+ /** What `curlingShotStats` gives back. */
243
+ export interface CurlingShotStats extends CurlingShotLine {
244
+ /** For each team (as the shots name it). */
245
+ teams: Record<string, CurlingShotLine>;
246
+ /** For each type of shot, most played first: the line, and its share of all the shots as a percent. */
247
+ types: (CurlingShotLine & {
248
+ type: string;
249
+ share: number | null;
250
+ })[];
251
+ /** Shots that were not scored, and shots with no place in `teams` or `types` because they name none. */
252
+ unscored: number;
253
+ }
254
+ /**
255
+ * The shots of a game, by team and by type: the shot percentage (the points earned, 0 to 4 for each shot, as a percent of 4 for each scored shot, the way World Curling's shot-by-shot
256
+ * summaries and broadcasts count it), the hit rate, and how often each type of shot was played. A score outside 0 to 4 is an error.
257
+ * @throws TypeError when `shots` is not a list; RangeError for a score that is not between 0 and 4.
258
+ * @example
259
+ * ```ts
260
+ * import { curlingShotStats } from "@johnmorrisdotca/kyogijo-stadiums/charts";
261
+ * const shots = curlingShotStats([
262
+ * { team: "home", type: "draw", score: 4 },
263
+ * { team: "home", type: "take-out", score: 3 },
264
+ * { team: "away", type: "Hit and Roll", score: 2 },
265
+ * { team: "away", type: "guard", score: 4 },
266
+ * ]);
267
+ * console.log(shots.percent, shots.teams.home.percent, shots.hitRate, shots.types[0].type);
268
+ * // 81.3 87.5 50 draw
269
+ * ```
270
+ */
271
+ export declare function curlingShotStats(shots: readonly CurlingShot[]): CurlingShotStats;
@@ -0,0 +1,301 @@
1
+ // Curling's statistics, as small pure functions beside `curlingScore`: the free guards in play, the stones in play after each throw, the ends of a game (who scored, who had the
2
+ // hammer, how often a team with it scored, blanked, was held to one or was stolen from, and how often a team without it stole), and the shots of a game (how each type of shot
3
+ // was played, the shot percentage and how many were hits). Written for this package from the rules (the free guard zone is World Curling's R6 and Curling Canada's Rule 12,
4
+ // the hammer's passing from their rules of play) and from the definitions that the analyses of curlovision (BSD) and Jordan Myslik's curling analytics describe in words; no code
5
+ // of either was read or used.
6
+ import { curlingScore } from "./charts.curling.js";
7
+ import { convertLength } from "./units.js";
8
+ const round = (value, places = 1) => Math.round(value * 10 ** places) / 10 ** places;
9
+ const percent = (part, whole) => (whole === 0 ? null : round((100 * part) / whole));
10
+ const mean = (sum, n) => (n === 0 ? null : round(sum / n, 2));
11
+ const other = (side) => (side === "home" ? "away" : "home");
12
+ /**
13
+ * The free guards of a layout. A free guard is a stone in play that is between the tee line and the hog line and is not in the house (the free guard zone, World Curling R6 and
14
+ * Curling Canada Rule 12): its centre is nearer the hog line than the tee line, and no part of it is within the 12-foot circle. Stones out of play are not counted. Places are feet
15
+ * from the button (in `units`), as `curlingScore` takes them.
16
+ * @example
17
+ * ```ts
18
+ * import { curlingFreeGuards } from "@johnmorrisdotca/kyogijo-stadiums/charts";
19
+ * const guards = curlingFreeGuards([
20
+ * { x: 0.2, y: -11, team: "home" },
21
+ * { x: -2, y: -16, team: "away" },
22
+ * { x: 0.5, y: 0.5, team: "home" },
23
+ * { x: 3, y: -19, team: "away" },
24
+ * ]);
25
+ * console.log(guards.home, guards.away, guards.indices);
26
+ * // 1 2 [ 0, 1, 3 ]
27
+ * ```
28
+ */
29
+ export function curlingFreeGuards(stones, options = {}) {
30
+ const judged = curlingScore(stones, options);
31
+ const unit = options.units ?? "ft";
32
+ const out = { home: 0, away: 0, indices: [], others: {} };
33
+ for (const r of judged.results) {
34
+ if (!r.inPlay || r.inHouse)
35
+ continue;
36
+ const y = convertLength(stones[r.index].y, unit, "ft");
37
+ if (!(y < 0))
38
+ continue;
39
+ out.indices.push(r.index);
40
+ const team = stones[r.index].team ?? "home";
41
+ if (team === "home" || team === "away")
42
+ out[team] += 1;
43
+ else
44
+ out.others[team] = (out.others[team] ?? 0) + 1;
45
+ }
46
+ return out;
47
+ }
48
+ /**
49
+ * The stones in play after each throw of an end, from the layout after each throw (`curlingScore`'s rules: a stone that touches a sideline, does not come to rest beyond the hog
50
+ * line or has wholly crossed the back line is out). The most stones in play is the measure of how crowded an end got.
51
+ * @example
52
+ * ```ts
53
+ * import { curlingInPlay } from "@johnmorrisdotca/kyogijo-stadiums/charts";
54
+ * const end = curlingInPlay([
55
+ * [{ x: 0, y: -12, team: "home" }],
56
+ * [{ x: 0, y: -12, team: "home" }, { x: 1, y: 0, team: "away" }],
57
+ * [{ x: 1, y: 0, team: "away" }],
58
+ * ]);
59
+ * console.log(end.counts, end.max, end.at);
60
+ * // [ 1, 2, 1 ] 2 1
61
+ * ```
62
+ */
63
+ export function curlingInPlay(layouts, options = {}) {
64
+ if (!Array.isArray(layouts))
65
+ throw new TypeError("layouts is a list of layouts, each a list of stones");
66
+ const counts = layouts.map((layout) => curlingScore(layout, options).inPlay);
67
+ const max = counts.reduce((m, n) => Math.max(m, n), 0);
68
+ return { counts, max, at: counts.indexOf(max) };
69
+ }
70
+ /**
71
+ * The team with the hammer in each end. The team that scored in an end gives up the hammer: the other team has it in the next end. After a blank end the same team keeps it. An end
72
+ * that names its own `hammer` is taken as it says, and the rule goes on from there. `first` is the team with the hammer in the first end.
73
+ * @example
74
+ * ```ts
75
+ * import { curlingHammers } from "@johnmorrisdotca/kyogijo-stadiums/charts";
76
+ * console.log(curlingHammers([{ home: 2, away: 0 }, { home: 0, away: 0 }, { home: 0, away: 1 }, { home: 1, away: 0 }], "home"));
77
+ * // [ 'home', 'away', 'away', 'home' ]
78
+ * ```
79
+ */
80
+ export function curlingHammers(ends, first) {
81
+ const out = [];
82
+ let hammer = first;
83
+ for (const e of ends) {
84
+ if (e.hammer === "home" || e.hammer === "away")
85
+ hammer = e.hammer;
86
+ out.push(hammer);
87
+ const scoring = e.home > 0 ? "home" : e.away > 0 ? "away" : null;
88
+ if (scoring !== null)
89
+ hammer = other(scoring);
90
+ }
91
+ return out;
92
+ }
93
+ const emptyHammer = () => ({ ends: 0, scored: 0, points: 0, blanked: 0, stolen: 0, forced: 0, multi: 0, big: 0, efficiency: null, scoringRate: null, blankRate: null, forceRate: null, stolenRate: null });
94
+ /**
95
+ * The ends of a game: for each end who scored and how many, who had the hammer, whether it was a blank end, a steal or a forced one; and for each team its hammer efficiency (points
96
+ * per end with the hammer), how often it scored, blanked, was held to one or was stolen from with the hammer, and how often it stole without it. Each end is what each team scored in it
97
+ * (one of them nothing: an end is scored by one team, so two scores in an end are an error), as a score board's `ends` give it; `hammer` is the team that threw last in the first end, or
98
+ * each end names its own.
99
+ * @throws TypeError when `ends` is not a list; RangeError when an end is not a whole number of points for each team, or both teams scored in it.
100
+ * @example
101
+ * ```ts
102
+ * import { curlingEndStats } from "@johnmorrisdotca/kyogijo-stadiums/charts";
103
+ * const game = curlingEndStats(
104
+ * [{ home: 2, away: 0 }, { home: 0, away: 1 }, { home: 0, away: 1 }, { home: 1, away: 0 }, { home: 0, away: 2 }],
105
+ * { hammer: "home" },
106
+ * );
107
+ * console.log(game.home.points, game.away.points, game.home.withHammer.efficiency, game.away.withoutHammer.steals);
108
+ * // 3 4 1 1
109
+ * ```
110
+ */
111
+ export function curlingEndStats(ends, options = {}) {
112
+ if (!Array.isArray(ends))
113
+ throw new TypeError("ends is a list of ends, each { home, away }");
114
+ const whole = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
115
+ ends.forEach((e, i) => {
116
+ if (typeof e !== "object" || e === null || !whole(e.home) || !whole(e.away))
117
+ throw new RangeError(`end ${i + 1} is not a whole number of points for each team`);
118
+ if (e.home > 0 && e.away > 0)
119
+ throw new RangeError(`end ${i + 1} has a score for both teams: one team scores in an end`);
120
+ });
121
+ const first = options.hammer ?? ends.find((e) => e.hammer === "home" || e.hammer === "away")?.hammer;
122
+ const hammers = first === undefined ? undefined : curlingHammers(ends, first);
123
+ const teams = {
124
+ home: { points: 0, endsScored: 0, withHammer: emptyHammer(), withoutHammer: { ends: 0, steals: 0, points: 0, stealRate: null } },
125
+ away: { points: 0, endsScored: 0, withHammer: emptyHammer(), withoutHammer: { ends: 0, steals: 0, points: 0, stealRate: null } },
126
+ };
127
+ const records = [];
128
+ let homeTotal = 0;
129
+ let awayTotal = 0;
130
+ let blanks = 0;
131
+ ends.forEach((e, i) => {
132
+ const hammer = hammers === undefined ? null : hammers[i];
133
+ const scoring = e.home > 0 ? "home" : e.away > 0 ? "away" : null;
134
+ const points = e.home + e.away;
135
+ homeTotal += e.home;
136
+ awayTotal += e.away;
137
+ const blank = scoring === null;
138
+ if (blank)
139
+ blanks += 1;
140
+ const steal = hammer !== null && scoring !== null && scoring !== hammer;
141
+ const forced = hammer !== null && scoring === hammer && points === 1;
142
+ records.push({ end: i + 1, home: e.home, away: e.away, hammer, scoring, points, blank, steal, forced, homeTotal, awayTotal });
143
+ if (scoring !== null) {
144
+ teams[scoring].points += points;
145
+ teams[scoring].endsScored += 1;
146
+ }
147
+ if (hammer === null)
148
+ return;
149
+ const h = teams[hammer].withHammer;
150
+ h.ends += 1;
151
+ if (scoring === hammer) {
152
+ h.scored += 1;
153
+ h.points += points;
154
+ if (points === 1)
155
+ h.forced += 1;
156
+ else
157
+ h.multi += 1;
158
+ if (points >= 3)
159
+ h.big += 1;
160
+ }
161
+ else if (blank)
162
+ h.blanked += 1;
163
+ else
164
+ h.stolen += 1;
165
+ const n = teams[other(hammer)].withoutHammer;
166
+ n.ends += 1;
167
+ if (steal) {
168
+ n.steals += 1;
169
+ n.points += points;
170
+ }
171
+ });
172
+ for (const side of ["home", "away"]) {
173
+ const h = teams[side].withHammer;
174
+ h.efficiency = mean(h.points, h.ends);
175
+ h.scoringRate = percent(h.scored, h.ends);
176
+ h.blankRate = percent(h.blanked, h.ends);
177
+ h.forceRate = percent(h.forced, h.ends);
178
+ h.stolenRate = percent(h.stolen, h.ends);
179
+ const n = teams[side].withoutHammer;
180
+ n.stealRate = percent(n.steals, n.ends);
181
+ }
182
+ return { ends: ends.length, blanks, records, home: teams.home, away: teams.away };
183
+ }
184
+ /**
185
+ * How many points each team scored against how many free guards each had out: the free guard analysis (does the team with the hammer score more when it has put guards out, does the
186
+ * team without it score more when it has?). The guards are put in bands, `0`, `1-2` and `3+` by default (`bands` are the lowest number of each band). `blanks: false` leaves the
187
+ * blank ends out of the hammer team's averages, as a team that blanks an end has chosen to score nothing.
188
+ * @throws TypeError when `ends` is not a list; RangeError for a number of guards or points that is not a whole number, or bands that do not rise from 0.
189
+ * @example
190
+ * ```ts
191
+ * import { curlingGuardScores } from "@johnmorrisdotca/kyogijo-stadiums/charts";
192
+ * const table = curlingGuardScores([
193
+ * { hammerGuards: 0, otherGuards: 1, hammerPoints: 1, otherPoints: 0 },
194
+ * { hammerGuards: 2, otherGuards: 0, hammerPoints: 2, otherPoints: 0 },
195
+ * { hammerGuards: 1, otherGuards: 2, hammerPoints: 0, otherPoints: 1 },
196
+ * ]);
197
+ * console.log(table.bands.join(" "), table.hammer.map((b) => b.average ?? "-").join(" "));
198
+ * // 0 1-2 3+ 1 1 -
199
+ * ```
200
+ */
201
+ export function curlingGuardScores(ends, options = {}) {
202
+ if (!Array.isArray(ends))
203
+ throw new TypeError("ends is a list of ends, each { hammerGuards, otherGuards, hammerPoints, otherPoints }");
204
+ const edges = options.bands ?? [0, 1, 3];
205
+ if (edges.length === 0 || edges[0] !== 0 || !edges.every((e, i) => Number.isInteger(e) && (i === 0 || e > edges[i - 1])))
206
+ throw new RangeError("bands are whole numbers that rise from 0: the lowest number of guards of each band");
207
+ const names = edges.map((e, i) => (i === edges.length - 1 ? `${e}+` : edges[i + 1] - 1 === e ? String(e) : `${e}-${edges[i + 1] - 1}`));
208
+ const bandOf = (n) => edges.reduce((at, e, i) => (n >= e ? i : at), 0);
209
+ const whole = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
210
+ ends.forEach((e, i) => {
211
+ if (typeof e !== "object" || e === null || ![e.hammerGuards, e.otherGuards, e.hammerPoints, e.otherPoints].every(whole))
212
+ throw new RangeError(`end ${i + 1} needs a whole number for each of hammerGuards, otherGuards, hammerPoints and otherPoints`);
213
+ });
214
+ const blanks = options.blanks !== false;
215
+ const average = (sum, n) => mean(sum, n);
216
+ const sums = (pick, points, keep) => names.map((band, i) => {
217
+ const list = ends.filter((e) => bandOf(pick(e)) === i && keep(e));
218
+ return { band, ends: list.length, average: average(list.reduce((sum, e) => sum + points(e), 0), list.length) };
219
+ });
220
+ const cells = names.flatMap((hammer, h) => names.map((other, o) => {
221
+ const list = ends.filter((e) => bandOf(e.hammerGuards) === h && bandOf(e.otherGuards) === o);
222
+ const kept = list.filter((e) => blanks || e.hammerPoints > 0 || e.otherPoints > 0);
223
+ return { hammer, other, ends: list.length, hammerAverage: average(kept.reduce((sum, e) => sum + e.hammerPoints, 0), kept.length), otherAverage: average(list.reduce((sum, e) => sum + e.otherPoints, 0), list.length) };
224
+ }));
225
+ return {
226
+ bands: names,
227
+ hammer: sums((e) => e.hammerGuards, (e) => e.hammerPoints, (e) => blanks || e.hammerPoints > 0 || e.otherPoints > 0),
228
+ other: sums((e) => e.otherGuards, (e) => e.otherPoints, () => true),
229
+ cells,
230
+ };
231
+ }
232
+ /** The shot types that are hits: a stone thrown to take out another. Any other type is not a hit, unless the shot says `hit`. */
233
+ export const CURLING_HIT_TYPES = Object.freeze(["take-out", "double take-out", "raise take-out", "hit and roll", "peel", "clearing", "runback"]);
234
+ /** A shot type as it is compared and named: lower case, one space between words (`Hit-and-Roll` and `Hit & Roll` are `hit and roll`). */
235
+ const typeKey = (type) => type.toLowerCase().replace(/&/g, " and ").replace(/[\s_-]+/g, " ").trim();
236
+ /** True for a shot that is a hit: it says so, or its type is one of `CURLING_HIT_TYPES`. */
237
+ export function curlingIsHit(shot) {
238
+ if (typeof shot.hit === "boolean")
239
+ return shot.hit;
240
+ if (typeof shot.type !== "string")
241
+ return false;
242
+ const key = typeKey(shot.type);
243
+ return CURLING_HIT_TYPES.some((t) => typeKey(t) === key);
244
+ }
245
+ const tally = (shots) => {
246
+ let scored = 0;
247
+ let points = 0;
248
+ let hits = 0;
249
+ for (const s of shots) {
250
+ if (typeof s.score === "number" && Number.isFinite(s.score)) {
251
+ scored += 1;
252
+ points += s.score;
253
+ }
254
+ if (curlingIsHit(s))
255
+ hits += 1;
256
+ }
257
+ return { shots: shots.length, scored, points, percent: percent(points, 4 * scored), hits, hitRate: percent(hits, shots.length) };
258
+ };
259
+ /**
260
+ * The shots of a game, by team and by type: the shot percentage (the points earned, 0 to 4 for each shot, as a percent of 4 for each scored shot, the way World Curling's shot-by-shot
261
+ * summaries and broadcasts count it), the hit rate, and how often each type of shot was played. A score outside 0 to 4 is an error.
262
+ * @throws TypeError when `shots` is not a list; RangeError for a score that is not between 0 and 4.
263
+ * @example
264
+ * ```ts
265
+ * import { curlingShotStats } from "@johnmorrisdotca/kyogijo-stadiums/charts";
266
+ * const shots = curlingShotStats([
267
+ * { team: "home", type: "draw", score: 4 },
268
+ * { team: "home", type: "take-out", score: 3 },
269
+ * { team: "away", type: "Hit and Roll", score: 2 },
270
+ * { team: "away", type: "guard", score: 4 },
271
+ * ]);
272
+ * console.log(shots.percent, shots.teams.home.percent, shots.hitRate, shots.types[0].type);
273
+ * // 81.3 87.5 50 draw
274
+ * ```
275
+ */
276
+ export function curlingShotStats(shots) {
277
+ if (!Array.isArray(shots))
278
+ throw new TypeError("shots is a list of shots");
279
+ shots.forEach((s, i) => {
280
+ if (typeof s !== "object" || s === null)
281
+ throw new TypeError(`shot ${i + 1} is not a shot`);
282
+ if (s.score !== undefined && s.score !== null && !(typeof s.score === "number" && s.score >= 0 && s.score <= 4))
283
+ throw new RangeError(`shot ${i + 1} has the score ${String(s.score)}: a shot is scored 0 to 4`);
284
+ });
285
+ const byTeam = new Map();
286
+ const byType = new Map();
287
+ for (const s of shots) {
288
+ if (typeof s.team === "string" && s.team !== "")
289
+ byTeam.set(s.team, [...(byTeam.get(s.team) ?? []), s]);
290
+ if (typeof s.type === "string" && s.type.trim() !== "") {
291
+ const key = typeKey(s.type);
292
+ const held = byType.get(key) ?? { name: key, shots: [] };
293
+ held.shots.push(s);
294
+ byType.set(key, held);
295
+ }
296
+ }
297
+ const types = [...byType.values()]
298
+ .map((t) => ({ type: t.name, ...tally(t.shots), share: percent(t.shots.length, shots.length) }))
299
+ .sort((a, b) => b.shots - a.shots || a.type.localeCompare(b.type));
300
+ return { ...tally(shots), teams: Object.fromEntries([...byTeam].map(([team, list]) => [team, tally(list)])), types, unscored: shots.filter((s) => typeof s.score !== "number").length };
301
+ }
package/dist/draw.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { isPortrait } from "./common.ts";
2
+ import { type CrowdStands } from "./crowd.ts";
3
+ import { type SoccerPitchId, type SoccerSize } from "./surfaces/soccer-spec.ts";
2
4
  import type { Colors, Language, ScoreOptions, Sport, SurfaceOptions } from "./types.ts";
3
5
  export { isPortrait };
4
6
  /** The sports that can be drawn, in the order a menu lists them. */
@@ -22,6 +24,8 @@ export declare function winnerOf(o: Pick<ScoreOptions, "home" | "away" | "status
22
24
  * neither, the away team's is first unless `homeSide` is left.
23
25
  */
24
26
  export declare function statusText(o: Pick<ScoreOptions, "home" | "away" | "status" | "homeSide"> & Partial<Pick<ScoreOptions, "sport" | "league">>, language?: Language): string | undefined;
27
+ /** The pitch the options ask for: a `pitch` of the caller's, else the size a `ground` publishes, else none (the international pitch). */
28
+ export declare function pitchOf(o: SurfaceOptions): SoccerPitchId | SoccerSize | undefined;
25
29
  /** A drawing's title and description, in a language. */
26
30
  export declare function describe(o: SurfaceOptions | ScoreOptions): {
27
31
  title: string;
@@ -50,3 +54,15 @@ export declare function drawSurface(options: SurfaceOptions): string;
50
54
  export declare function drawScore(options: ScoreOptions): string;
51
55
  /** Draw a score as one compact strip, for a list. The same as `drawScore` with `size: "badge"`. */
52
56
  export declare function drawBadge(options: ScoreOptions): string;
57
+ /**
58
+ * The stands round a surface as data: the rings of seats with their outlines, heights and rake, the blocks of visitors and the places left open, in feet in the surface's
59
+ * wide frame, so that another view (a 3D one) can raise them. `undefined` when the options ask for no crowd (or for a special game, which has none).
60
+ * @example
61
+ * ```ts
62
+ * import { crowdStands } from "@johnmorrisdotca/kyogijo-stadiums";
63
+ * const stands = crowdStands({ sport: "soccer", crowd: true });
64
+ * console.log(stands?.rings.map((ring) => ring.id).join(" "), stands?.shape, stands?.stadium);
65
+ * // lower upper bowl purpose
66
+ * ```
67
+ */
68
+ export declare function crowdStands(o: SurfaceOptions): CrowdStands | undefined;