volleyballsimtypes 0.0.491 → 0.0.493

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 (135) hide show
  1. package/dist/cjs/src/api/index.d.ts +2 -1
  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/index.d.ts +1 -0
  31. package/dist/cjs/src/index.js +6 -0
  32. package/dist/cjs/src/service/competition/event-draft.d.ts +40 -0
  33. package/dist/cjs/src/service/competition/event-draft.js +68 -0
  34. package/dist/cjs/src/service/competition/event-draft.test.d.ts +1 -0
  35. package/dist/cjs/src/service/competition/event-draft.test.js +118 -0
  36. package/dist/cjs/src/service/competition/event-rewards.d.ts +26 -0
  37. package/dist/cjs/src/service/competition/event-rewards.js +49 -0
  38. package/dist/cjs/src/service/competition/event-rewards.test.d.ts +1 -0
  39. package/dist/cjs/src/service/competition/event-rewards.test.js +58 -0
  40. package/dist/cjs/src/service/competition/event-schedule.d.ts +126 -0
  41. package/dist/cjs/src/service/competition/event-schedule.js +141 -0
  42. package/dist/cjs/src/service/competition/event-schedule.test.d.ts +1 -0
  43. package/dist/cjs/src/service/competition/event-schedule.test.js +138 -0
  44. package/dist/cjs/src/service/competition/index.d.ts +3 -0
  45. package/dist/cjs/src/service/competition/index.js +3 -0
  46. package/dist/cjs/src/service/match/index.d.ts +1 -0
  47. package/dist/cjs/src/service/match/index.js +1 -0
  48. package/dist/cjs/src/service/match/match-generator.js +25 -48
  49. package/dist/cjs/src/service/match/match-generator.test.d.ts +1 -0
  50. package/dist/cjs/src/service/match/match-generator.test.js +48 -0
  51. package/dist/cjs/src/service/match/round-robin.d.ts +11 -0
  52. package/dist/cjs/src/service/match/round-robin.js +44 -0
  53. package/dist/cjs/src/service/match/round-robin.test.d.ts +1 -0
  54. package/dist/cjs/src/service/match/round-robin.test.js +71 -0
  55. package/dist/cjs/src/service/notification/catalog/en.json +21 -0
  56. package/dist/cjs/src/service/notification/catalog/es.json +21 -0
  57. package/dist/cjs/src/service/notification/catalog/fr.json +21 -0
  58. package/dist/cjs/src/service/notification/catalog/it.json +21 -0
  59. package/dist/cjs/src/service/notification/catalog/pl.json +21 -0
  60. package/dist/cjs/src/service/notification/catalog/pt-BR.json +21 -0
  61. package/dist/cjs/src/service/notification/catalog/tr.json +21 -0
  62. package/dist/cjs/src/service/notification/notification-type.d.ts +1 -1
  63. package/dist/cjs/src/service/notification/notification-type.js +6 -1
  64. package/dist/cjs/src/service/notification/notification.test.js +3 -1
  65. package/dist/cjs/src/service/notification/registry.js +52 -0
  66. package/dist/cjs/src/service/player/player-generator.d.ts +2 -1
  67. package/dist/cjs/src/service/player/player-generator.js +37 -5
  68. package/dist/esm/src/api/index.d.ts +2 -1
  69. package/dist/esm/src/api/index.js +1 -0
  70. package/dist/esm/src/api/tactics-player-refs.d.ts +13 -0
  71. package/dist/esm/src/api/tactics-player-refs.js +40 -0
  72. package/dist/esm/src/data/event-purge.d.ts +14 -0
  73. package/dist/esm/src/data/event-purge.js +110 -0
  74. package/dist/esm/src/data/index.d.ts +1 -0
  75. package/dist/esm/src/data/index.js +1 -0
  76. package/dist/esm/src/data/init-models.js +42 -1
  77. package/dist/esm/src/data/models/competition.d.ts +1 -1
  78. package/dist/esm/src/data/models/competition.js +1 -1
  79. package/dist/esm/src/data/models/event-bot-team.d.ts +21 -0
  80. package/dist/esm/src/data/models/event-bot-team.js +40 -0
  81. package/dist/esm/src/data/models/event-competition.d.ts +29 -0
  82. package/dist/esm/src/data/models/event-competition.js +48 -0
  83. package/dist/esm/src/data/models/event-pack-player.d.ts +22 -0
  84. package/dist/esm/src/data/models/event-pack-player.js +27 -0
  85. package/dist/esm/src/data/models/event-pack.d.ts +37 -0
  86. package/dist/esm/src/data/models/event-pack.js +51 -0
  87. package/dist/esm/src/data/models/event-result.d.ts +34 -0
  88. package/dist/esm/src/data/models/event-result.js +44 -0
  89. package/dist/esm/src/data/models/event-squad.d.ts +30 -0
  90. package/dist/esm/src/data/models/event-squad.js +42 -0
  91. package/dist/esm/src/data/models/event.d.ts +51 -0
  92. package/dist/esm/src/data/models/event.js +78 -0
  93. package/dist/esm/src/data/models/index.d.ts +7 -0
  94. package/dist/esm/src/data/models/index.js +7 -0
  95. package/dist/esm/src/data/models/lineup-preset.d.ts +3 -1
  96. package/dist/esm/src/data/models/lineup-preset.js +20 -0
  97. package/dist/esm/src/index.d.ts +1 -0
  98. package/dist/esm/src/index.js +4 -0
  99. package/dist/esm/src/service/competition/event-draft.d.ts +40 -0
  100. package/dist/esm/src/service/competition/event-draft.js +62 -0
  101. package/dist/esm/src/service/competition/event-draft.test.d.ts +1 -0
  102. package/dist/esm/src/service/competition/event-draft.test.js +116 -0
  103. package/dist/esm/src/service/competition/event-rewards.d.ts +26 -0
  104. package/dist/esm/src/service/competition/event-rewards.js +44 -0
  105. package/dist/esm/src/service/competition/event-rewards.test.d.ts +1 -0
  106. package/dist/esm/src/service/competition/event-rewards.test.js +56 -0
  107. package/dist/esm/src/service/competition/event-schedule.d.ts +126 -0
  108. package/dist/esm/src/service/competition/event-schedule.js +129 -0
  109. package/dist/esm/src/service/competition/event-schedule.test.d.ts +1 -0
  110. package/dist/esm/src/service/competition/event-schedule.test.js +136 -0
  111. package/dist/esm/src/service/competition/index.d.ts +3 -0
  112. package/dist/esm/src/service/competition/index.js +3 -0
  113. package/dist/esm/src/service/match/index.d.ts +1 -0
  114. package/dist/esm/src/service/match/index.js +1 -0
  115. package/dist/esm/src/service/match/match-generator.js +25 -48
  116. package/dist/esm/src/service/match/match-generator.test.d.ts +1 -0
  117. package/dist/esm/src/service/match/match-generator.test.js +43 -0
  118. package/dist/esm/src/service/match/round-robin.d.ts +11 -0
  119. package/dist/esm/src/service/match/round-robin.js +41 -0
  120. package/dist/esm/src/service/match/round-robin.test.d.ts +1 -0
  121. package/dist/esm/src/service/match/round-robin.test.js +69 -0
  122. package/dist/esm/src/service/notification/catalog/en.json +21 -0
  123. package/dist/esm/src/service/notification/catalog/es.json +21 -0
  124. package/dist/esm/src/service/notification/catalog/fr.json +21 -0
  125. package/dist/esm/src/service/notification/catalog/it.json +21 -0
  126. package/dist/esm/src/service/notification/catalog/pl.json +21 -0
  127. package/dist/esm/src/service/notification/catalog/pt-BR.json +21 -0
  128. package/dist/esm/src/service/notification/catalog/tr.json +21 -0
  129. package/dist/esm/src/service/notification/notification-type.d.ts +1 -1
  130. package/dist/esm/src/service/notification/notification-type.js +6 -1
  131. package/dist/esm/src/service/notification/notification.test.js +3 -1
  132. package/dist/esm/src/service/notification/registry.js +52 -0
  133. package/dist/esm/src/service/player/player-generator.d.ts +2 -1
  134. package/dist/esm/src/service/player/player-generator.js +36 -4
  135. package/package.json +6 -1
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const event_draft_1 = require("./event-draft");
5
+ const player_1 = require("../player");
6
+ const test_helpers_1 = require("../test-helpers");
7
+ const COUNTRY = (0, test_helpers_1.makeCountry)();
8
+ const first = (roles) => roles[0];
9
+ // Aggregate helpers: skew is statistical, so the behaviour tests use large samples and generous margins to stay
10
+ // off the flaky edge while still failing loudly if the skew were removed.
11
+ function median(values) {
12
+ const sorted = [...values].sort((a, b) => a - b);
13
+ return sorted[Math.floor(sorted.length / 2)];
14
+ }
15
+ function percentile(values, p) {
16
+ const sorted = [...values].sort((a, b) => a - b);
17
+ return sorted[Math.floor((sorted.length - 1) * p)];
18
+ }
19
+ /** Role scores of `n` recruits drawn the way the DRAFT does (generatePack applies EVENT_DRAFT_TOP_SKEW). */
20
+ function draftScores(rarity, role, n) {
21
+ const out = [];
22
+ while (out.length < n) {
23
+ for (const p of (0, event_draft_1.generatePack)(COUNTRY, rarity, role, first))
24
+ out.push((0, player_1.calculateRoleScore)(p.stats, role));
25
+ }
26
+ return out.slice(0, n);
27
+ }
28
+ /** Role scores of `n` recruits drawn UNIFORMLY (the generator's historic behaviour, skew 0). */
29
+ function uniformScores(rarity, role, n) {
30
+ return Array.from({ length: n }, () => (0, player_1.calculateRoleScore)(player_1.PlayerGenerator.generatePlayer(COUNTRY, rarity, role, false, 1, 0).stats, role));
31
+ }
32
+ (0, globals_1.describe)('DRAFT_PLAN', () => {
33
+ (0, globals_1.it)('drafts a full 14-player squad, one pick per pack', () => {
34
+ (0, globals_1.expect)(event_draft_1.DRAFT_SQUAD_SIZE).toBe(14);
35
+ (0, globals_1.expect)(event_draft_1.DRAFT_PLAN).toHaveLength(14);
36
+ });
37
+ (0, globals_1.it)('guarantees a positionally valid squad', () => {
38
+ const counts = event_draft_1.DRAFT_PLAN.reduce((acc, role) => {
39
+ acc[role] = (acc[role] ?? 0) + 1;
40
+ return acc;
41
+ }, {});
42
+ (0, globals_1.expect)(counts[player_1.RoleEnum.SETTER]).toBe(2);
43
+ (0, globals_1.expect)(counts[player_1.RoleEnum.OUTSIDE_HITTER]).toBe(4);
44
+ (0, globals_1.expect)(counts[player_1.RoleEnum.MIDDLE_BLOCKER]).toBe(3);
45
+ (0, globals_1.expect)(counts[player_1.RoleEnum.OPPOSITE_HITTER]).toBe(2);
46
+ (0, globals_1.expect)(counts[player_1.RoleEnum.LIBERO]).toBe(2);
47
+ (0, globals_1.expect)(counts.ANY).toBe(1);
48
+ // Enough for a legal starting six plus a libero, whatever the free pack is spent on: a 5-1 needs a setter,
49
+ // two middles, two outsides and an opposite on court, and a libero off it.
50
+ (0, globals_1.expect)(counts[player_1.RoleEnum.SETTER]).toBeGreaterThanOrEqual(1);
51
+ (0, globals_1.expect)(counts[player_1.RoleEnum.MIDDLE_BLOCKER]).toBeGreaterThanOrEqual(2);
52
+ (0, globals_1.expect)(counts[player_1.RoleEnum.OUTSIDE_HITTER]).toBeGreaterThanOrEqual(2);
53
+ (0, globals_1.expect)(counts[player_1.RoleEnum.OPPOSITE_HITTER]).toBeGreaterThanOrEqual(1);
54
+ (0, globals_1.expect)(counts[player_1.RoleEnum.LIBERO]).toBeGreaterThanOrEqual(1);
55
+ });
56
+ (0, globals_1.it)('walks pack by pack and then reports the draft complete', () => {
57
+ for (let i = 0; i < event_draft_1.DRAFT_SQUAD_SIZE; i++) {
58
+ (0, globals_1.expect)((0, event_draft_1.draftPackRole)(i)).toBeDefined();
59
+ (0, globals_1.expect)((0, event_draft_1.isDraftComplete)(i)).toBe(false);
60
+ }
61
+ (0, globals_1.expect)((0, event_draft_1.draftPackRole)(event_draft_1.DRAFT_SQUAD_SIZE)).toBeUndefined();
62
+ (0, globals_1.expect)((0, event_draft_1.isDraftComplete)(event_draft_1.DRAFT_SQUAD_SIZE)).toBe(true);
63
+ });
64
+ });
65
+ (0, globals_1.describe)('generatePack()', () => {
66
+ globals_1.it.each([player_1.RarityEnum.COMMON, player_1.RarityEnum.RARE, player_1.RarityEnum.LEGENDARY])('offers %s players of the pack role', (rarity) => {
67
+ const role = player_1.RoleEnum.OUTSIDE_HITTER;
68
+ const pack = (0, event_draft_1.generatePack)(COUNTRY, rarity, role, first);
69
+ (0, globals_1.expect)(pack).toHaveLength(event_draft_1.PACK_SIZE);
70
+ for (const player of pack) {
71
+ (0, globals_1.expect)(player.rarity).toBe(rarity);
72
+ (0, globals_1.expect)(player.roles).toContain(role);
73
+ }
74
+ });
75
+ (0, globals_1.it)('rolls a free (ANY) pack at the role it is handed', () => {
76
+ const pack = (0, event_draft_1.generatePack)(COUNTRY, player_1.RarityEnum.COMMON, 'ANY', () => player_1.RoleEnum.LIBERO);
77
+ (0, globals_1.expect)(pack).toHaveLength(event_draft_1.PACK_SIZE);
78
+ for (const player of pack)
79
+ (0, globals_1.expect)(player.roles).toContain(player_1.RoleEnum.LIBERO);
80
+ });
81
+ });
82
+ (0, globals_1.describe)('EVENT_DRAFT_TOP_SKEW', () => {
83
+ (0, globals_1.it)('skews commons and rares toward the top and leaves legendaries (and up) uniform', () => {
84
+ (0, globals_1.expect)(event_draft_1.EVENT_DRAFT_TOP_SKEW[player_1.RarityEnum.COMMON]).toBeGreaterThan(0);
85
+ (0, globals_1.expect)(event_draft_1.EVENT_DRAFT_TOP_SKEW[player_1.RarityEnum.RARE]).toBeGreaterThan(0);
86
+ (0, globals_1.expect)(event_draft_1.EVENT_DRAFT_TOP_SKEW[player_1.RarityEnum.LEGENDARY]).toBe(0);
87
+ (0, globals_1.expect)(event_draft_1.EVENT_DRAFT_TOP_SKEW[player_1.RarityEnum.MYTHIC]).toBe(0);
88
+ (0, globals_1.expect)(event_draft_1.EVENT_DRAFT_TOP_SKEW[player_1.RarityEnum.SPECIAL]).toBe(0);
89
+ });
90
+ });
91
+ (0, globals_1.describe)('draft skew behaviour', () => {
92
+ const N = 2500;
93
+ const role = player_1.RoleEnum.OUTSIDE_HITTER;
94
+ globals_1.it.each([player_1.RarityEnum.COMMON, player_1.RarityEnum.RARE])('draws %s recruits near the top: the typical recruit outscores a uniform draw, and most land in its top fifth', (rarity) => {
95
+ const uniform = uniformScores(rarity, role, N);
96
+ const drafted = draftScores(rarity, role, N);
97
+ // The 80th percentile of a UNIFORM draw is the entry to the top fifth of the range.
98
+ const topFifthFloor = percentile(uniform, 0.80);
99
+ const draftedInTopFifth = drafted.filter(s => s >= topFifthFloor).length / drafted.length;
100
+ // A uniform draw puts ~20% here; the skew must push well past that (measured ~65-88% at skew 2).
101
+ (0, globals_1.expect)(draftedInTopFifth).toBeGreaterThan(0.45);
102
+ // The typical drafted recruit clearly outscores the typical uniform one.
103
+ (0, globals_1.expect)(median(drafted)).toBeGreaterThan(median(uniform));
104
+ // But it is a skew, not a clamp to the maximum: recruits still spread out, and the typical one sits
105
+ // below the best in the sample rather than everyone landing on the ceiling.
106
+ (0, globals_1.expect)(percentile(drafted, 0.10)).toBeLessThan(percentile(drafted, 0.90));
107
+ (0, globals_1.expect)(median(drafted)).toBeLessThan(Math.max(...drafted));
108
+ });
109
+ (0, globals_1.it)('leaves legendary recruits uniform: no top-end lift over a plain draw', () => {
110
+ const uniform = uniformScores(player_1.RarityEnum.LEGENDARY, role, N);
111
+ const drafted = draftScores(player_1.RarityEnum.LEGENDARY, role, N);
112
+ const topFifthFloor = percentile(uniform, 0.80);
113
+ const draftedInTopFifth = drafted.filter(s => s >= topFifthFloor).length / drafted.length;
114
+ // Same distribution as uniform (skew 0), so it stays near the ~20% a plain draw produces, nowhere near the
115
+ // >45% the skewed lower rarities reach.
116
+ (0, globals_1.expect)(draftedInTopFifth).toBeLessThan(0.35);
117
+ });
118
+ });
@@ -0,0 +1,26 @@
1
+ export type EventRewardBand = 'PARTICIPATION' | 'TOP_20' | 'TOP_10' | 'TOP_5' | 'TOP_3' | 'TOP_1';
2
+ export interface EventRewardRow {
3
+ readonly band: EventRewardBand;
4
+ /** Best final position that earns this row; PARTICIPATION has none. */
5
+ readonly maxPosition: number | null;
6
+ /** Event Packs awarded by this row alone. */
7
+ readonly packs: number;
8
+ /** Coins awarded by this row alone. */
9
+ readonly coins: number;
10
+ }
11
+ /**
12
+ * Best band first. A team earns its own row and every row BELOW it, which is what "cumulative" means: first
13
+ * place collects all six.
14
+ */
15
+ export declare const EVENT_REWARDS: readonly EventRewardRow[];
16
+ /**
17
+ * The band a finish falls into. `position` is the place in the FINAL table; null means the team never reached
18
+ * the final, and so does any position past the last banded row, which both count as taking part.
19
+ */
20
+ export declare function eventRewardBandFor(position: number | null): EventRewardBand;
21
+ export interface EventPayout {
22
+ readonly packs: number;
23
+ readonly coins: number;
24
+ }
25
+ /** Everything a finish earns, adding its own row to every row beneath it. */
26
+ export declare function eventPayoutFor(position: number | null): EventPayout;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ // What an event pays out, and the band a team's finish falls into. Owner-defined 2026-07-23.
3
+ //
4
+ // One source of truth for three consumers: the simulator grants from it, the About view lists it, and the
5
+ // accolades page names the band. Rewards are CUMULATIVE, so a team collects every row from Participation down
6
+ // to its own placement.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.EVENT_REWARDS = void 0;
9
+ exports.eventRewardBandFor = eventRewardBandFor;
10
+ exports.eventPayoutFor = eventPayoutFor;
11
+ /**
12
+ * Best band first. A team earns its own row and every row BELOW it, which is what "cumulative" means: first
13
+ * place collects all six.
14
+ */
15
+ exports.EVENT_REWARDS = [
16
+ { band: 'TOP_1', maxPosition: 1, packs: 1, coins: 0 },
17
+ { band: 'TOP_3', maxPosition: 3, packs: 0, coins: 500 },
18
+ { band: 'TOP_5', maxPosition: 5, packs: 0, coins: 1000 },
19
+ { band: 'TOP_10', maxPosition: 10, packs: 1, coins: 0 },
20
+ { band: 'TOP_20', maxPosition: 20, packs: 0, coins: 1000 },
21
+ { band: 'PARTICIPATION', maxPosition: null, packs: 1, coins: 1000 }
22
+ ];
23
+ /**
24
+ * The band a finish falls into. `position` is the place in the FINAL table; null means the team never reached
25
+ * the final, and so does any position past the last banded row, which both count as taking part.
26
+ */
27
+ function eventRewardBandFor(position) {
28
+ if (position == null)
29
+ return 'PARTICIPATION';
30
+ // Rows are ordered best-first, so the FIRST row the position satisfies is the narrowest band it earns.
31
+ // Scanning to the end instead would always land on the widest, handing first place a "top 20" badge.
32
+ for (const row of exports.EVENT_REWARDS) {
33
+ if (row.maxPosition != null && position <= row.maxPosition)
34
+ return row.band;
35
+ }
36
+ return 'PARTICIPATION';
37
+ }
38
+ /** Everything a finish earns, adding its own row to every row beneath it. */
39
+ function eventPayoutFor(position) {
40
+ const band = eventRewardBandFor(position);
41
+ const from = exports.EVENT_REWARDS.findIndex(row => row.band === band);
42
+ let packs = 0;
43
+ let coins = 0;
44
+ for (const row of exports.EVENT_REWARDS.slice(from)) {
45
+ packs += row.packs;
46
+ coins += row.coins;
47
+ }
48
+ return { packs, coins };
49
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const event_rewards_1 = require("./event-rewards");
5
+ (0, globals_1.describe)('eventRewardBandFor()', () => {
6
+ globals_1.it.each([
7
+ [1, 'TOP_1'],
8
+ [2, 'TOP_3'],
9
+ [3, 'TOP_3'],
10
+ [4, 'TOP_5'],
11
+ [5, 'TOP_5'],
12
+ [6, 'TOP_10'],
13
+ [10, 'TOP_10'],
14
+ [11, 'TOP_20'],
15
+ [20, 'TOP_20'],
16
+ // Inside a 24-team final but outside every banded row, so it counts as taking part.
17
+ [21, 'PARTICIPATION'],
18
+ [24, 'PARTICIPATION']
19
+ ])('places a finish of %i in %s', (position, band) => {
20
+ (0, globals_1.expect)((0, event_rewards_1.eventRewardBandFor)(position)).toBe(band);
21
+ });
22
+ (0, globals_1.it)('treats a team that never reached the final as participation', () => {
23
+ (0, globals_1.expect)((0, event_rewards_1.eventRewardBandFor)(null)).toBe('PARTICIPATION');
24
+ });
25
+ });
26
+ (0, globals_1.describe)('eventPayoutFor()', () => {
27
+ globals_1.it.each([
28
+ // position, packs, coins. Cumulative: every row from Participation down to the finish.
29
+ [null, 1, 1000], // group stage: 1 pack + 1000
30
+ [24, 1, 1000], // in the final but outside the top 20: still just participation
31
+ [20, 1, 2000], // + top 20's 1000
32
+ [11, 1, 2000],
33
+ [10, 2, 2000], // + top 10's pack
34
+ [6, 2, 2000],
35
+ [5, 2, 3000], // + top 5's 1000
36
+ [4, 2, 3000],
37
+ [3, 2, 3500], // + top 3's 500
38
+ [2, 2, 3500],
39
+ [1, 3, 3500] // + the winner's pack
40
+ ])('pays a finish of %s with %i packs and %i coins', (position, packs, coins) => {
41
+ (0, globals_1.expect)((0, event_rewards_1.eventPayoutFor)(position)).toEqual({ packs, coins });
42
+ });
43
+ (0, globals_1.it)('never pays less for a better finish', () => {
44
+ const finishes = [null, 24, 20, 11, 10, 6, 5, 4, 3, 2, 1];
45
+ for (let i = 1; i < finishes.length; i++) {
46
+ const worse = (0, event_rewards_1.eventPayoutFor)(finishes[i - 1]);
47
+ const better = (0, event_rewards_1.eventPayoutFor)(finishes[i]);
48
+ (0, globals_1.expect)(better.packs).toBeGreaterThanOrEqual(worse.packs);
49
+ (0, globals_1.expect)(better.coins).toBeGreaterThanOrEqual(worse.coins);
50
+ }
51
+ });
52
+ (0, globals_1.it)('adds up to the owner-defined table for a winner', () => {
53
+ // 1x pack + 1000 (participation), +1000 (top 20), +1 pack (top 10), +1000 (top 5), +500 (top 3), +1 pack (top 1)
54
+ const total = event_rewards_1.EVENT_REWARDS.reduce((acc, row) => ({ packs: acc.packs + row.packs, coins: acc.coins + row.coins }), { packs: 0, coins: 0 });
55
+ (0, globals_1.expect)((0, event_rewards_1.eventPayoutFor)(1)).toEqual(total);
56
+ (0, globals_1.expect)(total).toEqual({ packs: 3, coins: 3500 });
57
+ });
58
+ });
@@ -0,0 +1,126 @@
1
+ import { Rarity } from '../player';
2
+ export type EventKey = 'WAR_OF_THE_COMMONS' | 'RARE_GEMS' | 'STUFF_OF_LEGENDS';
3
+ /**
4
+ * How an event is played out. One member today; a new format is a new member here AND the code path in the
5
+ * simulator that builds its schedule, which is why formats live in code rather than in a table.
6
+ * - `GROUPS_FINAL` equal groups playing a round robin, then one final group of the qualifiers on the last day.
7
+ */
8
+ export type EventFormat = 'GROUPS_FINAL';
9
+ /**
10
+ * Where an event is in its life.
11
+ * - `REGISTRATION` the lineup can be saved; saving is what registers the team.
12
+ * - `IN_PROGRESS` matches are scheduled and playing (group stage, then the final).
13
+ * - `REVIEW` finished; results readable, nothing playing.
14
+ * - `ARCHIVED` purged; only the Event row itself survives.
15
+ */
16
+ export type EventStatus = 'REGISTRATION' | 'IN_PROGRESS' | 'REVIEW' | 'ARCHIVED';
17
+ /**
18
+ * A preset: everything about HOW an event works. An admin cannot change any of it, they pick one of these and
19
+ * say when it starts, so an event can never be configured into a shape the simulator cannot run.
20
+ */
21
+ export interface EventDefinition {
22
+ readonly key: EventKey;
23
+ readonly format: EventFormat;
24
+ /** Every player referenced anywhere in the event lineup must be EXACTLY this rarity. */
25
+ readonly rarity: Rarity;
26
+ /** Total days from the start, the last of which is the final. */
27
+ readonly durationDays: number;
28
+ }
29
+ export declare const EVENT_CATALOGUE: readonly EventDefinition[];
30
+ /** An event must be long enough to hold a group stage AND a final day. */
31
+ export declare const MIN_DURATION_DAYS = 2;
32
+ /** Registration has to give players a real chance to build a squad, and cannot be scheduled absurdly far out. */
33
+ export declare const MIN_REGISTRATION_DAYS = 1;
34
+ export declare const MAX_REGISTRATION_DAYS = 30;
35
+ /** Results stay readable for this long after an event ends, then everything but the Event row is deleted. */
36
+ export declare const REVIEW_DAYS = 3;
37
+ /**
38
+ * The final group is 24 teams: at one match per hour that is 23 rounds, which is the largest round robin that
39
+ * still fits inside the single final day.
40
+ */
41
+ export declare const MAX_FINAL_SIZE = 24;
42
+ /** A group is padded with bots to at least this many teams, so a small turnout still plays a real group stage. */
43
+ export declare const MIN_GROUP_SIZE = 16;
44
+ /** Rounds are never closer together than an hour, which is what bounds how large a group can be. */
45
+ export declare const MIN_ROUND_SPACING_MINUTES = 60;
46
+ /** One match per hour in the final: 24 teams = 23 rounds, 00:00 to 23:00. */
47
+ export declare const FINAL_ROUND_SPACING_MINUTES = 60;
48
+ /**
49
+ * How a turnout is split into groups, and how many of each group go through.
50
+ *
51
+ * Owner-defined (2026-07-23). Read as "fewer than `maxParticipants`". Every row multiplies out to exactly
52
+ * MAX_FINAL_SIZE, so the final is always 24 teams however many entered, and every group sends the same number
53
+ * through. Group counts that cannot divide 24 into whole qualifiers (5, 10) are deliberately absent.
54
+ */
55
+ export interface EventGroupTier {
56
+ /** Exclusive upper bound on the entrant count for this tier. */
57
+ readonly maxParticipants: number;
58
+ readonly groupCount: number;
59
+ readonly qualifiersPerGroup: number;
60
+ }
61
+ export declare const EVENT_GROUP_TIERS: readonly EventGroupTier[];
62
+ /** The tier a turnout falls into. The last row has no upper bound, so this always resolves. */
63
+ export declare function eventGroupTierFor(participantCount: number): EventGroupTier;
64
+ /** The preset a stored event was created from. Its rules are read from this, never re-derived. */
65
+ export declare function eventDefinitionForKey(key: EventKey): EventDefinition;
66
+ export interface EventWindows {
67
+ readonly key: EventKey;
68
+ readonly format: EventFormat;
69
+ readonly rarity: Rarity;
70
+ /** Registration opens this many days before the start. */
71
+ readonly registrationOpensAt: Date;
72
+ /** Registration closes the instant the event starts. */
73
+ readonly registrationClosesAt: Date;
74
+ readonly startsAt: Date;
75
+ /** 00:00 UTC on the last day: the final is a single day, one round per hour. */
76
+ readonly finalStartsAt: Date;
77
+ /** The exclusive end of the last day. */
78
+ readonly endsAt: Date;
79
+ /** Review ends and everything but the Event row is deleted. */
80
+ readonly purgeAt: Date;
81
+ }
82
+ /**
83
+ * Every date an event will ever have, computed once from its preset, its start day and how much lead time
84
+ * registration gets. The start is floored to 00:00 UTC so an event always runs whole days.
85
+ *
86
+ * ```
87
+ * start - registrationDays registration opens
88
+ * start registration closes, group stage begins
89
+ * start + duration - 1 the final (one day)
90
+ * start + duration the event ends, review begins
91
+ * start + duration + 3 purge
92
+ * ```
93
+ */
94
+ export declare function eventWindowsFor(definition: EventDefinition, startsAt: Date, registrationDays: number): EventWindows;
95
+ /** How long the group stage has, in hours: everything before the final day. */
96
+ export declare function groupStageHours(definition: EventDefinition): number;
97
+ export declare function eventStatusAt(windows: EventWindows, now: Date): EventStatus;
98
+ export interface EventGroupPlan {
99
+ readonly groupCount: number;
100
+ /** Equal across every group, always even, bots making up the difference. */
101
+ readonly groupSize: number;
102
+ /** How many bot teams have to be generated to fill the groups. */
103
+ readonly botCount: number;
104
+ readonly qualifiersPerGroup: number;
105
+ readonly finalSize: number;
106
+ /** Minutes between group-stage rounds, so the whole round robin lands inside the group-stage window. */
107
+ readonly groupRoundSpacingMinutes: number;
108
+ /** True when the group stage cannot fit even at the minimum spacing; the caller must log it loudly. */
109
+ readonly overflows: boolean;
110
+ }
111
+ /**
112
+ * Rounds start on the hour or the half hour and nothing else, so a kickoff never reads as 19:12.
113
+ *
114
+ * Snapped DOWN, so a stage that already fits its window still fits: rounding up could push the last round past
115
+ * the final. Events start at 00:00 UTC, so every round lands on the grid.
116
+ */
117
+ export declare const ROUND_SPACING_GRID_MINUTES = 30;
118
+ /**
119
+ * How a turnout of `participantCount` real teams is split, and how the round robin is paced to fit
120
+ * `stageHours` (the event's days before its final day, from groupStageHours).
121
+ *
122
+ * The group count and the number that go through come from EVENT_GROUP_TIERS. Groups are all equal in size,
123
+ * always even (a round robin needs an even field for everyone to play every round) and bot-padded to at least
124
+ * MIN_GROUP_SIZE, so a small turnout still plays a full group stage.
125
+ */
126
+ export declare function planEventGroups(participantCount: number, stageHours: number): EventGroupPlan;
@@ -0,0 +1,141 @@
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
+ exports.ROUND_SPACING_GRID_MINUTES = exports.EVENT_GROUP_TIERS = exports.FINAL_ROUND_SPACING_MINUTES = exports.MIN_ROUND_SPACING_MINUTES = exports.MIN_GROUP_SIZE = exports.MAX_FINAL_SIZE = exports.REVIEW_DAYS = exports.MAX_REGISTRATION_DAYS = exports.MIN_REGISTRATION_DAYS = exports.MIN_DURATION_DAYS = exports.EVENT_CATALOGUE = void 0;
7
+ exports.eventGroupTierFor = eventGroupTierFor;
8
+ exports.eventDefinitionForKey = eventDefinitionForKey;
9
+ exports.eventWindowsFor = eventWindowsFor;
10
+ exports.groupStageHours = groupStageHours;
11
+ exports.eventStatusAt = eventStatusAt;
12
+ exports.planEventGroups = planEventGroups;
13
+ const dayjs_1 = __importDefault(require("dayjs"));
14
+ const utc_1 = __importDefault(require("dayjs/plugin/utc"));
15
+ const player_1 = require("../player");
16
+ dayjs_1.default.extend(utc_1.default);
17
+ // The presets an admin can schedule. Adding one here (plus its name in the 7 locale files) is all a new event
18
+ // needs, as long as its format is one the simulator already builds.
19
+ exports.EVENT_CATALOGUE = [
20
+ { key: 'WAR_OF_THE_COMMONS', format: 'GROUPS_FINAL', rarity: player_1.RarityEnum.COMMON, durationDays: 7 },
21
+ { key: 'RARE_GEMS', format: 'GROUPS_FINAL', rarity: player_1.RarityEnum.RARE, durationDays: 7 },
22
+ { key: 'STUFF_OF_LEGENDS', format: 'GROUPS_FINAL', rarity: player_1.RarityEnum.LEGENDARY, durationDays: 7 }
23
+ ];
24
+ /** An event must be long enough to hold a group stage AND a final day. */
25
+ exports.MIN_DURATION_DAYS = 2;
26
+ /** Registration has to give players a real chance to build a squad, and cannot be scheduled absurdly far out. */
27
+ exports.MIN_REGISTRATION_DAYS = 1;
28
+ exports.MAX_REGISTRATION_DAYS = 30;
29
+ /** Results stay readable for this long after an event ends, then everything but the Event row is deleted. */
30
+ exports.REVIEW_DAYS = 3;
31
+ /**
32
+ * The final group is 24 teams: at one match per hour that is 23 rounds, which is the largest round robin that
33
+ * still fits inside the single final day.
34
+ */
35
+ exports.MAX_FINAL_SIZE = 24;
36
+ /** A group is padded with bots to at least this many teams, so a small turnout still plays a real group stage. */
37
+ exports.MIN_GROUP_SIZE = 16;
38
+ /** Rounds are never closer together than an hour, which is what bounds how large a group can be. */
39
+ exports.MIN_ROUND_SPACING_MINUTES = 60;
40
+ /** One match per hour in the final: 24 teams = 23 rounds, 00:00 to 23:00. */
41
+ exports.FINAL_ROUND_SPACING_MINUTES = 60;
42
+ exports.EVENT_GROUP_TIERS = [
43
+ { maxParticipants: 64, groupCount: 3, qualifiersPerGroup: 8 },
44
+ { maxParticipants: 96, groupCount: 4, qualifiersPerGroup: 6 },
45
+ { maxParticipants: 128, groupCount: 6, qualifiersPerGroup: 4 },
46
+ { maxParticipants: 192, groupCount: 8, qualifiersPerGroup: 3 },
47
+ { maxParticipants: Number.POSITIVE_INFINITY, groupCount: 12, qualifiersPerGroup: 2 }
48
+ ];
49
+ /** The tier a turnout falls into. The last row has no upper bound, so this always resolves. */
50
+ function eventGroupTierFor(participantCount) {
51
+ return exports.EVENT_GROUP_TIERS.find(tier => participantCount < tier.maxParticipants) ?? exports.EVENT_GROUP_TIERS[exports.EVENT_GROUP_TIERS.length - 1];
52
+ }
53
+ /** The preset a stored event was created from. Its rules are read from this, never re-derived. */
54
+ function eventDefinitionForKey(key) {
55
+ const definition = exports.EVENT_CATALOGUE.find(e => e.key === key);
56
+ if (definition == null)
57
+ throw new Error(`UNKNOWN_EVENT_KEY: ${key}`);
58
+ return definition;
59
+ }
60
+ /**
61
+ * Every date an event will ever have, computed once from its preset, its start day and how much lead time
62
+ * registration gets. The start is floored to 00:00 UTC so an event always runs whole days.
63
+ *
64
+ * ```
65
+ * start - registrationDays registration opens
66
+ * start registration closes, group stage begins
67
+ * start + duration - 1 the final (one day)
68
+ * start + duration the event ends, review begins
69
+ * start + duration + 3 purge
70
+ * ```
71
+ */
72
+ function eventWindowsFor(definition, startsAt, registrationDays) {
73
+ const start = dayjs_1.default.utc(startsAt).startOf('day');
74
+ return {
75
+ key: definition.key,
76
+ format: definition.format,
77
+ rarity: definition.rarity,
78
+ registrationOpensAt: start.subtract(registrationDays, 'day').toDate(),
79
+ registrationClosesAt: start.toDate(),
80
+ startsAt: start.toDate(),
81
+ finalStartsAt: start.add(definition.durationDays - 1, 'day').toDate(),
82
+ endsAt: start.add(definition.durationDays, 'day').toDate(),
83
+ purgeAt: start.add(definition.durationDays + exports.REVIEW_DAYS, 'day').toDate()
84
+ };
85
+ }
86
+ /** How long the group stage has, in hours: everything before the final day. */
87
+ function groupStageHours(definition) {
88
+ return (definition.durationDays - 1) * 24;
89
+ }
90
+ function eventStatusAt(windows, now) {
91
+ const t = now.getTime();
92
+ if (t < windows.startsAt.getTime())
93
+ return 'REGISTRATION';
94
+ if (t < windows.endsAt.getTime())
95
+ return 'IN_PROGRESS';
96
+ if (t < windows.purgeAt.getTime())
97
+ return 'REVIEW';
98
+ return 'ARCHIVED';
99
+ }
100
+ function evenUp(n) {
101
+ return n % 2 === 0 ? n : n + 1;
102
+ }
103
+ /**
104
+ * Rounds start on the hour or the half hour and nothing else, so a kickoff never reads as 19:12.
105
+ *
106
+ * Snapped DOWN, so a stage that already fits its window still fits: rounding up could push the last round past
107
+ * the final. Events start at 00:00 UTC, so every round lands on the grid.
108
+ */
109
+ exports.ROUND_SPACING_GRID_MINUTES = 30;
110
+ function snapToGrid(minutes) {
111
+ return Math.floor(minutes / exports.ROUND_SPACING_GRID_MINUTES) * exports.ROUND_SPACING_GRID_MINUTES;
112
+ }
113
+ /**
114
+ * How a turnout of `participantCount` real teams is split, and how the round robin is paced to fit
115
+ * `stageHours` (the event's days before its final day, from groupStageHours).
116
+ *
117
+ * The group count and the number that go through come from EVENT_GROUP_TIERS. Groups are all equal in size,
118
+ * always even (a round robin needs an even field for everyone to play every round) and bot-padded to at least
119
+ * MIN_GROUP_SIZE, so a small turnout still plays a full group stage.
120
+ */
121
+ function planEventGroups(participantCount, stageHours) {
122
+ const tier = eventGroupTierFor(participantCount);
123
+ const groupCount = tier.groupCount;
124
+ const groupSize = Math.max(exports.MIN_GROUP_SIZE, evenUp(Math.ceil(participantCount / groupCount)));
125
+ // Capped by the group size purely as a guard; with a 16-team minimum and at most 8 going through it never binds.
126
+ const qualifiersPerGroup = Math.min(tier.qualifiersPerGroup, groupSize);
127
+ // One round per pairing: an even group plays groupSize-1 rounds, spread across the whole group-stage window.
128
+ const rounds = groupSize - 1;
129
+ const spacing = Math.floor((stageHours * 60) / rounds);
130
+ return {
131
+ groupCount,
132
+ groupSize,
133
+ botCount: groupCount * groupSize - participantCount,
134
+ qualifiersPerGroup,
135
+ finalSize: qualifiersPerGroup * groupCount,
136
+ groupRoundSpacingMinutes: Math.max(exports.MIN_ROUND_SPACING_MINUTES, snapToGrid(spacing)),
137
+ // Judged on the UNSNAPPED spacing: snapping only ever shortens the gap, so it cannot rescue a stage that
138
+ // already does not fit, and judging the snapped value would hide a genuine overflow.
139
+ overflows: spacing < exports.MIN_ROUND_SPACING_MINUTES
140
+ };
141
+ }