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.
- package/dist/cjs/src/service/match/point-breakdown.js +22 -17
- package/dist/cjs/src/service/match/point-breakdown.test.js +8 -0
- package/dist/cjs/src/service/match/rally-metrics.d.ts +1 -0
- package/dist/cjs/src/service/match/rally-metrics.js +3 -1
- package/dist/cjs/src/service/match/rally-metrics.test.js +22 -0
- package/dist/esm/src/service/match/point-breakdown.js +22 -17
- package/dist/esm/src/service/match/point-breakdown.test.js +8 -0
- package/dist/esm/src/service/match/rally-metrics.d.ts +1 -0
- package/dist/esm/src/service/match/rally-metrics.js +4 -2
- package/dist/esm/src/service/match/rally-metrics.test.js +23 -1
- package/package.json +1 -1
|
@@ -35,13 +35,13 @@ function computeMatchRallyAggregates(input) {
|
|
|
35
35
|
});
|
|
36
36
|
return { rallyMetrics, breakdown };
|
|
37
37
|
}
|
|
38
|
-
// A block scores a point
|
|
39
|
-
//
|
|
40
|
-
// many blockers were on it; the box score records it as one
|
|
41
|
-
// `block_solo`), which is why summing solo+assists over-counts block
|
|
42
|
-
// from the rallies. blockSolo / blockAssists must reconstruct box.block.solo
|
|
43
|
-
// exactly (the correctness gate). `teamOf(playerId)` returns a player's team id (or
|
|
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
|
-
|
|
60
|
-
if (last == null || prev == null)
|
|
59
|
+
if (last == null)
|
|
61
60
|
continue;
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
34
|
-
//
|
|
35
|
-
// many blockers were on it; the box score records it as one
|
|
36
|
-
// `block_solo`), which is why summing solo+assists over-counts block
|
|
37
|
-
// from the rallies. blockSolo / blockAssists must reconstruct box.block.solo
|
|
38
|
-
// exactly (the correctness gate). `teamOf(playerId)` returns a player's team id (or
|
|
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
|
-
|
|
55
|
-
if (last == null || prev == null)
|
|
54
|
+
if (last == null)
|
|
56
55
|
continue;
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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,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
|
-
|
|
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
|
+
});
|