volleyballsimtypes 0.0.390 → 0.0.393
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/schemas/match-set.z.js +2 -2
- package/dist/cjs/src/service/match/vper.d.ts +3 -0
- package/dist/cjs/src/service/match/vper.js +11 -1
- package/dist/cjs/src/service/match/vper.test.js +34 -0
- package/dist/cjs/src/service/team/schemas/designated-sub.z.test.d.ts +1 -0
- package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +122 -0
- package/dist/esm/src/service/match/schemas/match-set.z.js +2 -2
- package/dist/esm/src/service/match/vper.d.ts +3 -0
- package/dist/esm/src/service/match/vper.js +10 -0
- package/dist/esm/src/service/match/vper.test.js +35 -1
- package/dist/esm/src/service/team/schemas/designated-sub.z.test.d.ts +1 -0
- package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +120 -0
- package/package.json +1 -1
|
@@ -18,8 +18,8 @@ exports.MatchSetInputSchema = zod_1.z.object({
|
|
|
18
18
|
id: zod_1.z.uuid(),
|
|
19
19
|
order: orderSchema,
|
|
20
20
|
isTieBreak: zod_1.z.boolean(),
|
|
21
|
-
homePlayerPosition: zod_1.z.array(PlayerPositionSchema).max(
|
|
22
|
-
awayPlayerPosition: zod_1.z.array(PlayerPositionSchema).max(
|
|
21
|
+
homePlayerPosition: zod_1.z.array(PlayerPositionSchema).max(14),
|
|
22
|
+
awayPlayerPosition: zod_1.z.array(PlayerPositionSchema).max(14),
|
|
23
23
|
homeScore: nonNegInt,
|
|
24
24
|
awayScore: nonNegInt,
|
|
25
25
|
boxScores: zod_1.z.array(zod_1.z.custom((v) => v instanceof box_score_1.BoxScore, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BoxScore } from './box-score';
|
|
2
|
+
export declare const MIN_POINTS_PLAYED_FOR_VPER = 35;
|
|
2
3
|
export interface VPERParams {
|
|
3
4
|
readonly playerId: string;
|
|
4
5
|
readonly matchId: string;
|
|
@@ -11,4 +12,6 @@ export declare class VPER {
|
|
|
11
12
|
static create(input: unknown): VPER;
|
|
12
13
|
private constructor();
|
|
13
14
|
static computeVPERForMatch(boxScores: BoxScore[]): number;
|
|
15
|
+
static totalPointsPlayed(boxScores: BoxScore[]): number;
|
|
16
|
+
static qualifiesForVPER(boxScores: BoxScore[]): boolean;
|
|
14
17
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VPER = void 0;
|
|
3
|
+
exports.VPER = exports.MIN_POINTS_PLAYED_FOR_VPER = void 0;
|
|
4
4
|
const vper_z_1 = require("./schemas/vper.z");
|
|
5
5
|
const WEIGHTS = {
|
|
6
6
|
// terminal positive
|
|
@@ -20,6 +20,10 @@ const WEIGHTS = {
|
|
|
20
20
|
// terminal errors (always point against)
|
|
21
21
|
TERMINAL_ERROR: -1.0
|
|
22
22
|
};
|
|
23
|
+
// Minimum points a player must be on court for in a match to receive a VPER. VPER is a per-100-points rate,
|
|
24
|
+
// so below a meaningful sample one good touch is extrapolated into a wildly inflated value. Players under this
|
|
25
|
+
// threshold get no VPER for the match.
|
|
26
|
+
exports.MIN_POINTS_PLAYED_FOR_VPER = 35;
|
|
23
27
|
class VPER {
|
|
24
28
|
static create(input) {
|
|
25
29
|
const result = vper_z_1.VPERInputSchema.safeParse(input);
|
|
@@ -65,5 +69,11 @@ class VPER {
|
|
|
65
69
|
}
|
|
66
70
|
return 100 * rawSum / Math.max(1, pointsPlayedSum);
|
|
67
71
|
}
|
|
72
|
+
static totalPointsPlayed(boxScores) {
|
|
73
|
+
return boxScores.reduce((sum, box) => sum + box.misc.pointsPlayed, 0);
|
|
74
|
+
}
|
|
75
|
+
static qualifiesForVPER(boxScores) {
|
|
76
|
+
return VPER.totalPointsPlayed(boxScores) >= exports.MIN_POINTS_PLAYED_FOR_VPER;
|
|
77
|
+
}
|
|
68
78
|
}
|
|
69
79
|
exports.VPER = VPER;
|
|
@@ -106,3 +106,37 @@ function makeBox() {
|
|
|
106
106
|
(0, globals_1.expect)(vper_1.VPER.computeVPERForMatch([box])).toBeCloseTo(14, 5);
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
|
+
// ─── VPER eligibility threshold ───────────────────────────────────────────────
|
|
110
|
+
(0, globals_1.describe)('VPER.qualifiesForVPER()', () => {
|
|
111
|
+
function boxWithPoints(points) {
|
|
112
|
+
const box = makeBox();
|
|
113
|
+
box.misc.pointsPlayed = points;
|
|
114
|
+
return box;
|
|
115
|
+
}
|
|
116
|
+
(0, globals_1.it)('uses a 35-point threshold', () => {
|
|
117
|
+
(0, globals_1.expect)(vper_1.MIN_POINTS_PLAYED_FOR_VPER).toBe(35);
|
|
118
|
+
});
|
|
119
|
+
(0, globals_1.it)('sums points played across box scores', () => {
|
|
120
|
+
(0, globals_1.expect)(vper_1.VPER.totalPointsPlayed([boxWithPoints(20), boxWithPoints(15)])).toBe(35);
|
|
121
|
+
});
|
|
122
|
+
(0, globals_1.it)('does not qualify an empty box score list', () => {
|
|
123
|
+
(0, globals_1.expect)(vper_1.VPER.qualifiesForVPER([])).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
(0, globals_1.it)('does not qualify below the threshold', () => {
|
|
126
|
+
(0, globals_1.expect)(vper_1.VPER.qualifiesForVPER([boxWithPoints(34)])).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
(0, globals_1.it)('qualifies exactly at the threshold (inclusive)', () => {
|
|
129
|
+
(0, globals_1.expect)(vper_1.VPER.qualifiesForVPER([boxWithPoints(35)])).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
(0, globals_1.it)('qualifies above the threshold across multiple sets', () => {
|
|
132
|
+
(0, globals_1.expect)(vper_1.VPER.qualifiesForVPER([boxWithPoints(20), boxWithPoints(20)])).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
(0, globals_1.it)('rejects the inflated-cameo case (one good touch in a single point)', () => {
|
|
135
|
+
const box = makeBox();
|
|
136
|
+
box.setting.assists = 1;
|
|
137
|
+
box.setting.pass = 1;
|
|
138
|
+
box.misc.pointsPlayed = 1;
|
|
139
|
+
// Pre-fix this produced a VPER of 85; now the player is simply ineligible.
|
|
140
|
+
(0, globals_1.expect)(vper_1.VPER.qualifiesForVPER([box])).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const designated_sub_z_1 = require("./designated-sub.z");
|
|
5
|
+
const energy_band_1 = require("../energy-band");
|
|
6
|
+
const pinch_condition_1 = require("../pinch-condition");
|
|
7
|
+
const test_helpers_1 = require("../../test-helpers");
|
|
8
|
+
(0, globals_1.describe)('SubBandSchema', () => {
|
|
9
|
+
(0, globals_1.it)('accepts every energy band and NEVER', () => {
|
|
10
|
+
const bands = [energy_band_1.EnergyBand.ENERGETIC, energy_band_1.EnergyBand.WORN, energy_band_1.EnergyBand.TIRED, energy_band_1.EnergyBand.EXHAUSTED, 'NEVER'];
|
|
11
|
+
for (const band of bands) {
|
|
12
|
+
(0, globals_1.expect)(designated_sub_z_1.SubBandSchema.safeParse(band).success).toBe(true);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
(0, globals_1.it)('rejects an unknown band', () => {
|
|
16
|
+
(0, globals_1.expect)(designated_sub_z_1.SubBandSchema.safeParse('SLEEPY').success).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
(0, globals_1.describe)('ConditionLogicSchema', () => {
|
|
20
|
+
(0, globals_1.it)('accepts ALL and ANY', () => {
|
|
21
|
+
(0, globals_1.expect)(designated_sub_z_1.ConditionLogicSchema.safeParse('ALL').success).toBe(true);
|
|
22
|
+
(0, globals_1.expect)(designated_sub_z_1.ConditionLogicSchema.safeParse('ANY').success).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
(0, globals_1.it)('rejects anything else', () => {
|
|
25
|
+
(0, globals_1.expect)(designated_sub_z_1.ConditionLogicSchema.safeParse('SOMETIMES').success).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
(0, globals_1.describe)('PinchConditionSchema', () => {
|
|
29
|
+
const valid = [
|
|
30
|
+
{ type: pinch_condition_1.PinchConditionType.ASAP },
|
|
31
|
+
{ type: pinch_condition_1.PinchConditionType.TEAM_SET_POINT, margin: 2, opponent: pinch_condition_1.OpponentRelation.EITHER },
|
|
32
|
+
{ type: pinch_condition_1.PinchConditionType.OPPONENT_SET_POINT, margin: 1 },
|
|
33
|
+
{ type: pinch_condition_1.PinchConditionType.SCORE_DIFF, direction: pinch_condition_1.ScoreDirection.TRAILING, points: 3 },
|
|
34
|
+
{ type: pinch_condition_1.PinchConditionType.AFTER_ROTATIONS, rotations: 1 },
|
|
35
|
+
{ type: pinch_condition_1.PinchConditionType.FROM_SET, set: 5 },
|
|
36
|
+
{ type: pinch_condition_1.PinchConditionType.MIN_OWN_SCORE, score: 20 }
|
|
37
|
+
];
|
|
38
|
+
(0, globals_1.it)('accepts a valid instance of every condition type', () => {
|
|
39
|
+
for (const condition of valid) {
|
|
40
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse(condition).success).toBe(true);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
(0, globals_1.it)('rejects an unknown condition type', () => {
|
|
44
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: 'WHENEVER' }).success).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
(0, globals_1.it)('rejects a condition missing its required field', () => {
|
|
47
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.TEAM_SET_POINT, opponent: pinch_condition_1.OpponentRelation.EITHER }).success).toBe(false);
|
|
48
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.SCORE_DIFF, direction: pinch_condition_1.ScoreDirection.LEADING }).success).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
(0, globals_1.it)('enforces numeric bounds', () => {
|
|
51
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.TEAM_SET_POINT, margin: 0, opponent: pinch_condition_1.OpponentRelation.AHEAD }).success).toBe(false);
|
|
52
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.TEAM_SET_POINT, margin: 26, opponent: pinch_condition_1.OpponentRelation.AHEAD }).success).toBe(false);
|
|
53
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.AFTER_ROTATIONS, rotations: 7 }).success).toBe(false);
|
|
54
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.FROM_SET, set: 6 }).success).toBe(false);
|
|
55
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.MIN_OWN_SCORE, score: 41 }).success).toBe(false);
|
|
56
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.SCORE_DIFF, direction: pinch_condition_1.ScoreDirection.TRAILING, points: 0 }).success).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
(0, globals_1.it)('rejects a non-integer numeric field', () => {
|
|
59
|
+
(0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.FROM_SET, set: 2.5 }).success).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
(0, globals_1.describe)('DesignatedSubSchema', () => {
|
|
63
|
+
const country = (0, test_helpers_1.makeCountry)();
|
|
64
|
+
const starter = (0, test_helpers_1.makePlayer)(country);
|
|
65
|
+
const bench = (0, test_helpers_1.makePlayer)(country);
|
|
66
|
+
(0, globals_1.it)('accepts a fatigue-mode entry with a band and bench', () => {
|
|
67
|
+
const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
|
|
68
|
+
starter,
|
|
69
|
+
bench,
|
|
70
|
+
isPinchServer: false,
|
|
71
|
+
fatigueBand: energy_band_1.EnergyBand.TIRED
|
|
72
|
+
});
|
|
73
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
(0, globals_1.it)('accepts a fatigue-mode entry with no bench (falls back to closest sub)', () => {
|
|
76
|
+
const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
|
|
77
|
+
starter,
|
|
78
|
+
isPinchServer: false
|
|
79
|
+
});
|
|
80
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
(0, globals_1.it)('accepts a pinch-mode entry with a bench server and conditions', () => {
|
|
83
|
+
const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
|
|
84
|
+
starter,
|
|
85
|
+
bench,
|
|
86
|
+
isPinchServer: true,
|
|
87
|
+
conditions: [{ type: pinch_condition_1.PinchConditionType.ASAP }],
|
|
88
|
+
conditionLogic: 'ALL'
|
|
89
|
+
});
|
|
90
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
(0, globals_1.it)('rejects a pinch-mode entry without a bench server', () => {
|
|
93
|
+
const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
|
|
94
|
+
starter,
|
|
95
|
+
isPinchServer: true,
|
|
96
|
+
conditions: [{ type: pinch_condition_1.PinchConditionType.ASAP }]
|
|
97
|
+
});
|
|
98
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
99
|
+
if (!res.success) {
|
|
100
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'PINCH_SERVER_REQUIRES_BENCH')).toBe(true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
(0, globals_1.it)('rejects a pinch-mode entry with no conditions', () => {
|
|
104
|
+
const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
|
|
105
|
+
starter,
|
|
106
|
+
bench,
|
|
107
|
+
isPinchServer: true,
|
|
108
|
+
conditions: []
|
|
109
|
+
});
|
|
110
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
111
|
+
if (!res.success) {
|
|
112
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'PINCH_SERVER_REQUIRES_CONDITIONS')).toBe(true);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
(0, globals_1.it)('rejects a starter that is not a Player instance', () => {
|
|
116
|
+
const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
|
|
117
|
+
starter: { id: 'not-a-player' },
|
|
118
|
+
isPinchServer: false
|
|
119
|
+
});
|
|
120
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -15,8 +15,8 @@ export const MatchSetInputSchema = z.object({
|
|
|
15
15
|
id: z.uuid(),
|
|
16
16
|
order: orderSchema,
|
|
17
17
|
isTieBreak: z.boolean(),
|
|
18
|
-
homePlayerPosition: z.array(PlayerPositionSchema).max(
|
|
19
|
-
awayPlayerPosition: z.array(PlayerPositionSchema).max(
|
|
18
|
+
homePlayerPosition: z.array(PlayerPositionSchema).max(14),
|
|
19
|
+
awayPlayerPosition: z.array(PlayerPositionSchema).max(14),
|
|
20
20
|
homeScore: nonNegInt,
|
|
21
21
|
awayScore: nonNegInt,
|
|
22
22
|
boxScores: z.array(z.custom((v) => v instanceof BoxScore, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BoxScore } from './box-score';
|
|
2
|
+
export declare const MIN_POINTS_PLAYED_FOR_VPER = 35;
|
|
2
3
|
export interface VPERParams {
|
|
3
4
|
readonly playerId: string;
|
|
4
5
|
readonly matchId: string;
|
|
@@ -11,4 +12,6 @@ export declare class VPER {
|
|
|
11
12
|
static create(input: unknown): VPER;
|
|
12
13
|
private constructor();
|
|
13
14
|
static computeVPERForMatch(boxScores: BoxScore[]): number;
|
|
15
|
+
static totalPointsPlayed(boxScores: BoxScore[]): number;
|
|
16
|
+
static qualifiesForVPER(boxScores: BoxScore[]): boolean;
|
|
14
17
|
}
|
|
@@ -17,6 +17,10 @@ const WEIGHTS = {
|
|
|
17
17
|
// terminal errors (always point against)
|
|
18
18
|
TERMINAL_ERROR: -1.0
|
|
19
19
|
};
|
|
20
|
+
// Minimum points a player must be on court for in a match to receive a VPER. VPER is a per-100-points rate,
|
|
21
|
+
// so below a meaningful sample one good touch is extrapolated into a wildly inflated value. Players under this
|
|
22
|
+
// threshold get no VPER for the match.
|
|
23
|
+
export const MIN_POINTS_PLAYED_FOR_VPER = 35;
|
|
20
24
|
export class VPER {
|
|
21
25
|
static create(input) {
|
|
22
26
|
const result = VPERInputSchema.safeParse(input);
|
|
@@ -62,4 +66,10 @@ export class VPER {
|
|
|
62
66
|
}
|
|
63
67
|
return 100 * rawSum / Math.max(1, pointsPlayedSum);
|
|
64
68
|
}
|
|
69
|
+
static totalPointsPlayed(boxScores) {
|
|
70
|
+
return boxScores.reduce((sum, box) => sum + box.misc.pointsPlayed, 0);
|
|
71
|
+
}
|
|
72
|
+
static qualifiesForVPER(boxScores) {
|
|
73
|
+
return VPER.totalPointsPlayed(boxScores) >= MIN_POINTS_PLAYED_FOR_VPER;
|
|
74
|
+
}
|
|
65
75
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
-
import { VPER } from './vper';
|
|
3
|
+
import { MIN_POINTS_PLAYED_FOR_VPER, VPER } from './vper';
|
|
4
4
|
import { BoxScore } from './box-score';
|
|
5
5
|
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
6
6
|
function makeBox() {
|
|
@@ -104,3 +104,37 @@ describe('VPER.computeVPERForMatch()', () => {
|
|
|
104
104
|
expect(VPER.computeVPERForMatch([box])).toBeCloseTo(14, 5);
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
|
+
// ─── VPER eligibility threshold ───────────────────────────────────────────────
|
|
108
|
+
describe('VPER.qualifiesForVPER()', () => {
|
|
109
|
+
function boxWithPoints(points) {
|
|
110
|
+
const box = makeBox();
|
|
111
|
+
box.misc.pointsPlayed = points;
|
|
112
|
+
return box;
|
|
113
|
+
}
|
|
114
|
+
it('uses a 35-point threshold', () => {
|
|
115
|
+
expect(MIN_POINTS_PLAYED_FOR_VPER).toBe(35);
|
|
116
|
+
});
|
|
117
|
+
it('sums points played across box scores', () => {
|
|
118
|
+
expect(VPER.totalPointsPlayed([boxWithPoints(20), boxWithPoints(15)])).toBe(35);
|
|
119
|
+
});
|
|
120
|
+
it('does not qualify an empty box score list', () => {
|
|
121
|
+
expect(VPER.qualifiesForVPER([])).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it('does not qualify below the threshold', () => {
|
|
124
|
+
expect(VPER.qualifiesForVPER([boxWithPoints(34)])).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
it('qualifies exactly at the threshold (inclusive)', () => {
|
|
127
|
+
expect(VPER.qualifiesForVPER([boxWithPoints(35)])).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('qualifies above the threshold across multiple sets', () => {
|
|
130
|
+
expect(VPER.qualifiesForVPER([boxWithPoints(20), boxWithPoints(20)])).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
it('rejects the inflated-cameo case (one good touch in a single point)', () => {
|
|
133
|
+
const box = makeBox();
|
|
134
|
+
box.setting.assists = 1;
|
|
135
|
+
box.setting.pass = 1;
|
|
136
|
+
box.misc.pointsPlayed = 1;
|
|
137
|
+
// Pre-fix this produced a VPER of 85; now the player is simply ineligible.
|
|
138
|
+
expect(VPER.qualifiesForVPER([box])).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { ConditionLogicSchema, DesignatedSubSchema, PinchConditionSchema, SubBandSchema } from './designated-sub.z';
|
|
3
|
+
import { EnergyBand } from '../energy-band';
|
|
4
|
+
import { OpponentRelation, PinchConditionType, ScoreDirection } from '../pinch-condition';
|
|
5
|
+
import { makeCountry, makePlayer } from '../../test-helpers';
|
|
6
|
+
describe('SubBandSchema', () => {
|
|
7
|
+
it('accepts every energy band and NEVER', () => {
|
|
8
|
+
const bands = [EnergyBand.ENERGETIC, EnergyBand.WORN, EnergyBand.TIRED, EnergyBand.EXHAUSTED, 'NEVER'];
|
|
9
|
+
for (const band of bands) {
|
|
10
|
+
expect(SubBandSchema.safeParse(band).success).toBe(true);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
it('rejects an unknown band', () => {
|
|
14
|
+
expect(SubBandSchema.safeParse('SLEEPY').success).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('ConditionLogicSchema', () => {
|
|
18
|
+
it('accepts ALL and ANY', () => {
|
|
19
|
+
expect(ConditionLogicSchema.safeParse('ALL').success).toBe(true);
|
|
20
|
+
expect(ConditionLogicSchema.safeParse('ANY').success).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('rejects anything else', () => {
|
|
23
|
+
expect(ConditionLogicSchema.safeParse('SOMETIMES').success).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('PinchConditionSchema', () => {
|
|
27
|
+
const valid = [
|
|
28
|
+
{ type: PinchConditionType.ASAP },
|
|
29
|
+
{ type: PinchConditionType.TEAM_SET_POINT, margin: 2, opponent: OpponentRelation.EITHER },
|
|
30
|
+
{ type: PinchConditionType.OPPONENT_SET_POINT, margin: 1 },
|
|
31
|
+
{ type: PinchConditionType.SCORE_DIFF, direction: ScoreDirection.TRAILING, points: 3 },
|
|
32
|
+
{ type: PinchConditionType.AFTER_ROTATIONS, rotations: 1 },
|
|
33
|
+
{ type: PinchConditionType.FROM_SET, set: 5 },
|
|
34
|
+
{ type: PinchConditionType.MIN_OWN_SCORE, score: 20 }
|
|
35
|
+
];
|
|
36
|
+
it('accepts a valid instance of every condition type', () => {
|
|
37
|
+
for (const condition of valid) {
|
|
38
|
+
expect(PinchConditionSchema.safeParse(condition).success).toBe(true);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
it('rejects an unknown condition type', () => {
|
|
42
|
+
expect(PinchConditionSchema.safeParse({ type: 'WHENEVER' }).success).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('rejects a condition missing its required field', () => {
|
|
45
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.TEAM_SET_POINT, opponent: OpponentRelation.EITHER }).success).toBe(false);
|
|
46
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.SCORE_DIFF, direction: ScoreDirection.LEADING }).success).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
it('enforces numeric bounds', () => {
|
|
49
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.TEAM_SET_POINT, margin: 0, opponent: OpponentRelation.AHEAD }).success).toBe(false);
|
|
50
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.TEAM_SET_POINT, margin: 26, opponent: OpponentRelation.AHEAD }).success).toBe(false);
|
|
51
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.AFTER_ROTATIONS, rotations: 7 }).success).toBe(false);
|
|
52
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.FROM_SET, set: 6 }).success).toBe(false);
|
|
53
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.MIN_OWN_SCORE, score: 41 }).success).toBe(false);
|
|
54
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.SCORE_DIFF, direction: ScoreDirection.TRAILING, points: 0 }).success).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it('rejects a non-integer numeric field', () => {
|
|
57
|
+
expect(PinchConditionSchema.safeParse({ type: PinchConditionType.FROM_SET, set: 2.5 }).success).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('DesignatedSubSchema', () => {
|
|
61
|
+
const country = makeCountry();
|
|
62
|
+
const starter = makePlayer(country);
|
|
63
|
+
const bench = makePlayer(country);
|
|
64
|
+
it('accepts a fatigue-mode entry with a band and bench', () => {
|
|
65
|
+
const res = DesignatedSubSchema.safeParse({
|
|
66
|
+
starter,
|
|
67
|
+
bench,
|
|
68
|
+
isPinchServer: false,
|
|
69
|
+
fatigueBand: EnergyBand.TIRED
|
|
70
|
+
});
|
|
71
|
+
expect(res.success).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('accepts a fatigue-mode entry with no bench (falls back to closest sub)', () => {
|
|
74
|
+
const res = DesignatedSubSchema.safeParse({
|
|
75
|
+
starter,
|
|
76
|
+
isPinchServer: false
|
|
77
|
+
});
|
|
78
|
+
expect(res.success).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it('accepts a pinch-mode entry with a bench server and conditions', () => {
|
|
81
|
+
const res = DesignatedSubSchema.safeParse({
|
|
82
|
+
starter,
|
|
83
|
+
bench,
|
|
84
|
+
isPinchServer: true,
|
|
85
|
+
conditions: [{ type: PinchConditionType.ASAP }],
|
|
86
|
+
conditionLogic: 'ALL'
|
|
87
|
+
});
|
|
88
|
+
expect(res.success).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('rejects a pinch-mode entry without a bench server', () => {
|
|
91
|
+
const res = DesignatedSubSchema.safeParse({
|
|
92
|
+
starter,
|
|
93
|
+
isPinchServer: true,
|
|
94
|
+
conditions: [{ type: PinchConditionType.ASAP }]
|
|
95
|
+
});
|
|
96
|
+
expect(res.success).toBe(false);
|
|
97
|
+
if (!res.success) {
|
|
98
|
+
expect(res.error.issues.some(issue => issue.message === 'PINCH_SERVER_REQUIRES_BENCH')).toBe(true);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
it('rejects a pinch-mode entry with no conditions', () => {
|
|
102
|
+
const res = DesignatedSubSchema.safeParse({
|
|
103
|
+
starter,
|
|
104
|
+
bench,
|
|
105
|
+
isPinchServer: true,
|
|
106
|
+
conditions: []
|
|
107
|
+
});
|
|
108
|
+
expect(res.success).toBe(false);
|
|
109
|
+
if (!res.success) {
|
|
110
|
+
expect(res.error.issues.some(issue => issue.message === 'PINCH_SERVER_REQUIRES_CONDITIONS')).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
it('rejects a starter that is not a Player instance', () => {
|
|
114
|
+
const res = DesignatedSubSchema.safeParse({
|
|
115
|
+
starter: { id: 'not-a-player' },
|
|
116
|
+
isPinchServer: false
|
|
117
|
+
});
|
|
118
|
+
expect(res.success).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|