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.
- package/dist/cjs/src/api/index.d.ts +2 -1
- package/dist/cjs/src/api/index.js +15 -0
- package/dist/cjs/src/api/tactics-player-refs.d.ts +13 -0
- package/dist/cjs/src/api/tactics-player-refs.js +43 -0
- package/dist/cjs/src/data/event-purge.d.ts +14 -0
- package/dist/cjs/src/data/event-purge.js +116 -0
- package/dist/cjs/src/data/index.d.ts +1 -0
- package/dist/cjs/src/data/index.js +1 -0
- package/dist/cjs/src/data/init-models.js +41 -0
- package/dist/cjs/src/data/models/competition.d.ts +1 -1
- package/dist/cjs/src/data/models/competition.js +1 -1
- package/dist/cjs/src/data/models/event-bot-team.d.ts +21 -0
- package/dist/cjs/src/data/models/event-bot-team.js +44 -0
- package/dist/cjs/src/data/models/event-competition.d.ts +29 -0
- package/dist/cjs/src/data/models/event-competition.js +52 -0
- package/dist/cjs/src/data/models/event-pack-player.d.ts +22 -0
- package/dist/cjs/src/data/models/event-pack-player.js +31 -0
- package/dist/cjs/src/data/models/event-pack.d.ts +37 -0
- package/dist/cjs/src/data/models/event-pack.js +55 -0
- package/dist/cjs/src/data/models/event-result.d.ts +34 -0
- package/dist/cjs/src/data/models/event-result.js +48 -0
- package/dist/cjs/src/data/models/event-squad.d.ts +30 -0
- package/dist/cjs/src/data/models/event-squad.js +46 -0
- package/dist/cjs/src/data/models/event.d.ts +51 -0
- package/dist/cjs/src/data/models/event.js +82 -0
- package/dist/cjs/src/data/models/index.d.ts +7 -0
- package/dist/cjs/src/data/models/index.js +7 -0
- package/dist/cjs/src/data/models/lineup-preset.d.ts +3 -1
- package/dist/cjs/src/data/models/lineup-preset.js +53 -0
- package/dist/cjs/src/index.d.ts +1 -0
- package/dist/cjs/src/index.js +6 -0
- package/dist/cjs/src/service/competition/event-draft.d.ts +40 -0
- package/dist/cjs/src/service/competition/event-draft.js +68 -0
- package/dist/cjs/src/service/competition/event-draft.test.d.ts +1 -0
- package/dist/cjs/src/service/competition/event-draft.test.js +118 -0
- package/dist/cjs/src/service/competition/event-rewards.d.ts +26 -0
- package/dist/cjs/src/service/competition/event-rewards.js +49 -0
- package/dist/cjs/src/service/competition/event-rewards.test.d.ts +1 -0
- package/dist/cjs/src/service/competition/event-rewards.test.js +58 -0
- package/dist/cjs/src/service/competition/event-schedule.d.ts +126 -0
- package/dist/cjs/src/service/competition/event-schedule.js +141 -0
- package/dist/cjs/src/service/competition/event-schedule.test.d.ts +1 -0
- package/dist/cjs/src/service/competition/event-schedule.test.js +138 -0
- package/dist/cjs/src/service/competition/index.d.ts +3 -0
- package/dist/cjs/src/service/competition/index.js +3 -0
- package/dist/cjs/src/service/match/index.d.ts +1 -0
- package/dist/cjs/src/service/match/index.js +1 -0
- package/dist/cjs/src/service/match/match-generator.js +25 -48
- package/dist/cjs/src/service/match/match-generator.test.d.ts +1 -0
- package/dist/cjs/src/service/match/match-generator.test.js +48 -0
- package/dist/cjs/src/service/match/round-robin.d.ts +11 -0
- package/dist/cjs/src/service/match/round-robin.js +44 -0
- package/dist/cjs/src/service/match/round-robin.test.d.ts +1 -0
- package/dist/cjs/src/service/match/round-robin.test.js +71 -0
- package/dist/cjs/src/service/notification/catalog/en.json +21 -0
- package/dist/cjs/src/service/notification/catalog/es.json +21 -0
- package/dist/cjs/src/service/notification/catalog/fr.json +21 -0
- package/dist/cjs/src/service/notification/catalog/it.json +21 -0
- package/dist/cjs/src/service/notification/catalog/pl.json +21 -0
- package/dist/cjs/src/service/notification/catalog/pt-BR.json +21 -0
- package/dist/cjs/src/service/notification/catalog/tr.json +21 -0
- package/dist/cjs/src/service/notification/notification-type.d.ts +1 -1
- package/dist/cjs/src/service/notification/notification-type.js +6 -1
- package/dist/cjs/src/service/notification/notification.test.js +3 -1
- package/dist/cjs/src/service/notification/registry.js +52 -0
- package/dist/cjs/src/service/player/player-generator.d.ts +2 -1
- package/dist/cjs/src/service/player/player-generator.js +37 -5
- package/dist/esm/src/api/index.d.ts +2 -1
- package/dist/esm/src/api/index.js +1 -0
- package/dist/esm/src/api/tactics-player-refs.d.ts +13 -0
- package/dist/esm/src/api/tactics-player-refs.js +40 -0
- package/dist/esm/src/data/event-purge.d.ts +14 -0
- package/dist/esm/src/data/event-purge.js +110 -0
- package/dist/esm/src/data/index.d.ts +1 -0
- package/dist/esm/src/data/index.js +1 -0
- package/dist/esm/src/data/init-models.js +42 -1
- package/dist/esm/src/data/models/competition.d.ts +1 -1
- package/dist/esm/src/data/models/competition.js +1 -1
- package/dist/esm/src/data/models/event-bot-team.d.ts +21 -0
- package/dist/esm/src/data/models/event-bot-team.js +40 -0
- package/dist/esm/src/data/models/event-competition.d.ts +29 -0
- package/dist/esm/src/data/models/event-competition.js +48 -0
- package/dist/esm/src/data/models/event-pack-player.d.ts +22 -0
- package/dist/esm/src/data/models/event-pack-player.js +27 -0
- package/dist/esm/src/data/models/event-pack.d.ts +37 -0
- package/dist/esm/src/data/models/event-pack.js +51 -0
- package/dist/esm/src/data/models/event-result.d.ts +34 -0
- package/dist/esm/src/data/models/event-result.js +44 -0
- package/dist/esm/src/data/models/event-squad.d.ts +30 -0
- package/dist/esm/src/data/models/event-squad.js +42 -0
- package/dist/esm/src/data/models/event.d.ts +51 -0
- package/dist/esm/src/data/models/event.js +78 -0
- package/dist/esm/src/data/models/index.d.ts +7 -0
- package/dist/esm/src/data/models/index.js +7 -0
- package/dist/esm/src/data/models/lineup-preset.d.ts +3 -1
- package/dist/esm/src/data/models/lineup-preset.js +20 -0
- package/dist/esm/src/index.d.ts +1 -0
- package/dist/esm/src/index.js +4 -0
- package/dist/esm/src/service/competition/event-draft.d.ts +40 -0
- package/dist/esm/src/service/competition/event-draft.js +62 -0
- package/dist/esm/src/service/competition/event-draft.test.d.ts +1 -0
- package/dist/esm/src/service/competition/event-draft.test.js +116 -0
- package/dist/esm/src/service/competition/event-rewards.d.ts +26 -0
- package/dist/esm/src/service/competition/event-rewards.js +44 -0
- package/dist/esm/src/service/competition/event-rewards.test.d.ts +1 -0
- package/dist/esm/src/service/competition/event-rewards.test.js +56 -0
- package/dist/esm/src/service/competition/event-schedule.d.ts +126 -0
- package/dist/esm/src/service/competition/event-schedule.js +129 -0
- package/dist/esm/src/service/competition/event-schedule.test.d.ts +1 -0
- package/dist/esm/src/service/competition/event-schedule.test.js +136 -0
- package/dist/esm/src/service/competition/index.d.ts +3 -0
- package/dist/esm/src/service/competition/index.js +3 -0
- package/dist/esm/src/service/match/index.d.ts +1 -0
- package/dist/esm/src/service/match/index.js +1 -0
- package/dist/esm/src/service/match/match-generator.js +25 -48
- package/dist/esm/src/service/match/match-generator.test.d.ts +1 -0
- package/dist/esm/src/service/match/match-generator.test.js +43 -0
- package/dist/esm/src/service/match/round-robin.d.ts +11 -0
- package/dist/esm/src/service/match/round-robin.js +41 -0
- package/dist/esm/src/service/match/round-robin.test.d.ts +1 -0
- package/dist/esm/src/service/match/round-robin.test.js +69 -0
- package/dist/esm/src/service/notification/catalog/en.json +21 -0
- package/dist/esm/src/service/notification/catalog/es.json +21 -0
- package/dist/esm/src/service/notification/catalog/fr.json +21 -0
- package/dist/esm/src/service/notification/catalog/it.json +21 -0
- package/dist/esm/src/service/notification/catalog/pl.json +21 -0
- package/dist/esm/src/service/notification/catalog/pt-BR.json +21 -0
- package/dist/esm/src/service/notification/catalog/tr.json +21 -0
- package/dist/esm/src/service/notification/notification-type.d.ts +1 -1
- package/dist/esm/src/service/notification/notification-type.js +6 -1
- package/dist/esm/src/service/notification/notification.test.js +3 -1
- package/dist/esm/src/service/notification/registry.js +52 -0
- package/dist/esm/src/service/player/player-generator.d.ts +2 -1
- package/dist/esm/src/service/player/player-generator.js +36 -4
- package/package.json +6 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { EVENT_CATALOGUE, MAX_FINAL_SIZE, MIN_GROUP_SIZE, MIN_ROUND_SPACING_MINUTES, REVIEW_DAYS, eventDefinitionForKey, eventStatusAt, eventWindowsFor, groupStageHours, planEventGroups } from './event-schedule';
|
|
3
|
+
import { RarityEnum } from '../player';
|
|
4
|
+
const COMMONS = eventDefinitionForKey('WAR_OF_THE_COMMONS');
|
|
5
|
+
describe('EVENT_CATALOGUE', () => {
|
|
6
|
+
it('restricts the three launch presets to a single rarity each, over one week', () => {
|
|
7
|
+
expect(EVENT_CATALOGUE.map(e => e.rarity)).toEqual([RarityEnum.COMMON, RarityEnum.RARE, RarityEnum.LEGENDARY]);
|
|
8
|
+
expect(EVENT_CATALOGUE.every(e => e.durationDays === 7)).toBe(true);
|
|
9
|
+
expect(EVENT_CATALOGUE.every(e => e.format === 'GROUPS_FINAL')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it('leaves room for a final day in every preset', () => {
|
|
12
|
+
expect(EVENT_CATALOGUE.every(e => e.durationDays >= 2)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe('eventDefinitionForKey()', () => {
|
|
16
|
+
it('reads the rules off the key an event stores', () => {
|
|
17
|
+
expect(eventDefinitionForKey('STUFF_OF_LEGENDS').rarity).toBe(RarityEnum.LEGENDARY);
|
|
18
|
+
expect(eventDefinitionForKey('RARE_GEMS').rarity).toBe(RarityEnum.RARE);
|
|
19
|
+
expect(eventDefinitionForKey('WAR_OF_THE_COMMONS').rarity).toBe(RarityEnum.COMMON);
|
|
20
|
+
});
|
|
21
|
+
it('throws on a key that is not in the catalogue', () => {
|
|
22
|
+
expect(() => eventDefinitionForKey('NOT_AN_EVENT')).toThrow('UNKNOWN_EVENT_KEY');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('eventWindowsFor()', () => {
|
|
26
|
+
it('derives every date from the preset, the start day and the registration lead time', () => {
|
|
27
|
+
const w = eventWindowsFor(COMMONS, new Date('2026-08-03T00:00:00.000Z'), 4);
|
|
28
|
+
expect(w.registrationOpensAt.toISOString()).toBe('2026-07-30T00:00:00.000Z'); // start - 4 days
|
|
29
|
+
expect(w.registrationClosesAt.toISOString()).toBe('2026-08-03T00:00:00.000Z'); // = startsAt
|
|
30
|
+
expect(w.startsAt.toISOString()).toBe('2026-08-03T00:00:00.000Z');
|
|
31
|
+
expect(w.finalStartsAt.toISOString()).toBe('2026-08-09T00:00:00.000Z'); // last day of 7
|
|
32
|
+
expect(w.endsAt.toISOString()).toBe('2026-08-10T00:00:00.000Z');
|
|
33
|
+
expect(w.purgeAt.toISOString()).toBe('2026-08-13T00:00:00.000Z'); // + 3 review days
|
|
34
|
+
});
|
|
35
|
+
it('floors the start to 00:00 UTC so an event always runs whole days', () => {
|
|
36
|
+
const w = eventWindowsFor(COMMONS, new Date('2026-08-03T17:42:11.000Z'), 4);
|
|
37
|
+
expect(w.startsAt.toISOString()).toBe('2026-08-03T00:00:00.000Z');
|
|
38
|
+
});
|
|
39
|
+
it('moves registration with the lead time and nothing else', () => {
|
|
40
|
+
const a = eventWindowsFor(COMMONS, new Date('2026-08-03T00:00:00.000Z'), 1);
|
|
41
|
+
const b = eventWindowsFor(COMMONS, new Date('2026-08-03T00:00:00.000Z'), 14);
|
|
42
|
+
expect(a.registrationOpensAt.toISOString()).toBe('2026-08-02T00:00:00.000Z');
|
|
43
|
+
expect(b.registrationOpensAt.toISOString()).toBe('2026-07-20T00:00:00.000Z');
|
|
44
|
+
expect(a.startsAt.getTime()).toBe(b.startsAt.getTime());
|
|
45
|
+
expect(a.endsAt.getTime()).toBe(b.endsAt.getTime());
|
|
46
|
+
expect(a.purgeAt.getTime()).toBe(b.purgeAt.getTime());
|
|
47
|
+
});
|
|
48
|
+
it('gives the final exactly one day, whatever the duration', () => {
|
|
49
|
+
for (const durationDays of [2, 3, 7, 10]) {
|
|
50
|
+
const w = eventWindowsFor({ ...COMMONS, durationDays }, new Date('2026-08-03T00:00:00.000Z'), 4);
|
|
51
|
+
expect(w.endsAt.getTime() - w.finalStartsAt.getTime()).toBe(24 * 3600 * 1000);
|
|
52
|
+
expect(w.endsAt.getTime() - w.startsAt.getTime()).toBe(durationDays * 24 * 3600 * 1000);
|
|
53
|
+
expect(w.purgeAt.getTime() - w.endsAt.getTime()).toBe(REVIEW_DAYS * 24 * 3600 * 1000);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('groupStageHours()', () => {
|
|
58
|
+
it('is every day before the final day', () => {
|
|
59
|
+
expect(groupStageHours({ ...COMMONS, durationDays: 7 })).toBe(144);
|
|
60
|
+
expect(groupStageHours({ ...COMMONS, durationDays: 2 })).toBe(24);
|
|
61
|
+
expect(groupStageHours({ ...COMMONS, durationDays: 10 })).toBe(216);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('eventStatusAt()', () => {
|
|
65
|
+
const w = eventWindowsFor(COMMONS, new Date('2026-08-03T00:00:00.000Z'), 4);
|
|
66
|
+
it.each([
|
|
67
|
+
['2026-07-30T00:00:00.000Z', 'REGISTRATION'],
|
|
68
|
+
['2026-08-02T23:59:59.000Z', 'REGISTRATION'],
|
|
69
|
+
['2026-08-03T00:00:00.000Z', 'IN_PROGRESS'],
|
|
70
|
+
['2026-08-09T12:00:00.000Z', 'IN_PROGRESS'],
|
|
71
|
+
['2026-08-09T23:59:59.000Z', 'IN_PROGRESS'],
|
|
72
|
+
['2026-08-10T00:00:00.000Z', 'REVIEW'],
|
|
73
|
+
['2026-08-12T23:59:59.000Z', 'REVIEW'],
|
|
74
|
+
['2026-08-13T00:00:00.000Z', 'ARCHIVED']
|
|
75
|
+
])('reads %s as %s', (iso, status) => {
|
|
76
|
+
expect(eventStatusAt(w, new Date(iso))).toBe(status);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('planEventGroups()', () => {
|
|
80
|
+
const WEEK = groupStageHours(COMMONS); // 144h, the six days before the final
|
|
81
|
+
it.each([
|
|
82
|
+
// turnout, groups, size, qualifiers/group, final. Tiers: <64 = 3 groups top 8, <96 = 4 top 6,
|
|
83
|
+
// <128 = 6 top 4, <192 = 8 top 3, otherwise 12 top 2. Every tier multiplies out to a 24-team final.
|
|
84
|
+
[1, 3, MIN_GROUP_SIZE, 8, 24],
|
|
85
|
+
[12, 3, MIN_GROUP_SIZE, 8, 24],
|
|
86
|
+
[48, 3, MIN_GROUP_SIZE, 8, 24],
|
|
87
|
+
[63, 3, 22, 8, 24],
|
|
88
|
+
[64, 4, MIN_GROUP_SIZE, 6, 24],
|
|
89
|
+
[95, 4, 24, 6, 24],
|
|
90
|
+
[96, 6, MIN_GROUP_SIZE, 4, 24],
|
|
91
|
+
[127, 6, 22, 4, 24],
|
|
92
|
+
[128, 8, MIN_GROUP_SIZE, 3, 24],
|
|
93
|
+
[191, 8, 24, 3, 24],
|
|
94
|
+
[192, 12, MIN_GROUP_SIZE, 2, 24],
|
|
95
|
+
[300, 12, 26, 2, 24]
|
|
96
|
+
])('splits a turnout of %i into %i groups of %i', (n, groupCount, groupSize, qualifiersPerGroup, finalSize) => {
|
|
97
|
+
const plan = planEventGroups(n, WEEK);
|
|
98
|
+
expect(plan.groupCount).toBe(groupCount);
|
|
99
|
+
expect(plan.groupSize).toBe(groupSize);
|
|
100
|
+
expect(plan.qualifiersPerGroup).toBe(qualifiersPerGroup);
|
|
101
|
+
expect(plan.finalSize).toBe(finalSize);
|
|
102
|
+
expect(plan.botCount).toBe(groupCount * groupSize - n);
|
|
103
|
+
});
|
|
104
|
+
it.each([1, 2, 7, 12, 33, 40, 41, 60, 61, 99, 100, 137, 250, 500])('keeps every group even, bot-padded and inside the final cap for a turnout of %i', (n) => {
|
|
105
|
+
const plan = planEventGroups(n, WEEK);
|
|
106
|
+
expect(plan.groupSize % 2).toBe(0); // even, so nobody sits a group-stage round out
|
|
107
|
+
expect(plan.groupSize).toBeGreaterThanOrEqual(MIN_GROUP_SIZE);
|
|
108
|
+
expect([3, 4, 6, 8, 12]).toContain(plan.groupCount);
|
|
109
|
+
expect(plan.botCount).toBeGreaterThanOrEqual(0);
|
|
110
|
+
expect(plan.groupCount * plan.groupSize).toBe(n + plan.botCount);
|
|
111
|
+
// Every tier is built to fill the final exactly, however many entered.
|
|
112
|
+
expect(plan.finalSize).toBe(MAX_FINAL_SIZE);
|
|
113
|
+
expect(plan.finalSize % 2).toBe(0); // an odd final would leave a team without an opponent every round
|
|
114
|
+
expect(plan.qualifiersPerGroup).toBeLessThanOrEqual(plan.groupSize);
|
|
115
|
+
expect(plan.groupRoundSpacingMinutes).toBeGreaterThanOrEqual(MIN_ROUND_SPACING_MINUTES);
|
|
116
|
+
// Rounds start on the hour or the half hour, never at 19:12.
|
|
117
|
+
expect(plan.groupRoundSpacingMinutes % 30).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
it('fits the whole round robin inside the group-stage window until it genuinely cannot', () => {
|
|
120
|
+
for (const n of [1, 40, 100, 250, 700]) {
|
|
121
|
+
const plan = planEventGroups(n, WEEK);
|
|
122
|
+
expect(plan.overflows).toBe(false);
|
|
123
|
+
expect((plan.groupSize - 1) * plan.groupRoundSpacingMinutes).toBeLessThanOrEqual(WEEK * 60);
|
|
124
|
+
}
|
|
125
|
+
// 12 groups of 168 needs 167 rounds; at the one-hour minimum that is far more than six days, so the plan
|
|
126
|
+
// reports the overflow instead of silently generating a schedule that runs past the deadline.
|
|
127
|
+
expect(planEventGroups(2000, WEEK).overflows).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('paces rounds to the window it is given, so a shorter event packs them tighter', () => {
|
|
130
|
+
const long = planEventGroups(40, WEEK);
|
|
131
|
+
const short = planEventGroups(40, groupStageHours({ ...COMMONS, durationDays: 3 })); // 48h
|
|
132
|
+
expect(short.groupSize).toBe(long.groupSize);
|
|
133
|
+
expect(short.groupRoundSpacingMinutes).toBeLessThan(long.groupRoundSpacingMinutes);
|
|
134
|
+
expect((short.groupSize - 1) * short.groupRoundSpacingMinutes).toBeLessThanOrEqual(48 * 60);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -2,9 +2,13 @@ import dayjs from 'dayjs';
|
|
|
2
2
|
import utc from 'dayjs/plugin/utc';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
import { Match } from './match';
|
|
5
|
+
import { roundRobinRounds } from './round-robin';
|
|
5
6
|
import { shuffle } from '../utils';
|
|
6
7
|
import { StatusEnum } from '../../data';
|
|
7
8
|
dayjs.extend(utc);
|
|
9
|
+
// Consecutive matches in a league season start half an hour apart, so a 16-team double round robin (240 matches)
|
|
10
|
+
// spans five days from the iteration's Monday.
|
|
11
|
+
const MATCH_SPACING_MINUTES = 30;
|
|
8
12
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
9
13
|
export default class MatchGenerator {
|
|
10
14
|
static generateMatches(teams, baseDate) {
|
|
@@ -16,57 +20,30 @@ export default class MatchGenerator {
|
|
|
16
20
|
if (t.size !== teams.length) {
|
|
17
21
|
throw new Error('DUPLICATE_TEAMS');
|
|
18
22
|
}
|
|
19
|
-
|
|
20
|
-
const l = shuffled.length - 1;
|
|
21
|
-
const half = Math.ceil(shuffled.length / 2);
|
|
22
|
-
const matches = [];
|
|
23
|
-
// We fix the first team
|
|
24
|
-
let firstTeam = shuffled.shift();
|
|
25
|
-
if (firstTeam == null) {
|
|
23
|
+
if (teams.length < 1) {
|
|
26
24
|
throw new Error('EMPTY_TEAM_ARRAY');
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
let awayRow;
|
|
30
|
-
for (let i = 0; i < l; i++) {
|
|
31
|
-
// split array in 2 halves
|
|
32
|
-
const row1 = shuffled.slice(0, half);
|
|
33
|
-
const row2 = shuffled.slice(half);
|
|
34
|
-
row2.push(firstTeam);
|
|
35
|
-
if (i % 2 === 0) {
|
|
36
|
-
homeRow = row1;
|
|
37
|
-
awayRow = row2;
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
homeRow = row2;
|
|
41
|
-
awayRow = row1;
|
|
42
|
-
}
|
|
43
|
-
for (let j = 0; j < half; j++) {
|
|
44
|
-
matches.push(Match.create({
|
|
45
|
-
id: uuidv4(),
|
|
46
|
-
homeTeam: homeRow[j],
|
|
47
|
-
awayTeam: awayRow[j],
|
|
48
|
-
scheduledDate: date.toDate(),
|
|
49
|
-
status: StatusEnum.PENDING
|
|
50
|
-
}));
|
|
51
|
-
date = date.add(30, 'minutes');
|
|
52
|
-
}
|
|
53
|
-
firstTeam = row2.pop();
|
|
54
|
-
row1.unshift(row2.shift());
|
|
55
|
-
row2.push(row1.pop());
|
|
56
|
-
shuffled = [...row1, ...row2];
|
|
57
|
-
}
|
|
58
|
-
// Generate Inverted matches for home-away pairings
|
|
59
|
-
matches.push(...matches.map(match => {
|
|
26
|
+
const nextDate = () => {
|
|
60
27
|
const scheduledDate = date.toDate();
|
|
61
|
-
date = date.add(
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
28
|
+
date = date.add(MATCH_SPACING_MINUTES, 'minutes');
|
|
29
|
+
return scheduledDate;
|
|
30
|
+
};
|
|
31
|
+
// First leg: one pass of the round robin, matches laid out back to back in round order.
|
|
32
|
+
const firstLeg = roundRobinRounds(shuffle([...teams])).flat().map(([homeTeam, awayTeam]) => Match.create({
|
|
33
|
+
id: uuidv4(),
|
|
34
|
+
homeTeam,
|
|
35
|
+
awayTeam,
|
|
36
|
+
scheduledDate: nextDate(),
|
|
37
|
+
status: StatusEnum.PENDING
|
|
38
|
+
}));
|
|
39
|
+
// Second leg: the same fixtures with the sides swapped, so every pairing is played home and away.
|
|
40
|
+
const secondLeg = firstLeg.map(match => Match.create({
|
|
41
|
+
id: uuidv4(),
|
|
42
|
+
homeTeam: match.awayTeam,
|
|
43
|
+
awayTeam: match.homeTeam,
|
|
44
|
+
scheduledDate: nextDate(),
|
|
45
|
+
status: StatusEnum.PENDING
|
|
69
46
|
}));
|
|
70
|
-
return
|
|
47
|
+
return [...firstLeg, ...secondLeg];
|
|
71
48
|
}
|
|
72
49
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import MatchGenerator from './match-generator';
|
|
3
|
+
import { makeTeam } from '../test-helpers';
|
|
4
|
+
// The league season schedule. These assertions pin the behaviour the rollover depends on, so the shared
|
|
5
|
+
// roundRobinRounds helper (used by events too) cannot silently change how a season is laid out.
|
|
6
|
+
const MONDAY = new Date('2026-07-20T00:00:00.000Z');
|
|
7
|
+
function orderedKey(match) {
|
|
8
|
+
return `${match.homeTeam.id}>${match.awayTeam.id}`;
|
|
9
|
+
}
|
|
10
|
+
describe('MatchGenerator.generateMatches()', () => {
|
|
11
|
+
it('plays a full double round robin for a 16-team division', () => {
|
|
12
|
+
const teams = Array.from({ length: 16 }, () => makeTeam());
|
|
13
|
+
const matches = MatchGenerator.generateMatches(teams, MONDAY);
|
|
14
|
+
// 16 teams x 15 opponents, home and away.
|
|
15
|
+
expect(matches).toHaveLength(240);
|
|
16
|
+
const seen = new Set(matches.map(orderedKey));
|
|
17
|
+
expect(seen.size).toBe(240);
|
|
18
|
+
// Every ordered pairing exists exactly once, so every team hosts and visits every other team.
|
|
19
|
+
for (const home of teams) {
|
|
20
|
+
for (const away of teams) {
|
|
21
|
+
if (home.id === away.id)
|
|
22
|
+
continue;
|
|
23
|
+
expect(seen.has(`${home.id}>${away.id}`)).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it('anchors to 00:00 UTC of the base day and spaces matches 30 minutes apart', () => {
|
|
28
|
+
const teams = Array.from({ length: 16 }, () => makeTeam());
|
|
29
|
+
const matches = MatchGenerator.generateMatches(teams, new Date('2026-07-20T17:42:11.000Z'));
|
|
30
|
+
const times = matches.map(m => m.scheduledDate.getTime()).sort((a, b) => a - b);
|
|
31
|
+
expect(new Date(times[0]).toISOString()).toBe('2026-07-20T00:00:00.000Z');
|
|
32
|
+
for (let i = 1; i < times.length; i++) {
|
|
33
|
+
expect(times[i] - times[i - 1]).toBe(30 * 60 * 1000);
|
|
34
|
+
}
|
|
35
|
+
// 240 matches at half-hour spacing = five days, ending Friday 23:30 UTC.
|
|
36
|
+
expect(new Date(times[times.length - 1]).toISOString()).toBe('2026-07-24T23:30:00.000Z');
|
|
37
|
+
});
|
|
38
|
+
it('rejects a duplicated team and an empty division', () => {
|
|
39
|
+
const team = makeTeam();
|
|
40
|
+
expect(() => MatchGenerator.generateMatches([team, team], MONDAY)).toThrow('DUPLICATE_TEAMS');
|
|
41
|
+
expect(() => MatchGenerator.generateMatches([], MONDAY)).toThrow('EMPTY_TEAM_ARRAY');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -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,41 @@
|
|
|
1
|
+
// A bye slot, used to keep the circle even when an odd number of entrants is passed. Pairings that involve it
|
|
2
|
+
// are dropped, so that entrant sits the round out. Module-level so the type is nameable inside the function.
|
|
3
|
+
const BYE = Symbol('ROUND_ROBIN_BYE');
|
|
4
|
+
/**
|
|
5
|
+
* Circle-method single round robin. Returns one entry per round, each holding the pairings that round plays,
|
|
6
|
+
* as `[home, away]` tuples. Every unordered pair of entrants appears exactly once across all rounds.
|
|
7
|
+
*
|
|
8
|
+
* An ODD entrant count produces a bye: one entrant sits out each round, so a round has `floor(n / 2)` pairings
|
|
9
|
+
* instead of `n / 2`. Callers that need everyone playing every round must pad to an even count first.
|
|
10
|
+
*
|
|
11
|
+
* Home and away alternate by round and by court index, so no entrant ends up at home in all of its games. For a
|
|
12
|
+
* double round robin the caller replays the same rounds with the sides swapped, which balances them exactly.
|
|
13
|
+
*/
|
|
14
|
+
export function roundRobinRounds(entrants) {
|
|
15
|
+
if (entrants.length < 2)
|
|
16
|
+
return [];
|
|
17
|
+
const wheel = entrants.length % 2 === 0 ? [...entrants] : [...entrants, BYE];
|
|
18
|
+
const size = wheel.length;
|
|
19
|
+
const half = size / 2;
|
|
20
|
+
const rounds = [];
|
|
21
|
+
// Index 0 stays put while the rest rotate one place per round; pairing i against size-1-i then yields every
|
|
22
|
+
// pair exactly once over size-1 rounds.
|
|
23
|
+
let circle = wheel;
|
|
24
|
+
for (let round = 0; round < size - 1; round++) {
|
|
25
|
+
const pairings = [];
|
|
26
|
+
for (let court = 0; court < half; court++) {
|
|
27
|
+
const a = circle[court];
|
|
28
|
+
const b = circle[size - 1 - court];
|
|
29
|
+
if (a === BYE || b === BYE)
|
|
30
|
+
continue;
|
|
31
|
+
// Flip the whole column every other round. A rotating entrant advances one place per round, so it spends
|
|
32
|
+
// consecutive rounds on the same side of the circle and this parity alternates its home/away within them.
|
|
33
|
+
// Folding the court index into the parity instead would keep it CONSTANT for that entrant (its index and
|
|
34
|
+
// the round number rise together), pinning some entrants to one side for the entire event.
|
|
35
|
+
pairings.push(round % 2 === 0 ? [a, b] : [b, a]);
|
|
36
|
+
}
|
|
37
|
+
rounds.push(pairings);
|
|
38
|
+
circle = [circle[0], circle[size - 1], ...circle.slice(1, size - 1)];
|
|
39
|
+
}
|
|
40
|
+
return rounds;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { roundRobinRounds } from './round-robin';
|
|
2
|
+
function key(a, b) {
|
|
3
|
+
return a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
4
|
+
}
|
|
5
|
+
function entrants(n) {
|
|
6
|
+
return Array.from({ length: n }, (_, i) => `t${i}`);
|
|
7
|
+
}
|
|
8
|
+
describe('roundRobinRounds()', () => {
|
|
9
|
+
it('returns nothing for fewer than two entrants', () => {
|
|
10
|
+
expect(roundRobinRounds([])).toEqual([]);
|
|
11
|
+
expect(roundRobinRounds(['solo'])).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
it.each([2, 4, 6, 8, 16, 20, 50])('pairs every even field of %i exactly once', (n) => {
|
|
14
|
+
const rounds = roundRobinRounds(entrants(n));
|
|
15
|
+
expect(rounds).toHaveLength(n - 1);
|
|
16
|
+
rounds.forEach(round => expect(round).toHaveLength(n / 2));
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
for (const round of rounds) {
|
|
19
|
+
// Nobody plays twice in the same round.
|
|
20
|
+
const playing = new Set();
|
|
21
|
+
for (const [home, away] of round) {
|
|
22
|
+
expect(home).not.toEqual(away);
|
|
23
|
+
expect(playing.has(home)).toBe(false);
|
|
24
|
+
expect(playing.has(away)).toBe(false);
|
|
25
|
+
playing.add(home);
|
|
26
|
+
playing.add(away);
|
|
27
|
+
const k = key(home, away);
|
|
28
|
+
expect(seen.has(k)).toBe(false);
|
|
29
|
+
seen.add(k);
|
|
30
|
+
}
|
|
31
|
+
expect(playing.size).toBe(n);
|
|
32
|
+
}
|
|
33
|
+
expect(seen.size).toBe((n * (n - 1)) / 2);
|
|
34
|
+
});
|
|
35
|
+
it.each([3, 5, 7, 15])('gives an odd field of %i one bye per round and still pairs everyone once', (n) => {
|
|
36
|
+
const rounds = roundRobinRounds(entrants(n));
|
|
37
|
+
// An odd field runs the even-sized circle it was padded into: n rounds, one entrant idle in each.
|
|
38
|
+
expect(rounds).toHaveLength(n);
|
|
39
|
+
rounds.forEach(round => expect(round).toHaveLength((n - 1) / 2));
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const byes = new Map();
|
|
42
|
+
for (const round of rounds) {
|
|
43
|
+
const playing = new Set();
|
|
44
|
+
for (const [home, away] of round) {
|
|
45
|
+
seen.add(key(home, away));
|
|
46
|
+
playing.add(home);
|
|
47
|
+
playing.add(away);
|
|
48
|
+
}
|
|
49
|
+
const idle = entrants(n).filter(t => !playing.has(t));
|
|
50
|
+
expect(idle).toHaveLength(1);
|
|
51
|
+
byes.set(idle[0], (byes.get(idle[0]) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
expect(seen.size).toBe((n * (n - 1)) / 2);
|
|
54
|
+
// Exactly one bye each: nobody sits out twice while another entrant never rests.
|
|
55
|
+
expect([...byes.values()]).toEqual(Array.from({ length: n }, () => 1));
|
|
56
|
+
});
|
|
57
|
+
it.each([8, 16, 20, 50])('splits home and away near evenly across a field of %i', (n) => {
|
|
58
|
+
const homeCounts = new Map();
|
|
59
|
+
for (const round of roundRobinRounds(entrants(n))) {
|
|
60
|
+
for (const [home] of round)
|
|
61
|
+
homeCounts.set(home, (homeCounts.get(home) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
// n-1 games each, so half of them is the ideal. Folding the court index into the side parity used to pin
|
|
64
|
+
// some entrants to a single side for the whole event; this guards that regression.
|
|
65
|
+
for (const t of entrants(n)) {
|
|
66
|
+
expect(Math.abs((homeCounts.get(t) ?? 0) - (n - 1) / 2)).toBeLessThanOrEqual(2);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -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];
|