volleyballsimtypes 0.0.423 → 0.0.424
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/rally-metrics.d.ts +3 -0
- package/dist/cjs/src/service/match/rally-metrics.js +12 -1
- package/dist/cjs/src/service/match/rally-metrics.test.js +33 -0
- package/dist/esm/src/service/match/rally-metrics.d.ts +3 -0
- package/dist/esm/src/service/match/rally-metrics.js +12 -1
- package/dist/esm/src/service/match/rally-metrics.test.js +33 -0
- package/package.json +1 -1
|
@@ -4,12 +4,18 @@ exports.computeRallyMetrics = computeRallyMetrics;
|
|
|
4
4
|
const event_1 = require("../event");
|
|
5
5
|
const court_position_1 = require("./court-position");
|
|
6
6
|
function newAcc() {
|
|
7
|
-
return { serves: 0, breakpointWins: 0, receptions: 0, sideoutWins: 0, serviceErrorsReceived: 0, rotations: new Map() };
|
|
7
|
+
return { serves: 0, breakpointWins: 0, receptions: 0, sideoutWins: 0, serviceErrorsReceived: 0, fbsoWins: 0, rotations: new Map() };
|
|
8
8
|
}
|
|
9
9
|
function isServiceError(rally) {
|
|
10
10
|
const serve = rally.events.find(e => e.eventType === event_1.EventTypeEnum.SERVE);
|
|
11
11
|
return serve != null && (serve.failure ?? 0) > 0;
|
|
12
12
|
}
|
|
13
|
+
// A first-ball sideout is won on the receiving team's first attack: the rally has exactly one spike (the
|
|
14
|
+
// engine scores a second spike as a transition). This counts first-ball kills and the block errors forced
|
|
15
|
+
// on that first attack, and excludes service-error sideouts (no spike) and dig-and-transition rallies (2+).
|
|
16
|
+
function spikeCount(rally) {
|
|
17
|
+
return rally.events.reduce((n, e) => e.eventType === event_1.EventTypeEnum.SPIKE ? n + 1 : n, 0);
|
|
18
|
+
}
|
|
13
19
|
// The setter's zone after (rotationIndex - 1) rotations from their start zone, using the engine's own
|
|
14
20
|
// rotatePosition (each rotation moves a player one zone: 2->1, 1->6, ...). null when no setter.
|
|
15
21
|
function setterZoneAt(startZone, rotationIndex) {
|
|
@@ -62,6 +68,8 @@ function accumulateSet(set, homeId, awayId, homeAcc, awayAcc) {
|
|
|
62
68
|
}
|
|
63
69
|
else {
|
|
64
70
|
receiving.sideoutWins++;
|
|
71
|
+
if (spikeCount(rallies[i]) === 1)
|
|
72
|
+
receiving.fbsoWins++;
|
|
65
73
|
addRotationPoint(winner, zone, rotIdx, 'sideout');
|
|
66
74
|
}
|
|
67
75
|
// The sideout winner gains serve, so they rotate (after the point is attributed to their old rotation).
|
|
@@ -86,6 +94,9 @@ function finalizeTeam(acc) {
|
|
|
86
94
|
modReceptions,
|
|
87
95
|
modSideoutWins,
|
|
88
96
|
modSideoutPct: modReceptions > 0 ? (modSideoutWins / modReceptions) * 100 : 0,
|
|
97
|
+
fbsoWins: acc.fbsoWins,
|
|
98
|
+
fbsoPct: acc.receptions > 0 ? (acc.fbsoWins / acc.receptions) * 100 : 0,
|
|
99
|
+
modFbsoPct: modReceptions > 0 ? (acc.fbsoWins / modReceptions) * 100 : 0,
|
|
89
100
|
rotations
|
|
90
101
|
};
|
|
91
102
|
}
|
|
@@ -57,3 +57,36 @@ describe('computeRallyMetrics()', () => {
|
|
|
57
57
|
]);
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
|
+
// First ball sideout: a sideout won on the receiving team's first attack (exactly one spike in the rally).
|
|
61
|
+
const serveEv = (failure = 0) => ({ eventType: event_1.EventTypeEnum.SERVE, failure });
|
|
62
|
+
const spikeEv = () => ({ eventType: event_1.EventTypeEnum.SPIKE, failure: 0 });
|
|
63
|
+
// serving H, A, H, A ; winners A, H, A, A (A 3 - H 1). Setter-agnostic, so no setters.
|
|
64
|
+
const fbsoSet = {
|
|
65
|
+
homePoints: 1,
|
|
66
|
+
awayPoints: 3,
|
|
67
|
+
homeSetterStartZone: null,
|
|
68
|
+
awaySetterStartZone: null,
|
|
69
|
+
rallies: [
|
|
70
|
+
{ servingTeamId: HOME, events: [serveEv(), spikeEv()] }, // A sideout on 1 spike -> A FBSO
|
|
71
|
+
{ servingTeamId: AWAY, events: [serveEv(), spikeEv(), spikeEv()] }, // H sideout on 2 spikes -> transition, not FBSO
|
|
72
|
+
{ servingTeamId: HOME, events: [serveEv(1)] }, // A sideout via H service error, 0 spikes -> not FBSO
|
|
73
|
+
{ servingTeamId: AWAY, events: [serveEv(), spikeEv()] } // A breakpoint (A serving) -> not a reception win
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
const fm = (0, rally_metrics_1.computeRallyMetrics)([fbsoSet], HOME, AWAY);
|
|
77
|
+
describe('computeRallyMetrics() first ball sideout', () => {
|
|
78
|
+
it('counts only sideouts won on the first attack (one spike)', () => {
|
|
79
|
+
expect(fm.total.away.receptions).toBe(2);
|
|
80
|
+
expect(fm.total.away.fbsoWins).toBe(1);
|
|
81
|
+
expect(fm.total.away.fbsoPct).toBeCloseTo(50, 5);
|
|
82
|
+
});
|
|
83
|
+
it('modified FBSO excludes opponent service errors from the denominator', () => {
|
|
84
|
+
expect(fm.total.away.serviceErrorsReceived).toBe(1);
|
|
85
|
+
expect(fm.total.away.modFbsoPct).toBeCloseTo(100, 5);
|
|
86
|
+
});
|
|
87
|
+
it('ignores transition sideouts (2+ spikes) and reception losses', () => {
|
|
88
|
+
expect(fm.total.home.fbsoWins).toBe(0);
|
|
89
|
+
expect(fm.total.home.fbsoPct).toBe(0);
|
|
90
|
+
expect(fm.total.home.modFbsoPct).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { EventTypeEnum } from '../event';
|
|
2
2
|
import { rotatePosition } from './court-position';
|
|
3
3
|
function newAcc() {
|
|
4
|
-
return { serves: 0, breakpointWins: 0, receptions: 0, sideoutWins: 0, serviceErrorsReceived: 0, rotations: new Map() };
|
|
4
|
+
return { serves: 0, breakpointWins: 0, receptions: 0, sideoutWins: 0, serviceErrorsReceived: 0, fbsoWins: 0, rotations: new Map() };
|
|
5
5
|
}
|
|
6
6
|
function isServiceError(rally) {
|
|
7
7
|
const serve = rally.events.find(e => e.eventType === EventTypeEnum.SERVE);
|
|
8
8
|
return serve != null && (serve.failure ?? 0) > 0;
|
|
9
9
|
}
|
|
10
|
+
// A first-ball sideout is won on the receiving team's first attack: the rally has exactly one spike (the
|
|
11
|
+
// engine scores a second spike as a transition). This counts first-ball kills and the block errors forced
|
|
12
|
+
// on that first attack, and excludes service-error sideouts (no spike) and dig-and-transition rallies (2+).
|
|
13
|
+
function spikeCount(rally) {
|
|
14
|
+
return rally.events.reduce((n, e) => e.eventType === EventTypeEnum.SPIKE ? n + 1 : n, 0);
|
|
15
|
+
}
|
|
10
16
|
// The setter's zone after (rotationIndex - 1) rotations from their start zone, using the engine's own
|
|
11
17
|
// rotatePosition (each rotation moves a player one zone: 2->1, 1->6, ...). null when no setter.
|
|
12
18
|
function setterZoneAt(startZone, rotationIndex) {
|
|
@@ -59,6 +65,8 @@ function accumulateSet(set, homeId, awayId, homeAcc, awayAcc) {
|
|
|
59
65
|
}
|
|
60
66
|
else {
|
|
61
67
|
receiving.sideoutWins++;
|
|
68
|
+
if (spikeCount(rallies[i]) === 1)
|
|
69
|
+
receiving.fbsoWins++;
|
|
62
70
|
addRotationPoint(winner, zone, rotIdx, 'sideout');
|
|
63
71
|
}
|
|
64
72
|
// The sideout winner gains serve, so they rotate (after the point is attributed to their old rotation).
|
|
@@ -83,6 +91,9 @@ function finalizeTeam(acc) {
|
|
|
83
91
|
modReceptions,
|
|
84
92
|
modSideoutWins,
|
|
85
93
|
modSideoutPct: modReceptions > 0 ? (modSideoutWins / modReceptions) * 100 : 0,
|
|
94
|
+
fbsoWins: acc.fbsoWins,
|
|
95
|
+
fbsoPct: acc.receptions > 0 ? (acc.fbsoWins / acc.receptions) * 100 : 0,
|
|
96
|
+
modFbsoPct: modReceptions > 0 ? (acc.fbsoWins / modReceptions) * 100 : 0,
|
|
86
97
|
rotations
|
|
87
98
|
};
|
|
88
99
|
}
|
|
@@ -55,3 +55,36 @@ describe('computeRallyMetrics()', () => {
|
|
|
55
55
|
]);
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
|
+
// First ball sideout: a sideout won on the receiving team's first attack (exactly one spike in the rally).
|
|
59
|
+
const serveEv = (failure = 0) => ({ eventType: EventTypeEnum.SERVE, failure });
|
|
60
|
+
const spikeEv = () => ({ eventType: EventTypeEnum.SPIKE, failure: 0 });
|
|
61
|
+
// serving H, A, H, A ; winners A, H, A, A (A 3 - H 1). Setter-agnostic, so no setters.
|
|
62
|
+
const fbsoSet = {
|
|
63
|
+
homePoints: 1,
|
|
64
|
+
awayPoints: 3,
|
|
65
|
+
homeSetterStartZone: null,
|
|
66
|
+
awaySetterStartZone: null,
|
|
67
|
+
rallies: [
|
|
68
|
+
{ servingTeamId: HOME, events: [serveEv(), spikeEv()] }, // A sideout on 1 spike -> A FBSO
|
|
69
|
+
{ servingTeamId: AWAY, events: [serveEv(), spikeEv(), spikeEv()] }, // H sideout on 2 spikes -> transition, not FBSO
|
|
70
|
+
{ servingTeamId: HOME, events: [serveEv(1)] }, // A sideout via H service error, 0 spikes -> not FBSO
|
|
71
|
+
{ servingTeamId: AWAY, events: [serveEv(), spikeEv()] } // A breakpoint (A serving) -> not a reception win
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
const fm = computeRallyMetrics([fbsoSet], HOME, AWAY);
|
|
75
|
+
describe('computeRallyMetrics() first ball sideout', () => {
|
|
76
|
+
it('counts only sideouts won on the first attack (one spike)', () => {
|
|
77
|
+
expect(fm.total.away.receptions).toBe(2);
|
|
78
|
+
expect(fm.total.away.fbsoWins).toBe(1);
|
|
79
|
+
expect(fm.total.away.fbsoPct).toBeCloseTo(50, 5);
|
|
80
|
+
});
|
|
81
|
+
it('modified FBSO excludes opponent service errors from the denominator', () => {
|
|
82
|
+
expect(fm.total.away.serviceErrorsReceived).toBe(1);
|
|
83
|
+
expect(fm.total.away.modFbsoPct).toBeCloseTo(100, 5);
|
|
84
|
+
});
|
|
85
|
+
it('ignores transition sideouts (2+ spikes) and reception losses', () => {
|
|
86
|
+
expect(fm.total.home.fbsoWins).toBe(0);
|
|
87
|
+
expect(fm.total.home.fbsoPct).toBe(0);
|
|
88
|
+
expect(fm.total.home.modFbsoPct).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
});
|