volleyballsimtypes 0.0.489 → 0.0.492

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 (151) hide show
  1. package/dist/cjs/src/api/index.d.ts +4 -3
  2. package/dist/cjs/src/api/index.js +15 -0
  3. package/dist/cjs/src/api/tactics-player-refs.d.ts +13 -0
  4. package/dist/cjs/src/api/tactics-player-refs.js +43 -0
  5. package/dist/cjs/src/data/event-purge.d.ts +14 -0
  6. package/dist/cjs/src/data/event-purge.js +116 -0
  7. package/dist/cjs/src/data/index.d.ts +1 -0
  8. package/dist/cjs/src/data/index.js +1 -0
  9. package/dist/cjs/src/data/init-models.js +41 -0
  10. package/dist/cjs/src/data/models/competition.d.ts +1 -1
  11. package/dist/cjs/src/data/models/competition.js +1 -1
  12. package/dist/cjs/src/data/models/event-bot-team.d.ts +21 -0
  13. package/dist/cjs/src/data/models/event-bot-team.js +44 -0
  14. package/dist/cjs/src/data/models/event-competition.d.ts +29 -0
  15. package/dist/cjs/src/data/models/event-competition.js +52 -0
  16. package/dist/cjs/src/data/models/event-pack-player.d.ts +22 -0
  17. package/dist/cjs/src/data/models/event-pack-player.js +31 -0
  18. package/dist/cjs/src/data/models/event-pack.d.ts +37 -0
  19. package/dist/cjs/src/data/models/event-pack.js +55 -0
  20. package/dist/cjs/src/data/models/event-result.d.ts +34 -0
  21. package/dist/cjs/src/data/models/event-result.js +48 -0
  22. package/dist/cjs/src/data/models/event-squad.d.ts +30 -0
  23. package/dist/cjs/src/data/models/event-squad.js +46 -0
  24. package/dist/cjs/src/data/models/event.d.ts +51 -0
  25. package/dist/cjs/src/data/models/event.js +82 -0
  26. package/dist/cjs/src/data/models/index.d.ts +7 -0
  27. package/dist/cjs/src/data/models/index.js +7 -0
  28. package/dist/cjs/src/data/models/lineup-preset.d.ts +3 -1
  29. package/dist/cjs/src/data/models/lineup-preset.js +53 -0
  30. package/dist/cjs/src/data/models/tactics.d.ts +2 -2
  31. package/dist/cjs/src/data/transformers/tactics.test.js +19 -0
  32. package/dist/cjs/src/index.d.ts +1 -0
  33. package/dist/cjs/src/index.js +6 -0
  34. package/dist/cjs/src/service/competition/event-draft.d.ts +46 -0
  35. package/dist/cjs/src/service/competition/event-draft.js +85 -0
  36. package/dist/cjs/src/service/competition/event-draft.test.d.ts +1 -0
  37. package/dist/cjs/src/service/competition/event-draft.test.js +91 -0
  38. package/dist/cjs/src/service/competition/event-rewards.d.ts +26 -0
  39. package/dist/cjs/src/service/competition/event-rewards.js +49 -0
  40. package/dist/cjs/src/service/competition/event-rewards.test.d.ts +1 -0
  41. package/dist/cjs/src/service/competition/event-rewards.test.js +58 -0
  42. package/dist/cjs/src/service/competition/event-schedule.d.ts +126 -0
  43. package/dist/cjs/src/service/competition/event-schedule.js +141 -0
  44. package/dist/cjs/src/service/competition/event-schedule.test.d.ts +1 -0
  45. package/dist/cjs/src/service/competition/event-schedule.test.js +138 -0
  46. package/dist/cjs/src/service/competition/index.d.ts +3 -0
  47. package/dist/cjs/src/service/competition/index.js +3 -0
  48. package/dist/cjs/src/service/match/index.d.ts +1 -0
  49. package/dist/cjs/src/service/match/index.js +1 -0
  50. package/dist/cjs/src/service/match/match-generator.js +25 -48
  51. package/dist/cjs/src/service/match/match-generator.test.d.ts +1 -0
  52. package/dist/cjs/src/service/match/match-generator.test.js +48 -0
  53. package/dist/cjs/src/service/match/round-robin.d.ts +11 -0
  54. package/dist/cjs/src/service/match/round-robin.js +44 -0
  55. package/dist/cjs/src/service/match/round-robin.test.d.ts +1 -0
  56. package/dist/cjs/src/service/match/round-robin.test.js +71 -0
  57. package/dist/cjs/src/service/notification/catalog/en.json +21 -0
  58. package/dist/cjs/src/service/notification/catalog/es.json +21 -0
  59. package/dist/cjs/src/service/notification/catalog/fr.json +21 -0
  60. package/dist/cjs/src/service/notification/catalog/it.json +21 -0
  61. package/dist/cjs/src/service/notification/catalog/pl.json +21 -0
  62. package/dist/cjs/src/service/notification/catalog/pt-BR.json +21 -0
  63. package/dist/cjs/src/service/notification/catalog/tr.json +21 -0
  64. package/dist/cjs/src/service/notification/notification-type.d.ts +1 -1
  65. package/dist/cjs/src/service/notification/notification-type.js +6 -1
  66. package/dist/cjs/src/service/notification/notification.test.js +3 -1
  67. package/dist/cjs/src/service/notification/registry.js +52 -0
  68. package/dist/cjs/src/service/player/player-generator.d.ts +1 -0
  69. package/dist/cjs/src/service/player/player-generator.js +16 -1
  70. package/dist/cjs/src/service/team/blocking-preference.d.ts +3 -1
  71. package/dist/cjs/src/service/team/blocking-preference.js +7 -0
  72. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +2 -2
  73. package/dist/cjs/src/service/team/schemas/tactics.z.js +7 -2
  74. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +32 -0
  75. package/dist/cjs/src/service/team/schemas/team.z.d.ts +2 -2
  76. package/dist/esm/src/api/index.d.ts +4 -3
  77. package/dist/esm/src/api/index.js +1 -0
  78. package/dist/esm/src/api/tactics-player-refs.d.ts +13 -0
  79. package/dist/esm/src/api/tactics-player-refs.js +40 -0
  80. package/dist/esm/src/data/event-purge.d.ts +14 -0
  81. package/dist/esm/src/data/event-purge.js +110 -0
  82. package/dist/esm/src/data/index.d.ts +1 -0
  83. package/dist/esm/src/data/index.js +1 -0
  84. package/dist/esm/src/data/init-models.js +42 -1
  85. package/dist/esm/src/data/models/competition.d.ts +1 -1
  86. package/dist/esm/src/data/models/competition.js +1 -1
  87. package/dist/esm/src/data/models/event-bot-team.d.ts +21 -0
  88. package/dist/esm/src/data/models/event-bot-team.js +40 -0
  89. package/dist/esm/src/data/models/event-competition.d.ts +29 -0
  90. package/dist/esm/src/data/models/event-competition.js +48 -0
  91. package/dist/esm/src/data/models/event-pack-player.d.ts +22 -0
  92. package/dist/esm/src/data/models/event-pack-player.js +27 -0
  93. package/dist/esm/src/data/models/event-pack.d.ts +37 -0
  94. package/dist/esm/src/data/models/event-pack.js +51 -0
  95. package/dist/esm/src/data/models/event-result.d.ts +34 -0
  96. package/dist/esm/src/data/models/event-result.js +44 -0
  97. package/dist/esm/src/data/models/event-squad.d.ts +30 -0
  98. package/dist/esm/src/data/models/event-squad.js +42 -0
  99. package/dist/esm/src/data/models/event.d.ts +51 -0
  100. package/dist/esm/src/data/models/event.js +78 -0
  101. package/dist/esm/src/data/models/index.d.ts +7 -0
  102. package/dist/esm/src/data/models/index.js +7 -0
  103. package/dist/esm/src/data/models/lineup-preset.d.ts +3 -1
  104. package/dist/esm/src/data/models/lineup-preset.js +20 -0
  105. package/dist/esm/src/data/models/tactics.d.ts +2 -2
  106. package/dist/esm/src/data/transformers/tactics.test.js +20 -1
  107. package/dist/esm/src/index.d.ts +1 -0
  108. package/dist/esm/src/index.js +4 -0
  109. package/dist/esm/src/service/competition/event-draft.d.ts +46 -0
  110. package/dist/esm/src/service/competition/event-draft.js +78 -0
  111. package/dist/esm/src/service/competition/event-draft.test.d.ts +1 -0
  112. package/dist/esm/src/service/competition/event-draft.test.js +89 -0
  113. package/dist/esm/src/service/competition/event-rewards.d.ts +26 -0
  114. package/dist/esm/src/service/competition/event-rewards.js +44 -0
  115. package/dist/esm/src/service/competition/event-rewards.test.d.ts +1 -0
  116. package/dist/esm/src/service/competition/event-rewards.test.js +56 -0
  117. package/dist/esm/src/service/competition/event-schedule.d.ts +126 -0
  118. package/dist/esm/src/service/competition/event-schedule.js +129 -0
  119. package/dist/esm/src/service/competition/event-schedule.test.d.ts +1 -0
  120. package/dist/esm/src/service/competition/event-schedule.test.js +136 -0
  121. package/dist/esm/src/service/competition/index.d.ts +3 -0
  122. package/dist/esm/src/service/competition/index.js +3 -0
  123. package/dist/esm/src/service/match/index.d.ts +1 -0
  124. package/dist/esm/src/service/match/index.js +1 -0
  125. package/dist/esm/src/service/match/match-generator.js +25 -48
  126. package/dist/esm/src/service/match/match-generator.test.d.ts +1 -0
  127. package/dist/esm/src/service/match/match-generator.test.js +43 -0
  128. package/dist/esm/src/service/match/round-robin.d.ts +11 -0
  129. package/dist/esm/src/service/match/round-robin.js +41 -0
  130. package/dist/esm/src/service/match/round-robin.test.d.ts +1 -0
  131. package/dist/esm/src/service/match/round-robin.test.js +69 -0
  132. package/dist/esm/src/service/notification/catalog/en.json +21 -0
  133. package/dist/esm/src/service/notification/catalog/es.json +21 -0
  134. package/dist/esm/src/service/notification/catalog/fr.json +21 -0
  135. package/dist/esm/src/service/notification/catalog/it.json +21 -0
  136. package/dist/esm/src/service/notification/catalog/pl.json +21 -0
  137. package/dist/esm/src/service/notification/catalog/pt-BR.json +21 -0
  138. package/dist/esm/src/service/notification/catalog/tr.json +21 -0
  139. package/dist/esm/src/service/notification/notification-type.d.ts +1 -1
  140. package/dist/esm/src/service/notification/notification-type.js +6 -1
  141. package/dist/esm/src/service/notification/notification.test.js +3 -1
  142. package/dist/esm/src/service/notification/registry.js +52 -0
  143. package/dist/esm/src/service/player/player-generator.d.ts +1 -0
  144. package/dist/esm/src/service/player/player-generator.js +15 -0
  145. package/dist/esm/src/service/team/blocking-preference.d.ts +3 -1
  146. package/dist/esm/src/service/team/blocking-preference.js +6 -1
  147. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +2 -2
  148. package/dist/esm/src/service/team/schemas/tactics.z.js +7 -2
  149. package/dist/esm/src/service/team/schemas/tactics.z.test.js +32 -0
  150. package/dist/esm/src/service/team/schemas/team.z.d.ts +2 -2
  151. package/package.json +6 -1
@@ -16,6 +16,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./standing"), exports);
18
18
  __exportStar(require("./season"), exports);
19
+ __exportStar(require("./event-schedule"), exports);
20
+ __exportStar(require("./event-draft"), exports);
21
+ __exportStar(require("./event-rewards"), exports);
19
22
  __exportStar(require("./league"), exports);
20
23
  __exportStar(require("./division"), exports);
21
24
  __exportStar(require("./iteration"), exports);
@@ -4,6 +4,7 @@ export * from './match-generator';
4
4
  export * from './match-rating';
5
5
  export * from './match-set';
6
6
  export * from './rally';
7
+ export * from './round-robin';
7
8
  export * from './point-breakdown';
8
9
  export * from './rally-metrics';
9
10
  export * from './court-position';
@@ -20,6 +20,7 @@ __exportStar(require("./match-generator"), exports);
20
20
  __exportStar(require("./match-rating"), exports);
21
21
  __exportStar(require("./match-set"), exports);
22
22
  __exportStar(require("./rally"), exports);
23
+ __exportStar(require("./round-robin"), exports);
23
24
  __exportStar(require("./point-breakdown"), exports);
24
25
  __exportStar(require("./rally-metrics"), exports);
25
26
  __exportStar(require("./court-position"), exports);
@@ -7,9 +7,13 @@ const dayjs_1 = __importDefault(require("dayjs"));
7
7
  const utc_1 = __importDefault(require("dayjs/plugin/utc"));
8
8
  const uuid_1 = require("uuid");
9
9
  const match_1 = require("./match");
10
+ const round_robin_1 = require("./round-robin");
10
11
  const utils_1 = require("../utils");
11
12
  const data_1 = require("../../data");
12
13
  dayjs_1.default.extend(utc_1.default);
14
+ // Consecutive matches in a league season start half an hour apart, so a 16-team double round robin (240 matches)
15
+ // spans five days from the iteration's Monday.
16
+ const MATCH_SPACING_MINUTES = 30;
13
17
  // eslint-disable-next-line @typescript-eslint/no-extraneous-class
14
18
  class MatchGenerator {
15
19
  static generateMatches(teams, baseDate) {
@@ -21,58 +25,31 @@ class MatchGenerator {
21
25
  if (t.size !== teams.length) {
22
26
  throw new Error('DUPLICATE_TEAMS');
23
27
  }
24
- let shuffled = (0, utils_1.shuffle)([...teams]);
25
- const l = shuffled.length - 1;
26
- const half = Math.ceil(shuffled.length / 2);
27
- const matches = [];
28
- // We fix the first team
29
- let firstTeam = shuffled.shift();
30
- if (firstTeam == null) {
28
+ if (teams.length < 1) {
31
29
  throw new Error('EMPTY_TEAM_ARRAY');
32
30
  }
33
- let homeRow;
34
- let awayRow;
35
- for (let i = 0; i < l; i++) {
36
- // split array in 2 halves
37
- const row1 = shuffled.slice(0, half);
38
- const row2 = shuffled.slice(half);
39
- row2.push(firstTeam);
40
- if (i % 2 === 0) {
41
- homeRow = row1;
42
- awayRow = row2;
43
- }
44
- else {
45
- homeRow = row2;
46
- awayRow = row1;
47
- }
48
- for (let j = 0; j < half; j++) {
49
- matches.push(match_1.Match.create({
50
- id: (0, uuid_1.v4)(),
51
- homeTeam: homeRow[j],
52
- awayTeam: awayRow[j],
53
- scheduledDate: date.toDate(),
54
- status: data_1.StatusEnum.PENDING
55
- }));
56
- date = date.add(30, 'minutes');
57
- }
58
- firstTeam = row2.pop();
59
- row1.unshift(row2.shift());
60
- row2.push(row1.pop());
61
- shuffled = [...row1, ...row2];
62
- }
63
- // Generate Inverted matches for home-away pairings
64
- matches.push(...matches.map(match => {
31
+ const nextDate = () => {
65
32
  const scheduledDate = date.toDate();
66
- date = date.add(30, 'minutes');
67
- return match_1.Match.create({
68
- id: (0, uuid_1.v4)(),
69
- homeTeam: match.awayTeam,
70
- awayTeam: match.homeTeam,
71
- scheduledDate,
72
- status: data_1.StatusEnum.PENDING
73
- });
33
+ date = date.add(MATCH_SPACING_MINUTES, 'minutes');
34
+ return scheduledDate;
35
+ };
36
+ // First leg: one pass of the round robin, matches laid out back to back in round order.
37
+ const firstLeg = (0, round_robin_1.roundRobinRounds)((0, utils_1.shuffle)([...teams])).flat().map(([homeTeam, awayTeam]) => match_1.Match.create({
38
+ id: (0, uuid_1.v4)(),
39
+ homeTeam,
40
+ awayTeam,
41
+ scheduledDate: nextDate(),
42
+ status: data_1.StatusEnum.PENDING
43
+ }));
44
+ // Second leg: the same fixtures with the sides swapped, so every pairing is played home and away.
45
+ const secondLeg = firstLeg.map(match => match_1.Match.create({
46
+ id: (0, uuid_1.v4)(),
47
+ homeTeam: match.awayTeam,
48
+ awayTeam: match.homeTeam,
49
+ scheduledDate: nextDate(),
50
+ status: data_1.StatusEnum.PENDING
74
51
  }));
75
- return matches;
52
+ return [...firstLeg, ...secondLeg];
76
53
  }
77
54
  }
78
55
  exports.default = MatchGenerator;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const globals_1 = require("@jest/globals");
7
+ const match_generator_1 = __importDefault(require("./match-generator"));
8
+ const test_helpers_1 = require("../test-helpers");
9
+ // The league season schedule. These assertions pin the behaviour the rollover depends on, so the shared
10
+ // roundRobinRounds helper (used by events too) cannot silently change how a season is laid out.
11
+ const MONDAY = new Date('2026-07-20T00:00:00.000Z');
12
+ function orderedKey(match) {
13
+ return `${match.homeTeam.id}>${match.awayTeam.id}`;
14
+ }
15
+ (0, globals_1.describe)('MatchGenerator.generateMatches()', () => {
16
+ (0, globals_1.it)('plays a full double round robin for a 16-team division', () => {
17
+ const teams = Array.from({ length: 16 }, () => (0, test_helpers_1.makeTeam)());
18
+ const matches = match_generator_1.default.generateMatches(teams, MONDAY);
19
+ // 16 teams x 15 opponents, home and away.
20
+ (0, globals_1.expect)(matches).toHaveLength(240);
21
+ const seen = new Set(matches.map(orderedKey));
22
+ (0, globals_1.expect)(seen.size).toBe(240);
23
+ // Every ordered pairing exists exactly once, so every team hosts and visits every other team.
24
+ for (const home of teams) {
25
+ for (const away of teams) {
26
+ if (home.id === away.id)
27
+ continue;
28
+ (0, globals_1.expect)(seen.has(`${home.id}>${away.id}`)).toBe(true);
29
+ }
30
+ }
31
+ });
32
+ (0, globals_1.it)('anchors to 00:00 UTC of the base day and spaces matches 30 minutes apart', () => {
33
+ const teams = Array.from({ length: 16 }, () => (0, test_helpers_1.makeTeam)());
34
+ const matches = match_generator_1.default.generateMatches(teams, new Date('2026-07-20T17:42:11.000Z'));
35
+ const times = matches.map(m => m.scheduledDate.getTime()).sort((a, b) => a - b);
36
+ (0, globals_1.expect)(new Date(times[0]).toISOString()).toBe('2026-07-20T00:00:00.000Z');
37
+ for (let i = 1; i < times.length; i++) {
38
+ (0, globals_1.expect)(times[i] - times[i - 1]).toBe(30 * 60 * 1000);
39
+ }
40
+ // 240 matches at half-hour spacing = five days, ending Friday 23:30 UTC.
41
+ (0, globals_1.expect)(new Date(times[times.length - 1]).toISOString()).toBe('2026-07-24T23:30:00.000Z');
42
+ });
43
+ (0, globals_1.it)('rejects a duplicated team and an empty division', () => {
44
+ const team = (0, test_helpers_1.makeTeam)();
45
+ (0, globals_1.expect)(() => match_generator_1.default.generateMatches([team, team], MONDAY)).toThrow('DUPLICATE_TEAMS');
46
+ (0, globals_1.expect)(() => match_generator_1.default.generateMatches([], MONDAY)).toThrow('EMPTY_TEAM_ARRAY');
47
+ });
48
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Circle-method single round robin. Returns one entry per round, each holding the pairings that round plays,
3
+ * as `[home, away]` tuples. Every unordered pair of entrants appears exactly once across all rounds.
4
+ *
5
+ * An ODD entrant count produces a bye: one entrant sits out each round, so a round has `floor(n / 2)` pairings
6
+ * instead of `n / 2`. Callers that need everyone playing every round must pad to an even count first.
7
+ *
8
+ * Home and away alternate by round and by court index, so no entrant ends up at home in all of its games. For a
9
+ * double round robin the caller replays the same rounds with the sides swapped, which balances them exactly.
10
+ */
11
+ export declare function roundRobinRounds<T>(entrants: T[]): Array<Array<[T, T]>>;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.roundRobinRounds = roundRobinRounds;
4
+ // A bye slot, used to keep the circle even when an odd number of entrants is passed. Pairings that involve it
5
+ // are dropped, so that entrant sits the round out. Module-level so the type is nameable inside the function.
6
+ const BYE = Symbol('ROUND_ROBIN_BYE');
7
+ /**
8
+ * Circle-method single round robin. Returns one entry per round, each holding the pairings that round plays,
9
+ * as `[home, away]` tuples. Every unordered pair of entrants appears exactly once across all rounds.
10
+ *
11
+ * An ODD entrant count produces a bye: one entrant sits out each round, so a round has `floor(n / 2)` pairings
12
+ * instead of `n / 2`. Callers that need everyone playing every round must pad to an even count first.
13
+ *
14
+ * Home and away alternate by round and by court index, so no entrant ends up at home in all of its games. For a
15
+ * double round robin the caller replays the same rounds with the sides swapped, which balances them exactly.
16
+ */
17
+ function roundRobinRounds(entrants) {
18
+ if (entrants.length < 2)
19
+ return [];
20
+ const wheel = entrants.length % 2 === 0 ? [...entrants] : [...entrants, BYE];
21
+ const size = wheel.length;
22
+ const half = size / 2;
23
+ const rounds = [];
24
+ // Index 0 stays put while the rest rotate one place per round; pairing i against size-1-i then yields every
25
+ // pair exactly once over size-1 rounds.
26
+ let circle = wheel;
27
+ for (let round = 0; round < size - 1; round++) {
28
+ const pairings = [];
29
+ for (let court = 0; court < half; court++) {
30
+ const a = circle[court];
31
+ const b = circle[size - 1 - court];
32
+ if (a === BYE || b === BYE)
33
+ continue;
34
+ // Flip the whole column every other round. A rotating entrant advances one place per round, so it spends
35
+ // consecutive rounds on the same side of the circle and this parity alternates its home/away within them.
36
+ // Folding the court index into the parity instead would keep it CONSTANT for that entrant (its index and
37
+ // the round number rise together), pinning some entrants to one side for the entire event.
38
+ pairings.push(round % 2 === 0 ? [a, b] : [b, a]);
39
+ }
40
+ rounds.push(pairings);
41
+ circle = [circle[0], circle[size - 1], ...circle.slice(1, size - 1)];
42
+ }
43
+ return rounds;
44
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const round_robin_1 = require("./round-robin");
4
+ function key(a, b) {
5
+ return a < b ? `${a}:${b}` : `${b}:${a}`;
6
+ }
7
+ function entrants(n) {
8
+ return Array.from({ length: n }, (_, i) => `t${i}`);
9
+ }
10
+ describe('roundRobinRounds()', () => {
11
+ it('returns nothing for fewer than two entrants', () => {
12
+ expect((0, round_robin_1.roundRobinRounds)([])).toEqual([]);
13
+ expect((0, round_robin_1.roundRobinRounds)(['solo'])).toEqual([]);
14
+ });
15
+ it.each([2, 4, 6, 8, 16, 20, 50])('pairs every even field of %i exactly once', (n) => {
16
+ const rounds = (0, round_robin_1.roundRobinRounds)(entrants(n));
17
+ expect(rounds).toHaveLength(n - 1);
18
+ rounds.forEach(round => expect(round).toHaveLength(n / 2));
19
+ const seen = new Set();
20
+ for (const round of rounds) {
21
+ // Nobody plays twice in the same round.
22
+ const playing = new Set();
23
+ for (const [home, away] of round) {
24
+ expect(home).not.toEqual(away);
25
+ expect(playing.has(home)).toBe(false);
26
+ expect(playing.has(away)).toBe(false);
27
+ playing.add(home);
28
+ playing.add(away);
29
+ const k = key(home, away);
30
+ expect(seen.has(k)).toBe(false);
31
+ seen.add(k);
32
+ }
33
+ expect(playing.size).toBe(n);
34
+ }
35
+ expect(seen.size).toBe((n * (n - 1)) / 2);
36
+ });
37
+ it.each([3, 5, 7, 15])('gives an odd field of %i one bye per round and still pairs everyone once', (n) => {
38
+ const rounds = (0, round_robin_1.roundRobinRounds)(entrants(n));
39
+ // An odd field runs the even-sized circle it was padded into: n rounds, one entrant idle in each.
40
+ expect(rounds).toHaveLength(n);
41
+ rounds.forEach(round => expect(round).toHaveLength((n - 1) / 2));
42
+ const seen = new Set();
43
+ const byes = new Map();
44
+ for (const round of rounds) {
45
+ const playing = new Set();
46
+ for (const [home, away] of round) {
47
+ seen.add(key(home, away));
48
+ playing.add(home);
49
+ playing.add(away);
50
+ }
51
+ const idle = entrants(n).filter(t => !playing.has(t));
52
+ expect(idle).toHaveLength(1);
53
+ byes.set(idle[0], (byes.get(idle[0]) ?? 0) + 1);
54
+ }
55
+ expect(seen.size).toBe((n * (n - 1)) / 2);
56
+ // Exactly one bye each: nobody sits out twice while another entrant never rests.
57
+ expect([...byes.values()]).toEqual(Array.from({ length: n }, () => 1));
58
+ });
59
+ it.each([8, 16, 20, 50])('splits home and away near evenly across a field of %i', (n) => {
60
+ const homeCounts = new Map();
61
+ for (const round of (0, round_robin_1.roundRobinRounds)(entrants(n))) {
62
+ for (const [home] of round)
63
+ homeCounts.set(home, (homeCounts.get(home) ?? 0) + 1);
64
+ }
65
+ // n-1 games each, so half of them is the ideal. Folding the court index into the side parity used to pin
66
+ // some entrants to a single side for the whole event; this guards that regression.
67
+ for (const t of entrants(n)) {
68
+ expect(Math.abs((homeCounts.get(t) ?? 0) - (n - 1) / 2)).toBeLessThanOrEqual(2);
69
+ }
70
+ });
71
+ });
@@ -37,5 +37,26 @@
37
37
  "generic": {
38
38
  "title": "World Aces",
39
39
  "body": "You have a new notification."
40
+ },
41
+ "eventRegistrationOpen": {
42
+ "title": "Event registration open",
43
+ "body": "A new event is open. Draft your squad from the packs before registration closes."
44
+ },
45
+ "eventRegistrationClosing": {
46
+ "title": "Registration closes soon",
47
+ "body": "Registration for the event closes in an hour. Draft your squad to take part."
48
+ },
49
+ "eventStarted": {
50
+ "title": "Event started",
51
+ "body": "Your event matches have begun. Good luck!"
52
+ },
53
+ "eventFinalRound": {
54
+ "title": "You reached the final",
55
+ "body": "You qualified for the final round. It starts now."
56
+ },
57
+ "eventFinished": {
58
+ "title": "Event finished",
59
+ "body": "The event is over. See the final table and your rewards.",
60
+ "placed": "The event is over. You finished {{position}} of {{entrants}}."
40
61
  }
41
62
  }
@@ -37,5 +37,26 @@
37
37
  "generic": {
38
38
  "title": "World Aces",
39
39
  "body": "Tienes una nueva notificación."
40
+ },
41
+ "eventRegistrationOpen": {
42
+ "title": "Inscripciones abiertas",
43
+ "body": "Hay un nuevo evento abierto. Arma tu plantilla con los sobres antes de que cierren las inscripciones."
44
+ },
45
+ "eventRegistrationClosing": {
46
+ "title": "Las inscripciones cierran pronto",
47
+ "body": "Las inscripciones del evento cierran en una hora. Arma tu plantilla para participar."
48
+ },
49
+ "eventStarted": {
50
+ "title": "El evento ha empezado",
51
+ "body": "Tus partidos del evento han comenzado. ¡Suerte!"
52
+ },
53
+ "eventFinalRound": {
54
+ "title": "Has llegado a la final",
55
+ "body": "Te has clasificado para la ronda final. Empieza ahora."
56
+ },
57
+ "eventFinished": {
58
+ "title": "Evento terminado",
59
+ "body": "El evento ha terminado. Mira la tabla final y tus recompensas.",
60
+ "placed": "El evento ha terminado. Has quedado {{position}} de {{entrants}}."
40
61
  }
41
62
  }
@@ -37,5 +37,26 @@
37
37
  "generic": {
38
38
  "title": "World Aces",
39
39
  "body": "Vous avez une nouvelle notification."
40
+ },
41
+ "eventRegistrationOpen": {
42
+ "title": "Inscriptions ouvertes",
43
+ "body": "Un nouvel événement est ouvert. Compose ton effectif avec les packs avant la clôture des inscriptions."
44
+ },
45
+ "eventRegistrationClosing": {
46
+ "title": "Les inscriptions ferment bientôt",
47
+ "body": "Les inscriptions à l'événement ferment dans une heure. Compose ton effectif pour participer."
48
+ },
49
+ "eventStarted": {
50
+ "title": "L'événement a commencé",
51
+ "body": "Tes matchs ont commencé. Bonne chance !"
52
+ },
53
+ "eventFinalRound": {
54
+ "title": "Tu es en finale",
55
+ "body": "Tu es qualifié pour la finale. Elle commence maintenant."
56
+ },
57
+ "eventFinished": {
58
+ "title": "Événement terminé",
59
+ "body": "L'événement est terminé. Consulte le classement final et tes récompenses.",
60
+ "placed": "L'événement est terminé. Tu finis {{position}} sur {{entrants}}."
40
61
  }
41
62
  }
@@ -37,5 +37,26 @@
37
37
  "generic": {
38
38
  "title": "World Aces",
39
39
  "body": "Hai una nuova notifica."
40
+ },
41
+ "eventRegistrationOpen": {
42
+ "title": "Iscrizioni aperte",
43
+ "body": "Un nuovo evento è aperto. Costruisci la rosa dai pacchetti prima che chiudano le iscrizioni."
44
+ },
45
+ "eventRegistrationClosing": {
46
+ "title": "Le iscrizioni stanno per chiudere",
47
+ "body": "Le iscrizioni all'evento chiudono tra un'ora. Costruisci la rosa per partecipare."
48
+ },
49
+ "eventStarted": {
50
+ "title": "L'evento è iniziato",
51
+ "body": "Le tue partite sono iniziate. In bocca al lupo!"
52
+ },
53
+ "eventFinalRound": {
54
+ "title": "Sei in finale",
55
+ "body": "Ti sei qualificato per la finale. Inizia ora."
56
+ },
57
+ "eventFinished": {
58
+ "title": "Evento concluso",
59
+ "body": "L'evento è finito. Guarda la classifica finale e i tuoi premi.",
60
+ "placed": "L'evento è finito. Hai chiuso {{position}} su {{entrants}}."
40
61
  }
41
62
  }
@@ -37,5 +37,26 @@
37
37
  "generic": {
38
38
  "title": "World Aces",
39
39
  "body": "Masz nowe powiadomienie."
40
+ },
41
+ "eventRegistrationOpen": {
42
+ "title": "Zapisy otwarte",
43
+ "body": "Nowe wydarzenie jest otwarte. Zbuduj skład z paczek, zanim zamkną się zapisy."
44
+ },
45
+ "eventRegistrationClosing": {
46
+ "title": "Zapisy wkrótce się zamkną",
47
+ "body": "Zapisy na wydarzenie zamykają się za godzinę. Zbuduj skład, aby wziąć udział."
48
+ },
49
+ "eventStarted": {
50
+ "title": "Wydarzenie ruszyło",
51
+ "body": "Twoje mecze się rozpoczęły. Powodzenia!"
52
+ },
53
+ "eventFinalRound": {
54
+ "title": "Jesteś w finale",
55
+ "body": "Awansowałeś do rundy finałowej. Zaczyna się teraz."
56
+ },
57
+ "eventFinished": {
58
+ "title": "Wydarzenie zakończone",
59
+ "body": "Wydarzenie się skończyło. Zobacz tabelę końcową i nagrody.",
60
+ "placed": "Wydarzenie się skończyło. Zająłeś {{position}} miejsce na {{entrants}}."
40
61
  }
41
62
  }
@@ -37,5 +37,26 @@
37
37
  "generic": {
38
38
  "title": "World Aces",
39
39
  "body": "Você tem uma nova notificação."
40
+ },
41
+ "eventRegistrationOpen": {
42
+ "title": "Inscrições abertas",
43
+ "body": "Um novo evento está aberto. Monte seu elenco com os pacotes antes das inscrições fecharem."
44
+ },
45
+ "eventRegistrationClosing": {
46
+ "title": "As inscrições fecham em breve",
47
+ "body": "As inscrições do evento fecham em uma hora. Monte seu elenco para participar."
48
+ },
49
+ "eventStarted": {
50
+ "title": "O evento começou",
51
+ "body": "Suas partidas do evento começaram. Boa sorte!"
52
+ },
53
+ "eventFinalRound": {
54
+ "title": "Você chegou à final",
55
+ "body": "Você se classificou para a rodada final. Começa agora."
56
+ },
57
+ "eventFinished": {
58
+ "title": "Evento encerrado",
59
+ "body": "O evento acabou. Veja a tabela final e suas recompensas.",
60
+ "placed": "O evento acabou. Você terminou em {{position}} de {{entrants}}."
40
61
  }
41
62
  }
@@ -37,5 +37,26 @@
37
37
  "generic": {
38
38
  "title": "World Aces",
39
39
  "body": "Yeni bir bildirimin var."
40
+ },
41
+ "eventRegistrationOpen": {
42
+ "title": "Kayıtlar açıldı",
43
+ "body": "Yeni bir etkinlik açıldı. Kayıtlar kapanmadan paketlerden kadronu kur."
44
+ },
45
+ "eventRegistrationClosing": {
46
+ "title": "Kayıtlar birazdan kapanıyor",
47
+ "body": "Etkinlik kayıtları bir saat içinde kapanıyor. Katılmak için kadronu kur."
48
+ },
49
+ "eventStarted": {
50
+ "title": "Etkinlik başladı",
51
+ "body": "Etkinlik maçların başladı. Bol şans!"
52
+ },
53
+ "eventFinalRound": {
54
+ "title": "Finale kaldın",
55
+ "body": "Final turuna kaldın. Şimdi başlıyor."
56
+ },
57
+ "eventFinished": {
58
+ "title": "Etkinlik bitti",
59
+ "body": "Etkinlik sona erdi. Final tablosuna ve ödüllerine bak.",
60
+ "placed": "Etkinlik sona erdi. {{entrants}} takım arasında {{position}}. oldun."
40
61
  }
41
62
  }
@@ -1,4 +1,4 @@
1
- export declare const NOTIFICATION_TYPES: readonly ["MATCH_RESULT", "INJURY", "INJURY_RECOVERED", "SEASON_END", "GRANT_RECEIVED", "PROMOTION", "DAILY_LOGIN", "WELCOME", "COMPENSATION"];
1
+ export declare const NOTIFICATION_TYPES: readonly ["MATCH_RESULT", "INJURY", "INJURY_RECOVERED", "SEASON_END", "GRANT_RECEIVED", "PROMOTION", "DAILY_LOGIN", "WELCOME", "COMPENSATION", "EVENT_REGISTRATION_OPEN", "EVENT_REGISTRATION_CLOSING", "EVENT_STARTED", "EVENT_FINAL_ROUND", "EVENT_FINISHED"];
2
2
  export type NotificationType = typeof NOTIFICATION_TYPES[number];
3
3
  export declare const NOTIFICATION_LOCALES: readonly ["en", "es", "fr", "it", "pl", "pt-BR", "tr"];
4
4
  export type NotificationLocale = typeof NOTIFICATION_LOCALES[number];
@@ -16,7 +16,12 @@ exports.NOTIFICATION_TYPES = [
16
16
  'PROMOTION',
17
17
  'DAILY_LOGIN',
18
18
  'WELCOME',
19
- 'COMPENSATION'
19
+ 'COMPENSATION',
20
+ 'EVENT_REGISTRATION_OPEN',
21
+ 'EVENT_REGISTRATION_CLOSING',
22
+ 'EVENT_STARTED',
23
+ 'EVENT_FINAL_ROUND',
24
+ 'EVENT_FINISHED'
20
25
  ];
21
26
  // The 7 supported locales. Must stay in sync with the API VALID_LOCALES set and the UI src/locales/*.json.
22
27
  exports.NOTIFICATION_LOCALES = ['en', 'es', 'fr', 'it', 'pl', 'pt-BR', 'tr'];
@@ -132,7 +132,9 @@ function flattenStrings(node, prefix = '') {
132
132
  .toEqual(notification_type_1.NOTIFICATION_TYPES.filter((t) => registry_1.NOTIFICATION_REGISTRY[t].emitsPush).sort((a, b) => a.localeCompare(b)));
133
133
  (0, globals_1.expect)(registry_1.PUSH_NOTIFICATION_TYPES).not.toContain('COMPENSATION');
134
134
  (0, globals_1.expect)(registry_1.PUSH_NOTIFICATION_TYPES).toContain('MATCH_RESULT');
135
- (0, globals_1.expect)(registry_1.PUSH_NOTIFICATION_TYPES).toHaveLength(8);
135
+ // COMPENSATION is the ONLY in-app-only type, so every other type pushes. Expressed as a relationship
136
+ // rather than a count, so adding a type does not need this number edited.
137
+ (0, globals_1.expect)(registry_1.PUSH_NOTIFICATION_TYPES).toHaveLength(notification_type_1.NOTIFICATION_TYPES.length - 1);
136
138
  });
137
139
  (0, globals_1.it)('is opt-out: absent / null / empty / true all enable, only explicit false suppresses', () => {
138
140
  (0, globals_1.expect)((0, notification_type_1.pushEnabledFor)(undefined, 'MATCH_RESULT')).toBe(true);
@@ -88,6 +88,58 @@ exports.NOTIFICATION_REGISTRY = {
88
88
  inAppMessageKey: 'promotion.body',
89
89
  catalogKeys: ['promotion.title', 'promotion.body']
90
90
  },
91
+ // In-game events. All five deep-link to the Events section, which is where every phase of an event lives.
92
+ // eventName is the event's key; the UI resolves it to the translated name, so the catalog copy interpolates
93
+ // a key rather than a localised string the sender would have to translate.
94
+ EVENT_REGISTRATION_OPEN: {
95
+ emitsPush: true,
96
+ deepLink: () => '/events',
97
+ pushTitleKey: 'eventRegistrationOpen.title',
98
+ pushBodyKey: 'eventRegistrationOpen.body',
99
+ inAppMessageKey: 'eventRegistrationOpen.body',
100
+ catalogKeys: ['eventRegistrationOpen.title', 'eventRegistrationOpen.body']
101
+ },
102
+ EVENT_REGISTRATION_CLOSING: {
103
+ emitsPush: true,
104
+ deepLink: () => '/events',
105
+ pushTitleKey: 'eventRegistrationClosing.title',
106
+ pushBodyKey: 'eventRegistrationClosing.body',
107
+ inAppMessageKey: 'eventRegistrationClosing.body',
108
+ catalogKeys: ['eventRegistrationClosing.title', 'eventRegistrationClosing.body']
109
+ },
110
+ EVENT_STARTED: {
111
+ emitsPush: true,
112
+ deepLink: () => '/events',
113
+ pushTitleKey: 'eventStarted.title',
114
+ pushBodyKey: 'eventStarted.body',
115
+ inAppMessageKey: 'eventStarted.body',
116
+ catalogKeys: ['eventStarted.title', 'eventStarted.body']
117
+ },
118
+ EVENT_FINAL_ROUND: {
119
+ emitsPush: true,
120
+ deepLink: () => '/events',
121
+ pushTitleKey: 'eventFinalRound.title',
122
+ pushBodyKey: 'eventFinalRound.body',
123
+ inAppMessageKey: 'eventFinalRound.body',
124
+ catalogKeys: ['eventFinalRound.title', 'eventFinalRound.body']
125
+ },
126
+ EVENT_FINISHED: {
127
+ emitsPush: true,
128
+ deepLink: () => '/events',
129
+ pushTitleKey: 'eventFinished.title',
130
+ // Copy depends on whether we know where they finished; a missing position never renders "You finished th".
131
+ pushBodyKey: (p) => readString(p.position) != null || typeof p.position === 'number'
132
+ ? 'eventFinished.placed'
133
+ : 'eventFinished.body',
134
+ inAppMessageKey: (p) => readString(p.position) != null || typeof p.position === 'number'
135
+ ? 'eventFinished.placed'
136
+ : 'eventFinished.body',
137
+ params: (p) => ({
138
+ position: typeof p.position === 'number' ? String(p.position) : (readString(p.position) ?? ''),
139
+ entrants: typeof p.entrants === 'number' ? String(p.entrants) : (readString(p.entrants) ?? '')
140
+ }),
141
+ catalogKeys: ['eventFinished.title', 'eventFinished.body', 'eventFinished.placed']
142
+ },
91
143
  DAILY_LOGIN: {
92
144
  emitsPush: true,
93
145
  // Fires when the reward becomes claimable (00:00 UTC), so it opens the daily-login claim view.
@@ -35,6 +35,7 @@ export interface GachaPullConfig {
35
35
  }
36
36
  export declare const NORMAL_PULL_CONFIG: GachaPullConfig;
37
37
  export declare const QUALIFIER_PULL_CONFIG: GachaPullConfig;
38
+ export declare const EVENT_PULL_CONFIG: GachaPullConfig;
38
39
  export declare const TOURNAMENT_PULL_CONFIG: GachaPullConfig;
39
40
  export declare function scoutPullConfig(level: string): GachaPullConfig;
40
41
  export declare function rollRarity(pity: PityState, config?: GachaPullConfig): RollResult;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PlayerGenerator = exports.TOURNAMENT_PULL_CONFIG = exports.QUALIFIER_PULL_CONFIG = exports.NORMAL_PULL_CONFIG = exports.PITY_STEP = exports.PITY_THRESHOLDS = void 0;
3
+ exports.PlayerGenerator = exports.TOURNAMENT_PULL_CONFIG = exports.EVENT_PULL_CONFIG = exports.QUALIFIER_PULL_CONFIG = exports.NORMAL_PULL_CONFIG = exports.PITY_STEP = exports.PITY_THRESHOLDS = void 0;
4
4
  exports.scoutPullConfig = scoutPullConfig;
5
5
  exports.rollRarity = rollRarity;
6
6
  exports.pickPlayerCountry = pickPlayerCountry;
@@ -69,6 +69,21 @@ exports.QUALIFIER_PULL_CONFIG = {
69
69
  accumulateMythicPity: true,
70
70
  accumulateSpecialPity: true
71
71
  };
72
+ // Event Pack (x5): the NATIONAL pool and structure, with TRIPLE the legendary and mythic chance. Special is
73
+ // untouched and rare absorbs the difference, so it still sums to 1. Builds all three pities, like NATIONAL.
74
+ // Owner-defined 2026-07-23.
75
+ exports.EVENT_PULL_CONFIG = {
76
+ special: 0.003,
77
+ mythic: 0.03,
78
+ legendary: 0.30,
79
+ rare: 0.667,
80
+ applyLegendaryPity: true,
81
+ applyMythicPity: true,
82
+ applySpecialPity: true,
83
+ accumulateLegendaryPity: true,
84
+ accumulateMythicPity: true,
85
+ accumulateSpecialPity: true
86
+ };
72
87
  // Tournament gacha (x5 per match): LEGENDARY–SPECIAL, no legendary pity (floor is legendary)
73
88
  exports.TOURNAMENT_PULL_CONFIG = {
74
89
  special: 0.01,
@@ -1,5 +1,7 @@
1
1
  import { LineupFunction } from './lineup-function';
2
+ export declare const NO_BLOCK: "NO_BLOCK";
3
+ export type BlockingAssignment = LineupFunction | typeof NO_BLOCK;
2
4
  export interface BlockingPreference {
3
5
  readonly rotation: number;
4
- readonly targets: Record<string, LineupFunction>;
6
+ readonly targets: Record<string, BlockingAssignment>;
5
7
  }