@opndev/tennis-match-center 0.0.1

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.
@@ -0,0 +1,122 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesley@opndev.io>
2
+ //
3
+ // SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ import { GROUP_TEMPLATES, templateMatches } from './group-schedule.mjs'
6
+ import { buildUnits, placeLayout, unitLayout } from './paired-court-planner.mjs'
7
+
8
+ /**
9
+ * Plans genuine 3-team groups: A v B, B v C, C v A.
10
+ *
11
+ * A 3-team round robin never needs more than one court at once (every
12
+ * one of its three matches shares a team with the other two, so none
13
+ * of them can run alongside each other) - unlike a 4-team group, which
14
+ * needs a pair. But a lone 3-team group still runs its whole round
15
+ * robin back to back with no rest between a team's own matches, the
16
+ * same problem 4-team bundling solves. So a singles/doubles division
17
+ * drawn in threes takes the very same bundling: two such groups share
18
+ * a court set and take turns, one group's turn a slot at a time,
19
+ * giving each a rest through the other's - it only ever costs one
20
+ * court doing it, not a pair.
21
+ *
22
+ * Team Cup is a genuine 3-team shape too (its own division's teams
23
+ * numbering 4 players a side, not 1 or 2) but does NOT bundle -
24
+ * group-schedule.mjs's own GROUP_TEMPLATES[3] comment already says
25
+ * why ("one court per group"). auto-planner.mjs's buildThreeTeamUnits
26
+ * is what tells the two apart (division.team_size, sent through as
27
+ * each group's own team_size) and keeps Team Cup groups out of this
28
+ * module's bundling entirely, each on its own dedicated unit of one -
29
+ * this module itself has no opinion on team_size at all, only on the
30
+ * groups it is actually handed.
31
+ *
32
+ * The bundling and layout mechanics live in paired-court-planner.mjs,
33
+ * shared with four-team-planner.mjs. This module owns only what is
34
+ * genuinely 3-team's own: its template (group-schedule.mjs's
35
+ * GROUP_TEMPLATES[3]) and the public names callers depend on.
36
+ *
37
+ * Groups are pooled across divisions on purpose - a division with a
38
+ * single 3-team group has nobody of its own to share with.
39
+ */
40
+
41
+ /**
42
+ * @param {Object} params
43
+ * @param {Array<Object>} params.groups
44
+ * {id, competitionId, divisionId, divisionName, divisionGender, groupName, teamIds}
45
+ * @param {Array<Object>} params.courts in the order they should be used
46
+ * @param {Array<Object>} params.slots the day's slots, with start/end minutes
47
+ * @param {string} params.date
48
+ * @param {Object} params.occupancy shared with the other planners
49
+ * @param {?number} params.darkFrom minutes past midnight
50
+ * @returns {{blocks: Array<Object>, unplaced: Array<Object>}}
51
+ */
52
+ export function planThreeTeamGroups({
53
+ groups,
54
+ courts,
55
+ slots,
56
+ date,
57
+ occupancy,
58
+ darkFrom = null,
59
+ }) {
60
+ const blocks = []
61
+ const unplaced = []
62
+
63
+ // buildUnits leaves a solitary group (nobody to bundle with) out
64
+ // entirely - the caller's to decide what happens to it. Here, that
65
+ // is placing it on its own: still one continuous run on one court,
66
+ // same as it always was, just with nobody to trade rest with.
67
+ const units = groups.length === 1 ? [groups] : threeTeamUnits(groups)
68
+
69
+ for (const unit of units) {
70
+ const placed = placeUnit({ unit, courts, slots, date, occupancy, darkFrom, blocks })
71
+
72
+ if (!placed) {
73
+ unplaced.push(...unit)
74
+ }
75
+ }
76
+
77
+ return { blocks, unplaced }
78
+ }
79
+
80
+ /**
81
+ * Splits the groups into units sharing a court set. A lone group (no
82
+ * dense/sparse choice for 3-team - see paired-court-planner.mjs's
83
+ * TODO) is left for the caller to place on its own, the same as
84
+ * four-team-planner.mjs's buildUnits does.
85
+ */
86
+ export function threeTeamUnits(groups) {
87
+ return buildUnits(groups)
88
+ }
89
+
90
+ function placeUnit({ unit, courts, slots, date, occupancy, darkFrom, blocks }) {
91
+ const layout = threeTeamUnitLayout(unit.map(group => ({
92
+ id: group.id,
93
+ teamIds: group.teamIds,
94
+ })))
95
+
96
+ return placeLayout({
97
+ layout,
98
+ lookup: groupId => unit.find(entry => entry.id === groupId),
99
+ courts,
100
+ slots,
101
+ date,
102
+ occupancy,
103
+ darkFrom,
104
+ blocks,
105
+ })
106
+ }
107
+
108
+ /**
109
+ * Lays out a set of 3-team groups sharing one court, turn by turn - see
110
+ * paired-court-planner.mjs's unitLayout. A single group's own layout
111
+ * (unit of one, no bundling) is the degenerate case: three turns of
112
+ * one match each, back to back on whichever one court hosts it.
113
+ *
114
+ * @param {Array<{id: number, teamIds: number[]}>} groups
115
+ * @returns {Array<Array<{groupId: number, index: number}|null>>} slot by court
116
+ */
117
+ export function threeTeamUnitLayout(groups) {
118
+ return unitLayout(groups, {
119
+ template: GROUP_TEMPLATES[3],
120
+ matchesFor: group => templateMatches({ teamIds: group.teamIds, groupSize: 3 }),
121
+ })
122
+ }
@@ -0,0 +1,25 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesley@opndev.io>
2
+ //
3
+ // SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ /**
6
+ * Registers every component under its own tag.
7
+ *
8
+ * Imported for its side effect and nothing else:
9
+ * `import '@opndev/tennis-match-center/webcomponents'`. A component
10
+ * never registers itself - whether a tag is claimed, and under which
11
+ * name, is the page's decision. A caller wanting one component, or its
12
+ * own tag for it, imports the component's module and registers it.
13
+ */
14
+
15
+ import { TennisPlanningBoardElement } from './components/planning-board.mjs'
16
+ import { TennisRosterTeamElement } from './components/roster-team.mjs'
17
+ import { TennisRosterBoardElement } from './components/roster-board.mjs'
18
+ import { TennisBracketBoardElement } from './components/bracket-board.mjs'
19
+ import { TennisScheduleBoardElement } from './components/schedule-board.mjs'
20
+
21
+ TennisPlanningBoardElement.register()
22
+ TennisRosterTeamElement.register()
23
+ TennisRosterBoardElement.register()
24
+ TennisBracketBoardElement.register()
25
+ TennisScheduleBoardElement.register()
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "author": {
3
+ "email": "wesley@opndev.io",
4
+ "name": "Wesley Schwengle"
5
+ },
6
+ "dependencies": {
7
+ "@twemoji/api": ">=17.0.0"
8
+ },
9
+ "description": "Match Center",
10
+ "devDependencies": {
11
+ "jsdoc": "latest",
12
+ "jsdom": "latest",
13
+ "tap": "latest"
14
+ },
15
+ "exports": {
16
+ ".": "./lib/index.mjs",
17
+ "./webcomponents": "./lib/webcomponents.mjs"
18
+ },
19
+ "keywords": [
20
+ "tennis",
21
+ "aruba-open"
22
+ ],
23
+ "license": "AGPL-3.0-or-later",
24
+ "name": "@opndev/tennis-match-center",
25
+ "peerDependencies": {
26
+ "@skirbi/sugar": ">=0.1.20"
27
+ },
28
+ "private": false,
29
+ "scripts": {
30
+ "build": "rzil build",
31
+ "jsdoc": "jsdoc -c .jsdoc.json",
32
+ "pkg": "rzil pkg",
33
+ "release": "rzil release",
34
+ "test": "tap"
35
+ },
36
+ "sideEffects": true,
37
+ "type": "module",
38
+ "version": "0.0.1"
39
+ }