volleyballsimtypes 0.0.509 → 0.0.511

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.
@@ -35,13 +35,13 @@ function computeMatchRallyAggregates(input) {
35
35
  });
36
36
  return { rallyMetrics, breakdown };
37
37
  }
38
- // A block scores a point in exactly ONE place in the sim (reception-simulator: a dig of a block fails
39
- // and the block landed in-court, i.e. target >= 7). Each such rally is ONE block point regardless of how
40
- // many blockers were on it; the box score records it as one `block_assists` per blocker (or one
41
- // `block_solo`), which is why summing solo+assists over-counts block points. We rebuild the true count
42
- // from the rallies. blockSolo / blockAssists must reconstruct box.block.solo and box.block.assists
43
- // exactly (the correctness gate). `teamOf(playerId)` returns a player's team id (or null); the scoring
44
- // team is the blockers' team.
38
+ // A block scores a point when the block lands in-court (target >= 7): the positional engine ends the rally
39
+ // on that BLOCK event; the zone engine ends it on the failed dig (RECEPTION) right after the BLOCK. Each
40
+ // such rally is ONE block point regardless of how many blockers were on it; the box score records it as one
41
+ // `block_assists` per blocker (or one `block_solo`), which is why summing solo+assists over-counts block
42
+ // points. We rebuild the true count from the rallies. blockSolo / blockAssists must reconstruct box.block.solo
43
+ // and box.block.assists exactly (the correctness gate). `teamOf(playerId)` returns a player's team id (or
44
+ // null); the scoring team is the blockers' team.
45
45
  function tallyBlockPoints(rallies, teamOf) {
46
46
  const out = new Map();
47
47
  const tallyFor = (team) => {
@@ -56,17 +56,22 @@ function tallyBlockPoints(rallies, teamOf) {
56
56
  // Ignore substitutions / libero replacements (eventType < SERVE) when locating the terminal action.
57
57
  const inPlay = rally.events.filter(e => e.eventType >= event_1.EventTypeEnum.SERVE);
58
58
  const last = inPlay[inPlay.length - 1];
59
- const prev = inPlay[inPlay.length - 2];
60
- if (last == null || prev == null)
59
+ if (last == null)
61
60
  continue;
62
- // Block point signature: the rally ends on a failed RECEPTION (dig) whose previous event is a BLOCK
63
- // that landed in-court (target >= 7). target < 7 is a deflection -> the ATTACKER's kill, not a block.
64
- if (last.eventType !== event_1.EventTypeEnum.RECEPTION || last.failure <= 0)
65
- continue;
66
- if (prev.eventType !== event_1.EventTypeEnum.BLOCK)
67
- continue;
68
- const block = prev;
69
- if (block.target < 7)
61
+ // A block point lands in-court (target >= 7); a deflection / tool-out (target < 7) is the ATTACKER's kill.
62
+ // Two engines end a block point in different shapes, both counted here:
63
+ // - positional: the rally ENDS on the BLOCK itself (the stuff kills the ball outright);
64
+ // - zone: the rally ends on a failed RECEPTION (dig) whose previous event is the BLOCK.
65
+ let block;
66
+ if (last.eventType === event_1.EventTypeEnum.BLOCK) {
67
+ block = last;
68
+ }
69
+ else if (last.eventType === event_1.EventTypeEnum.RECEPTION && last.failure > 0) {
70
+ const prev = inPlay[inPlay.length - 2];
71
+ if (prev != null && prev.eventType === event_1.EventTypeEnum.BLOCK)
72
+ block = prev;
73
+ }
74
+ if (block == null || block.target < 7)
70
75
  continue;
71
76
  const team = teamOf(block.blockers[0] ?? block.playerId);
72
77
  if (team == null)
@@ -30,6 +30,14 @@ const teamOf = (id) => id.startsWith('h') ? 'HOME' : id.startsWith('a') ? 'AWAY'
30
30
  const t = (0, point_breakdown_1.tallyBlockPoints)([rally([blockEvent('h1', 8, ['h1', 'h2']), receptionEvent('a3', 0), spikeEvent('a3')])], teamOf);
31
31
  (0, globals_1.expect)(t.size).toBe(0);
32
32
  });
33
+ (0, globals_1.it)('counts a POSITIONAL stuff that ENDS on the BLOCK (no trailing dig)', () => {
34
+ const t = (0, point_breakdown_1.tallyBlockPoints)([rally([spikeEvent('a1'), blockEvent('h1', 9, ['h1', 'h2'])])], teamOf);
35
+ (0, globals_1.expect)(t.get('HOME')).toEqual({ blockPoints: 1, blockSolo: 0, blockAssists: 2 });
36
+ });
37
+ (0, globals_1.it)('does NOT count a terminal touch / tool-out block (target < 7)', () => {
38
+ const t = (0, point_breakdown_1.tallyBlockPoints)([rally([spikeEvent('a1'), blockEvent('h1', 0, ['h1'])])], teamOf);
39
+ (0, globals_1.expect)(t.size).toBe(0);
40
+ });
33
41
  (0, globals_1.it)('reconstructs box block.solo and block.assists across a mix (incl. a triple block)', () => {
34
42
  const t = (0, point_breakdown_1.tallyBlockPoints)([
35
43
  rally([blockEvent('h1', 8, ['h1', 'h2']), receptionEvent('a3', 1)]), // double: +2 assists
@@ -1,6 +1,7 @@
1
1
  export interface RallyMetricsEvent {
2
2
  readonly eventType: number;
3
3
  readonly failure?: number;
4
+ readonly type?: number;
4
5
  }
5
6
  export interface RallyMetricsRally {
6
7
  readonly servingTeamId: string;
@@ -14,7 +14,9 @@ function isServiceError(rally) {
14
14
  // engine scores a second spike as a transition). This counts first-ball kills and the block errors forced
15
15
  // on that first attack, and excludes service-error sideouts (no spike) and dig-and-transition rallies (2+).
16
16
  function spikeCount(rally) {
17
- return rally.events.reduce((n, e) => e.eventType === event_1.EventTypeEnum.SPIKE ? n + 1 : n, 0);
17
+ // An attack is a SPIKE, or a SET whose type is DUMP (the setter attacking on the second ball). Counting the
18
+ // dump matches the report builder's attack test, so a first-ball sideout won on a dump is not dropped.
19
+ return rally.events.reduce((n, e) => (e.eventType === event_1.EventTypeEnum.SPIKE || (e.eventType === event_1.EventTypeEnum.SET && e.type === event_1.SetTypeEnum.DUMP)) ? n + 1 : n, 0);
18
20
  }
19
21
  // The setter's zone after (rotationIndex - 1) rotations from their start zone, using the engine's own
20
22
  // rotatePosition (each rotation moves a player one zone: 2->1, 1->6, ...). null when no setter.
@@ -90,3 +90,25 @@ describe('computeRallyMetrics() first ball sideout', () => {
90
90
  expect(fm.total.home.modFbsoPct).toBe(0);
91
91
  });
92
92
  });
93
+ // The positional engine emits a setter DUMP as a SET event (type DUMP), not a SPIKE. A first-ball sideout won
94
+ // on a dump is still a first attack and must count as an FBSO. (Regression: spikeCount used to count only SPIKE.)
95
+ const dumpEv = () => ({ eventType: event_1.EventTypeEnum.SET, type: event_1.SetTypeEnum.DUMP, failure: 0 });
96
+ // serving H, A ; winners A, A (A 2 - H 0). A sides out on rally 0 via a single setter dump (no spike).
97
+ const dumpSet = {
98
+ homePoints: 0,
99
+ awayPoints: 2,
100
+ homeSetterStartZone: null,
101
+ awaySetterStartZone: null,
102
+ rallies: [
103
+ { servingTeamId: HOME, events: [serveEv(), dumpEv()] }, // A sideout on a dump -> A FBSO
104
+ { servingTeamId: AWAY, events: [serveEv()] } // A breakpoint (A serving) -> not a reception win
105
+ ]
106
+ };
107
+ const dm = (0, rally_metrics_1.computeRallyMetrics)([dumpSet], HOME, AWAY);
108
+ describe('computeRallyMetrics() first ball sideout - setter dump', () => {
109
+ it('counts a first-ball sideout won on a setter dump (SET type DUMP), not just a spike', () => {
110
+ expect(dm.total.away.receptions).toBe(1);
111
+ expect(dm.total.away.fbsoWins).toBe(1);
112
+ expect(dm.total.away.fbsoPct).toBeCloseTo(100, 5);
113
+ });
114
+ });
@@ -30,13 +30,13 @@ export function computeMatchRallyAggregates(input) {
30
30
  });
31
31
  return { rallyMetrics, breakdown };
32
32
  }
33
- // A block scores a point in exactly ONE place in the sim (reception-simulator: a dig of a block fails
34
- // and the block landed in-court, i.e. target >= 7). Each such rally is ONE block point regardless of how
35
- // many blockers were on it; the box score records it as one `block_assists` per blocker (or one
36
- // `block_solo`), which is why summing solo+assists over-counts block points. We rebuild the true count
37
- // from the rallies. blockSolo / blockAssists must reconstruct box.block.solo and box.block.assists
38
- // exactly (the correctness gate). `teamOf(playerId)` returns a player's team id (or null); the scoring
39
- // team is the blockers' team.
33
+ // A block scores a point when the block lands in-court (target >= 7): the positional engine ends the rally
34
+ // on that BLOCK event; the zone engine ends it on the failed dig (RECEPTION) right after the BLOCK. Each
35
+ // such rally is ONE block point regardless of how many blockers were on it; the box score records it as one
36
+ // `block_assists` per blocker (or one `block_solo`), which is why summing solo+assists over-counts block
37
+ // points. We rebuild the true count from the rallies. blockSolo / blockAssists must reconstruct box.block.solo
38
+ // and box.block.assists exactly (the correctness gate). `teamOf(playerId)` returns a player's team id (or
39
+ // null); the scoring team is the blockers' team.
40
40
  export function tallyBlockPoints(rallies, teamOf) {
41
41
  const out = new Map();
42
42
  const tallyFor = (team) => {
@@ -51,17 +51,22 @@ export function tallyBlockPoints(rallies, teamOf) {
51
51
  // Ignore substitutions / libero replacements (eventType < SERVE) when locating the terminal action.
52
52
  const inPlay = rally.events.filter(e => e.eventType >= EventTypeEnum.SERVE);
53
53
  const last = inPlay[inPlay.length - 1];
54
- const prev = inPlay[inPlay.length - 2];
55
- if (last == null || prev == null)
54
+ if (last == null)
56
55
  continue;
57
- // Block point signature: the rally ends on a failed RECEPTION (dig) whose previous event is a BLOCK
58
- // that landed in-court (target >= 7). target < 7 is a deflection -> the ATTACKER's kill, not a block.
59
- if (last.eventType !== EventTypeEnum.RECEPTION || last.failure <= 0)
60
- continue;
61
- if (prev.eventType !== EventTypeEnum.BLOCK)
62
- continue;
63
- const block = prev;
64
- if (block.target < 7)
56
+ // A block point lands in-court (target >= 7); a deflection / tool-out (target < 7) is the ATTACKER's kill.
57
+ // Two engines end a block point in different shapes, both counted here:
58
+ // - positional: the rally ENDS on the BLOCK itself (the stuff kills the ball outright);
59
+ // - zone: the rally ends on a failed RECEPTION (dig) whose previous event is the BLOCK.
60
+ let block;
61
+ if (last.eventType === EventTypeEnum.BLOCK) {
62
+ block = last;
63
+ }
64
+ else if (last.eventType === EventTypeEnum.RECEPTION && last.failure > 0) {
65
+ const prev = inPlay[inPlay.length - 2];
66
+ if (prev != null && prev.eventType === EventTypeEnum.BLOCK)
67
+ block = prev;
68
+ }
69
+ if (block == null || block.target < 7)
65
70
  continue;
66
71
  const team = teamOf(block.blockers[0] ?? block.playerId);
67
72
  if (team == null)
@@ -28,6 +28,14 @@ describe('tallyBlockPoints', () => {
28
28
  const t = tallyBlockPoints([rally([blockEvent('h1', 8, ['h1', 'h2']), receptionEvent('a3', 0), spikeEvent('a3')])], teamOf);
29
29
  expect(t.size).toBe(0);
30
30
  });
31
+ it('counts a POSITIONAL stuff that ENDS on the BLOCK (no trailing dig)', () => {
32
+ const t = tallyBlockPoints([rally([spikeEvent('a1'), blockEvent('h1', 9, ['h1', 'h2'])])], teamOf);
33
+ expect(t.get('HOME')).toEqual({ blockPoints: 1, blockSolo: 0, blockAssists: 2 });
34
+ });
35
+ it('does NOT count a terminal touch / tool-out block (target < 7)', () => {
36
+ const t = tallyBlockPoints([rally([spikeEvent('a1'), blockEvent('h1', 0, ['h1'])])], teamOf);
37
+ expect(t.size).toBe(0);
38
+ });
31
39
  it('reconstructs box block.solo and block.assists across a mix (incl. a triple block)', () => {
32
40
  const t = tallyBlockPoints([
33
41
  rally([blockEvent('h1', 8, ['h1', 'h2']), receptionEvent('a3', 1)]), // double: +2 assists
@@ -1,6 +1,7 @@
1
1
  export interface RallyMetricsEvent {
2
2
  readonly eventType: number;
3
3
  readonly failure?: number;
4
+ readonly type?: number;
4
5
  }
5
6
  export interface RallyMetricsRally {
6
7
  readonly servingTeamId: string;
@@ -1,4 +1,4 @@
1
- import { EventTypeEnum } from '../event';
1
+ import { EventTypeEnum, SetTypeEnum } from '../event';
2
2
  import { rotatePosition } from './court-position';
3
3
  function newAcc() {
4
4
  return { serves: 0, breakpointWins: 0, receptions: 0, sideoutWins: 0, serviceErrorsReceived: 0, fbsoWins: 0, rotations: new Map() };
@@ -11,7 +11,9 @@ function isServiceError(rally) {
11
11
  // engine scores a second spike as a transition). This counts first-ball kills and the block errors forced
12
12
  // on that first attack, and excludes service-error sideouts (no spike) and dig-and-transition rallies (2+).
13
13
  function spikeCount(rally) {
14
- return rally.events.reduce((n, e) => e.eventType === EventTypeEnum.SPIKE ? n + 1 : n, 0);
14
+ // An attack is a SPIKE, or a SET whose type is DUMP (the setter attacking on the second ball). Counting the
15
+ // dump matches the report builder's attack test, so a first-ball sideout won on a dump is not dropped.
16
+ return rally.events.reduce((n, e) => (e.eventType === EventTypeEnum.SPIKE || (e.eventType === EventTypeEnum.SET && e.type === SetTypeEnum.DUMP)) ? n + 1 : n, 0);
15
17
  }
16
18
  // The setter's zone after (rotationIndex - 1) rotations from their start zone, using the engine's own
17
19
  // rotatePosition (each rotation moves a player one zone: 2->1, 1->6, ...). null when no setter.
@@ -1,5 +1,5 @@
1
1
  import { computeRallyMetrics } from './rally-metrics';
2
- import { EventTypeEnum } from '../event';
2
+ import { EventTypeEnum, SetTypeEnum } from '../event';
3
3
  const serve = (failure = 0) => ({
4
4
  events: [{ eventType: EventTypeEnum.SERVE, failure }]
5
5
  });
@@ -88,3 +88,25 @@ describe('computeRallyMetrics() first ball sideout', () => {
88
88
  expect(fm.total.home.modFbsoPct).toBe(0);
89
89
  });
90
90
  });
91
+ // The positional engine emits a setter DUMP as a SET event (type DUMP), not a SPIKE. A first-ball sideout won
92
+ // on a dump is still a first attack and must count as an FBSO. (Regression: spikeCount used to count only SPIKE.)
93
+ const dumpEv = () => ({ eventType: EventTypeEnum.SET, type: SetTypeEnum.DUMP, failure: 0 });
94
+ // serving H, A ; winners A, A (A 2 - H 0). A sides out on rally 0 via a single setter dump (no spike).
95
+ const dumpSet = {
96
+ homePoints: 0,
97
+ awayPoints: 2,
98
+ homeSetterStartZone: null,
99
+ awaySetterStartZone: null,
100
+ rallies: [
101
+ { servingTeamId: HOME, events: [serveEv(), dumpEv()] }, // A sideout on a dump -> A FBSO
102
+ { servingTeamId: AWAY, events: [serveEv()] } // A breakpoint (A serving) -> not a reception win
103
+ ]
104
+ };
105
+ const dm = computeRallyMetrics([dumpSet], HOME, AWAY);
106
+ describe('computeRallyMetrics() first ball sideout - setter dump', () => {
107
+ it('counts a first-ball sideout won on a setter dump (SET type DUMP), not just a spike', () => {
108
+ expect(dm.total.away.receptions).toBe(1);
109
+ expect(dm.total.away.fbsoWins).toBe(1);
110
+ expect(dm.total.away.fbsoPct).toBeCloseTo(100, 5);
111
+ });
112
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.509",
3
+ "version": "0.0.511",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",