volleyballsimtypes 0.0.496 → 0.0.498

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.
@@ -1,19 +1,35 @@
1
1
  import { Country } from '../country';
2
2
  import { Player, Rarity, Role } from '../player';
3
- /** A pack targets one role, or ANY for the single free pick at the end. */
4
- export type DraftRole = Role | 'ANY';
5
- /** How many players an opened pack offers. */
3
+ /**
4
+ * The pack that is not targeted at a position: the single free pick at the end. Stored on the pack row and
5
+ * read by the view to label it, so it is a named constant rather than a bare string in three repos.
6
+ */
7
+ export declare const FREE_PACK_ROLE = "ANY";
8
+ /** A pack targets one role, or the free pick. */
9
+ export type DraftRole = Role | typeof FREE_PACK_ROLE;
10
+ /** How many players a role-targeted pack offers. */
6
11
  export declare const PACK_SIZE = 4;
7
12
  /**
8
- * The pack sequence, in the order they are opened. One pick each, so this is also the squad: 2 setters,
9
- * 4 outsides, 3 middles, 2 opposites, 2 liberos, plus one free pick, for a squad of 14 like every other team.
13
+ * The pack sequence, in the order they are opened. One pick each, so this is also the squad: a full
14
+ * 14-player team by position (2 setters, 4 outsides, 4 middles, 2 opposites, 2 liberos), and then a
15
+ * fifteenth free pick on top of it.
10
16
  *
11
17
  * Role-targeted rather than free choice on purpose: it makes a positionally valid squad impossible to get
12
- * wrong, so nobody drafts six liberos and then cannot field a legal lineup.
18
+ * wrong, so nobody drafts six liberos and then cannot field a legal lineup. The fourteen targeted packs
19
+ * stand on their own as a complete squad, which is why the free pick can be a genuine bonus rather than a
20
+ * slot the squad depends on.
13
21
  */
14
22
  export declare const DRAFT_PLAN: readonly DraftRole[];
15
23
  /** The size of a drafted squad: one pick per pack. */
16
24
  export declare const DRAFT_SQUAD_SIZE: number;
25
+ /**
26
+ * How many packs are targeted at a position, which is every pack but the free one. Derived rather than typed
27
+ * out because the copy describing the draft quotes it, and prose that repeats a constant is prose that drifts
28
+ * away from it.
29
+ */
30
+ export declare const TARGETED_PACK_COUNT: number;
31
+ /** How many players the free pack offers: one per position. */
32
+ export declare const FREE_PACK_SIZE: number;
17
33
  /**
18
34
  * How hard each rarity's recruits are skewed toward the top of their range when drafted in an event.
19
35
  *
@@ -27,14 +43,13 @@ export declare const DRAFT_SQUAD_SIZE: number;
27
43
  */
28
44
  export declare const EVENT_DRAFT_TOP_SKEW: Record<Rarity, number>;
29
45
  /**
30
- * The players a pack offers: `PACK_SIZE` of the pack's role at the event's rarity. The lower rarities are
31
- * generated with a top-end skew (EVENT_DRAFT_TOP_SKEW) so their recruits cluster near the best; legendaries
32
- * are left uniform.
46
+ * The players a pack offers, at the event's rarity. A role-targeted pack offers `PACK_SIZE` of that one role;
47
+ * the free pack offers `FREE_PACK_SIZE`, one per position, so the choice is which position to add to.
33
48
  *
34
- * `pickRole` decides what an ANY pack rolls; it is passed in rather than chosen here so the caller controls the
35
- * randomness (and a test can make it deterministic).
49
+ * The lower rarities are generated with a top-end skew (EVENT_DRAFT_TOP_SKEW) so their recruits cluster near
50
+ * the best; legendaries are left uniform.
36
51
  */
37
- export declare function generatePack(country: Country, rarity: Rarity, packRole: DraftRole, pickRole: (roles: readonly Role[]) => Role): Player[];
52
+ export declare function generatePack(country: Country, rarity: Rarity, packRole: DraftRole): Player[];
38
53
  /** The role of the pack at `index`, or undefined once the draft is complete. */
39
54
  export declare function draftPackRole(index: number): DraftRole | undefined;
40
55
  export declare function isDraftComplete(picks: number): boolean;
@@ -1,33 +1,61 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EVENT_DRAFT_TOP_SKEW = exports.DRAFT_SQUAD_SIZE = exports.DRAFT_PLAN = exports.PACK_SIZE = void 0;
3
+ exports.EVENT_DRAFT_TOP_SKEW = exports.FREE_PACK_SIZE = exports.TARGETED_PACK_COUNT = exports.DRAFT_SQUAD_SIZE = exports.DRAFT_PLAN = exports.PACK_SIZE = exports.FREE_PACK_ROLE = void 0;
4
4
  exports.generatePack = generatePack;
5
5
  exports.draftPackRole = draftPackRole;
6
6
  exports.isDraftComplete = isDraftComplete;
7
7
  const player_1 = require("../player");
8
- /** How many players an opened pack offers. */
8
+ // The event draft. An entrant does not bring their own squad: they open a fixed sequence of role-targeted
9
+ // packs, keep one player from each, and the resulting team is thrown away when the event ends.
10
+ //
11
+ // This module is pure: the pack sequence and pack generation. Persistence (which packs a user has opened, what
12
+ // they picked) lives in the API, because the draft is user-driven rather than scheduled.
13
+ /**
14
+ * The pack that is not targeted at a position: the single free pick at the end. Stored on the pack row and
15
+ * read by the view to label it, so it is a named constant rather than a bare string in three repos.
16
+ */
17
+ exports.FREE_PACK_ROLE = 'ANY';
18
+ /** How many players a role-targeted pack offers. */
9
19
  exports.PACK_SIZE = 4;
10
20
  /**
11
- * The pack sequence, in the order they are opened. One pick each, so this is also the squad: 2 setters,
12
- * 4 outsides, 3 middles, 2 opposites, 2 liberos, plus one free pick, for a squad of 14 like every other team.
21
+ * The pack sequence, in the order they are opened. One pick each, so this is also the squad: a full
22
+ * 14-player team by position (2 setters, 4 outsides, 4 middles, 2 opposites, 2 liberos), and then a
23
+ * fifteenth free pick on top of it.
13
24
  *
14
25
  * Role-targeted rather than free choice on purpose: it makes a positionally valid squad impossible to get
15
- * wrong, so nobody drafts six liberos and then cannot field a legal lineup.
26
+ * wrong, so nobody drafts six liberos and then cannot field a legal lineup. The fourteen targeted packs
27
+ * stand on their own as a complete squad, which is why the free pick can be a genuine bonus rather than a
28
+ * slot the squad depends on.
16
29
  */
17
30
  exports.DRAFT_PLAN = [
18
31
  player_1.RoleEnum.SETTER, player_1.RoleEnum.SETTER,
19
32
  player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.OUTSIDE_HITTER,
20
- player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER,
33
+ player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER,
21
34
  player_1.RoleEnum.OPPOSITE_HITTER, player_1.RoleEnum.OPPOSITE_HITTER,
22
35
  player_1.RoleEnum.LIBERO, player_1.RoleEnum.LIBERO,
23
- 'ANY'
36
+ exports.FREE_PACK_ROLE
24
37
  ];
25
38
  /** The size of a drafted squad: one pick per pack. */
26
39
  exports.DRAFT_SQUAD_SIZE = exports.DRAFT_PLAN.length;
27
- /** The role a free pack rolls its choices at, so an ANY pack still offers a coherent set. */
40
+ /**
41
+ * How many packs are targeted at a position, which is every pack but the free one. Derived rather than typed
42
+ * out because the copy describing the draft quotes it, and prose that repeats a constant is prose that drifts
43
+ * away from it.
44
+ */
45
+ exports.TARGETED_PACK_COUNT = exports.DRAFT_PLAN.filter(role => role !== exports.FREE_PACK_ROLE).length;
46
+ /**
47
+ * The free pack offers one player per position, in this order, so the entrant chooses which position to
48
+ * strengthen rather than being handed a position at random.
49
+ *
50
+ * It used to roll ONE role at random and offer four of it, which meant a 1-in-5 lottery decided whether an
51
+ * entrant ended up with a third setter or a fifth outside. The squad is complete without this pick now, so
52
+ * offering the whole board is both fairer and the point of calling it free.
53
+ */
28
54
  const ANY_PACK_ROLES = [
29
55
  player_1.RoleEnum.SETTER, player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.OPPOSITE_HITTER, player_1.RoleEnum.LIBERO
30
56
  ];
57
+ /** How many players the free pack offers: one per position. */
58
+ exports.FREE_PACK_SIZE = ANY_PACK_ROLES.length;
31
59
  /**
32
60
  * How hard each rarity's recruits are skewed toward the top of their range when drafted in an event.
33
61
  *
@@ -47,17 +75,18 @@ exports.EVENT_DRAFT_TOP_SKEW = {
47
75
  [player_1.RarityEnum.SPECIAL]: 0
48
76
  };
49
77
  /**
50
- * The players a pack offers: `PACK_SIZE` of the pack's role at the event's rarity. The lower rarities are
51
- * generated with a top-end skew (EVENT_DRAFT_TOP_SKEW) so their recruits cluster near the best; legendaries
52
- * are left uniform.
78
+ * The players a pack offers, at the event's rarity. A role-targeted pack offers `PACK_SIZE` of that one role;
79
+ * the free pack offers `FREE_PACK_SIZE`, one per position, so the choice is which position to add to.
53
80
  *
54
- * `pickRole` decides what an ANY pack rolls; it is passed in rather than chosen here so the caller controls the
55
- * randomness (and a test can make it deterministic).
81
+ * The lower rarities are generated with a top-end skew (EVENT_DRAFT_TOP_SKEW) so their recruits cluster near
82
+ * the best; legendaries are left uniform.
56
83
  */
57
- function generatePack(country, rarity, packRole, pickRole) {
58
- const role = packRole === 'ANY' ? pickRole(ANY_PACK_ROLES) : packRole;
84
+ function generatePack(country, rarity, packRole) {
59
85
  const skew = exports.EVENT_DRAFT_TOP_SKEW[rarity] ?? 0;
60
- return Array.from({ length: exports.PACK_SIZE }, () => player_1.PlayerGenerator.generatePlayer(country, rarity, role, false, 1, skew));
86
+ const roll = (role) => player_1.PlayerGenerator.generatePlayer(country, rarity, role, false, 1, skew);
87
+ if (packRole === exports.FREE_PACK_ROLE)
88
+ return ANY_PACK_ROLES.map(roll);
89
+ return Array.from({ length: exports.PACK_SIZE }, () => roll(packRole));
61
90
  }
62
91
  /** The role of the pack at `index`, or undefined once the draft is complete. */
63
92
  function draftPackRole(index) {
@@ -5,7 +5,6 @@ const event_draft_1 = require("./event-draft");
5
5
  const player_1 = require("../player");
6
6
  const test_helpers_1 = require("../test-helpers");
7
7
  const COUNTRY = (0, test_helpers_1.makeCountry)();
8
- const first = (roles) => roles[0];
9
8
  // Aggregate helpers: skew is statistical, so the behaviour tests use large samples and generous margins to stay
10
9
  // off the flaky edge while still failing loudly if the skew were removed.
11
10
  function median(values) {
@@ -20,7 +19,7 @@ function percentile(values, p) {
20
19
  function draftScores(rarity, role, n) {
21
20
  const out = [];
22
21
  while (out.length < n) {
23
- for (const p of (0, event_draft_1.generatePack)(COUNTRY, rarity, role, first))
22
+ for (const p of (0, event_draft_1.generatePack)(COUNTRY, rarity, role))
24
23
  out.push((0, player_1.calculateRoleScore)(p.stats, role));
25
24
  }
26
25
  return out.slice(0, n);
@@ -30,28 +29,31 @@ function uniformScores(rarity, role, n) {
30
29
  return Array.from({ length: n }, () => (0, player_1.calculateRoleScore)(player_1.PlayerGenerator.generatePlayer(COUNTRY, rarity, role, false, 1, 0).stats, role));
31
30
  }
32
31
  (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);
32
+ (0, globals_1.it)('drafts a 15-player squad, one pick per pack', () => {
33
+ (0, globals_1.expect)(event_draft_1.DRAFT_SQUAD_SIZE).toBe(15);
34
+ (0, globals_1.expect)(event_draft_1.DRAFT_PLAN).toHaveLength(15);
36
35
  });
37
- (0, globals_1.it)('guarantees a positionally valid squad', () => {
36
+ (0, globals_1.it)('fills every position outright, then adds the free pick on top', () => {
38
37
  const counts = event_draft_1.DRAFT_PLAN.reduce((acc, role) => {
39
38
  acc[role] = (acc[role] ?? 0) + 1;
40
39
  return acc;
41
40
  }, {});
42
41
  (0, globals_1.expect)(counts[player_1.RoleEnum.SETTER]).toBe(2);
43
42
  (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);
43
+ (0, globals_1.expect)(counts[player_1.RoleEnum.MIDDLE_BLOCKER]).toBe(4);
45
44
  (0, globals_1.expect)(counts[player_1.RoleEnum.OPPOSITE_HITTER]).toBe(2);
46
45
  (0, globals_1.expect)(counts[player_1.RoleEnum.LIBERO]).toBe(2);
47
46
  (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);
47
+ // The targeted packs alone are a complete 14-player squad, so the free pick adds to a squad that is
48
+ // already legal rather than filling a hole in it.
49
+ const targeted = event_draft_1.DRAFT_PLAN.filter(role => role !== event_draft_1.FREE_PACK_ROLE);
50
+ (0, globals_1.expect)(targeted).toHaveLength(14);
51
+ (0, globals_1.expect)(event_draft_1.TARGETED_PACK_COUNT).toBe(14);
52
+ (0, globals_1.expect)(event_draft_1.TARGETED_PACK_COUNT).toBe(event_draft_1.DRAFT_SQUAD_SIZE - 1);
53
+ });
54
+ (0, globals_1.it)('puts the free pick last, so nothing after it depends on what was taken', () => {
55
+ (0, globals_1.expect)(event_draft_1.DRAFT_PLAN[event_draft_1.DRAFT_PLAN.length - 1]).toBe(event_draft_1.FREE_PACK_ROLE);
56
+ (0, globals_1.expect)(event_draft_1.DRAFT_PLAN.filter(role => role === event_draft_1.FREE_PACK_ROLE)).toHaveLength(1);
55
57
  });
56
58
  (0, globals_1.it)('walks pack by pack and then reports the draft complete', () => {
57
59
  for (let i = 0; i < event_draft_1.DRAFT_SQUAD_SIZE; i++) {
@@ -65,18 +67,32 @@ function uniformScores(rarity, role, n) {
65
67
  (0, globals_1.describe)('generatePack()', () => {
66
68
  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
69
  const role = player_1.RoleEnum.OUTSIDE_HITTER;
68
- const pack = (0, event_draft_1.generatePack)(COUNTRY, rarity, role, first);
70
+ const pack = (0, event_draft_1.generatePack)(COUNTRY, rarity, role);
69
71
  (0, globals_1.expect)(pack).toHaveLength(event_draft_1.PACK_SIZE);
70
72
  for (const player of pack) {
71
73
  (0, globals_1.expect)(player.rarity).toBe(rarity);
72
74
  (0, globals_1.expect)(player.roles).toContain(role);
73
75
  }
74
76
  });
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);
77
+ (0, globals_1.it)('offers one player per position in the free pack, so every position is on the board', () => {
78
+ const pack = (0, event_draft_1.generatePack)(COUNTRY, player_1.RarityEnum.COMMON, event_draft_1.FREE_PACK_ROLE);
79
+ (0, globals_1.expect)(pack).toHaveLength(event_draft_1.FREE_PACK_SIZE);
80
+ (0, globals_1.expect)(event_draft_1.FREE_PACK_SIZE).toBe(5);
81
+ // The primary role is the one the pack was rolled at, and all five positions appear exactly once.
82
+ const primary = pack.map(p => p.roles[0]);
83
+ (0, globals_1.expect)([...primary].sort()).toEqual([
84
+ player_1.RoleEnum.LIBERO, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.OPPOSITE_HITTER, player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.SETTER
85
+ ].sort());
86
+ });
87
+ globals_1.it.each([player_1.RarityEnum.COMMON, player_1.RarityEnum.RARE, player_1.RarityEnum.LEGENDARY])('rolls the free pack at the event rarity (%s), like every other pack', (rarity) => {
88
+ const pack = (0, event_draft_1.generatePack)(COUNTRY, rarity, event_draft_1.FREE_PACK_ROLE);
89
+ (0, globals_1.expect)(pack).toHaveLength(event_draft_1.FREE_PACK_SIZE);
78
90
  for (const player of pack)
79
- (0, globals_1.expect)(player.roles).toContain(player_1.RoleEnum.LIBERO);
91
+ (0, globals_1.expect)(player.rarity).toBe(rarity);
92
+ });
93
+ (0, globals_1.it)('never offers the same player twice in a free pack', () => {
94
+ const pack = (0, event_draft_1.generatePack)(COUNTRY, player_1.RarityEnum.RARE, event_draft_1.FREE_PACK_ROLE);
95
+ (0, globals_1.expect)(new Set(pack.map(p => p.id)).size).toBe(event_draft_1.FREE_PACK_SIZE);
80
96
  });
81
97
  });
82
98
  (0, globals_1.describe)('EVENT_DRAFT_TOP_SKEW', () => {
@@ -31,10 +31,14 @@ function getMultipliers(statName, performanceStats) {
31
31
  };
32
32
  case StatsEnum.SET:
33
33
  return {
34
- setting: 0.75,
34
+ setting: 0.73,
35
+ // fast-tempo-gates: a setter's `quick` is what lets them run a fast offence, so it belongs in the SET
36
+ // rating AND in the SET training pool. Without it a setter on SET focus can never raise `quick` (it has
37
+ // weight 0 in every role too), and their whole tempo game would be fixed forever by a generation roll.
38
+ quick: 0.07,
35
39
  overhand: 0.1,
36
- awareness: 0.075,
37
- attack: 0.075
40
+ awareness: 0.05,
41
+ attack: 0.05
38
42
  };
39
43
  case StatsEnum.RECEIVE:
40
44
  return {