volleyballsimtypes 0.0.393 → 0.0.395

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.
Files changed (43) hide show
  1. package/dist/cjs/src/api/index.d.ts +8 -1
  2. package/dist/cjs/src/data/models/tactics.d.ts +12 -2
  3. package/dist/cjs/src/data/models/tactics.js +15 -0
  4. package/dist/cjs/src/data/transformers/tactics.js +12 -0
  5. package/dist/cjs/src/service/team/base-position.d.ts +25 -0
  6. package/dist/cjs/src/service/team/base-position.js +113 -0
  7. package/dist/cjs/src/service/team/base-position.test.d.ts +1 -0
  8. package/dist/cjs/src/service/team/base-position.test.js +175 -0
  9. package/dist/cjs/src/service/team/index.d.ts +4 -0
  10. package/dist/cjs/src/service/team/index.js +4 -0
  11. package/dist/cjs/src/service/team/lineup-function.d.ts +13 -0
  12. package/dist/cjs/src/service/team/lineup-function.js +67 -0
  13. package/dist/cjs/src/service/team/offensive-preference.d.ts +5 -0
  14. package/dist/cjs/src/service/team/offensive-preference.js +2 -0
  15. package/dist/cjs/src/service/team/rotation-system.d.ts +8 -0
  16. package/dist/cjs/src/service/team/rotation-system.js +25 -0
  17. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +7 -0
  18. package/dist/cjs/src/service/team/schemas/tactics.z.js +67 -1
  19. package/dist/cjs/src/service/team/schemas/team.z.d.ts +6 -0
  20. package/dist/cjs/src/service/team/tactics.d.ts +8 -0
  21. package/dist/cjs/src/service/team/tactics.js +5 -1
  22. package/dist/esm/src/api/index.d.ts +8 -1
  23. package/dist/esm/src/data/models/tactics.d.ts +12 -2
  24. package/dist/esm/src/data/models/tactics.js +15 -0
  25. package/dist/esm/src/data/transformers/tactics.js +12 -0
  26. package/dist/esm/src/service/team/base-position.d.ts +25 -0
  27. package/dist/esm/src/service/team/base-position.js +108 -0
  28. package/dist/esm/src/service/team/base-position.test.d.ts +1 -0
  29. package/dist/esm/src/service/team/base-position.test.js +173 -0
  30. package/dist/esm/src/service/team/index.d.ts +4 -0
  31. package/dist/esm/src/service/team/index.js +4 -0
  32. package/dist/esm/src/service/team/lineup-function.d.ts +13 -0
  33. package/dist/esm/src/service/team/lineup-function.js +60 -0
  34. package/dist/esm/src/service/team/offensive-preference.d.ts +5 -0
  35. package/dist/esm/src/service/team/offensive-preference.js +1 -0
  36. package/dist/esm/src/service/team/rotation-system.d.ts +8 -0
  37. package/dist/esm/src/service/team/rotation-system.js +21 -0
  38. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +7 -0
  39. package/dist/esm/src/service/team/schemas/tactics.z.js +67 -1
  40. package/dist/esm/src/service/team/schemas/team.z.d.ts +6 -0
  41. package/dist/esm/src/service/team/tactics.d.ts +8 -0
  42. package/dist/esm/src/service/team/tactics.js +5 -1
  43. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, CourtPosition, ConditionLogic, PinchCondition, SubBand } from '../service';
1
+ import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, CourtPosition, ConditionLogic, PinchCondition, RotationSystemEnum, SubBand } from '../service';
2
2
  export type Rally = DataProps<_Rally> & {
3
3
  homePlayerPosition: PlayerPosition[];
4
4
  awayPlayerPosition: PlayerPosition[];
@@ -64,6 +64,10 @@ export interface ApiDesignatedSub {
64
64
  conditions?: PinchCondition[];
65
65
  conditionLogic?: ConditionLogic;
66
66
  }
67
+ export interface ApiOffensivePreference {
68
+ rotation: number;
69
+ order: string[];
70
+ }
67
71
  export interface Tactics {
68
72
  lineup: StartingLineup;
69
73
  substitutionTolerance: number;
@@ -72,6 +76,9 @@ export interface Tactics {
72
76
  receiveRotationOffset: boolean;
73
77
  designatedSubs: ApiDesignatedSub[];
74
78
  substitutionBand: SubBand;
79
+ rotationSystem: RotationSystemEnum;
80
+ designatedSetters: string[];
81
+ offensivePreferences: ApiOffensivePreference[];
75
82
  }
76
83
  export type Team = Omit<DataProps<_Team>, 'roster' | '_rating' | 'rating' | 'tactics'> & {
77
84
  roster: Player[];
@@ -1,7 +1,7 @@
1
1
  import * as Sequelize from 'sequelize';
2
2
  import { Model, Optional } from 'sequelize';
3
3
  import { PlayerId, TeamId, TeamModel } from '.';
4
- import { ConditionLogic, CourtPosition, PinchCondition, SubBand } from '../../service';
4
+ import { ConditionLogic, CourtPosition, PinchCondition, RotationSystemEnum, SubBand } from '../../service';
5
5
  export interface TacticsLineupAttributes {
6
6
  [CourtPosition.LIBERO_ZONE]?: PlayerId;
7
7
  [CourtPosition.LEFT_BACK]: PlayerId;
@@ -21,6 +21,10 @@ export interface DesignatedSubAttributes {
21
21
  conditions?: PinchCondition[];
22
22
  conditionLogic?: ConditionLogic;
23
23
  }
24
+ export interface OffensivePreferenceAttributes {
25
+ rotation: number;
26
+ order: PlayerId[];
27
+ }
24
28
  export interface TacticsAttributes {
25
29
  team_id: string;
26
30
  substitution_tolerance: number;
@@ -30,10 +34,13 @@ export interface TacticsAttributes {
30
34
  receive_rotation_offset: boolean;
31
35
  designated_subs: DesignatedSubAttributes[];
32
36
  substitution_band: SubBand;
37
+ rotation_system: RotationSystemEnum;
38
+ designated_setters: PlayerId[];
39
+ offensive_preferences: OffensivePreferenceAttributes[];
33
40
  }
34
41
  export type TacticsPk = 'team_id';
35
42
  export type TacticsId = TacticsModel[TacticsPk];
36
- export type TacticsOptionalAttributes = 'substitution_tolerance' | 'lineup' | 'libero_replacements' | 'pinch_server_subs' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band';
43
+ export type TacticsOptionalAttributes = 'substitution_tolerance' | 'lineup' | 'libero_replacements' | 'pinch_server_subs' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'rotation_system' | 'designated_setters' | 'offensive_preferences';
37
44
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
38
45
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
39
46
  team_id: string;
@@ -44,6 +51,9 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
44
51
  receive_rotation_offset: boolean;
45
52
  designated_subs: DesignatedSubAttributes[];
46
53
  substitution_band: SubBand;
54
+ rotation_system: RotationSystemEnum;
55
+ designated_setters: PlayerId[];
56
+ offensive_preferences: OffensivePreferenceAttributes[];
47
57
  team: TeamModel;
48
58
  getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
49
59
  setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
@@ -49,6 +49,21 @@ class TacticsModel extends sequelize_1.Model {
49
49
  type: sequelize_1.DataTypes.STRING,
50
50
  allowNull: false,
51
51
  defaultValue: 'TIRED'
52
+ },
53
+ rotation_system: {
54
+ type: sequelize_1.DataTypes.STRING,
55
+ allowNull: false,
56
+ defaultValue: '6-0'
57
+ },
58
+ designated_setters: {
59
+ type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.UUID),
60
+ allowNull: false,
61
+ defaultValue: []
62
+ },
63
+ offensive_preferences: {
64
+ type: sequelize_1.DataTypes.JSONB,
65
+ allowNull: false,
66
+ defaultValue: []
52
67
  }
53
68
  }, {
54
69
  sequelize,
@@ -49,6 +49,12 @@ function transformToAttributes(tactics, teamId) {
49
49
  fatigueBand: ds.fatigueBand,
50
50
  conditions: ds.conditions,
51
51
  conditionLogic: ds.conditionLogic
52
+ })),
53
+ rotation_system: tactics.rotationSystem,
54
+ designated_setters: tactics.designatedSetters.map((s) => s.id),
55
+ offensive_preferences: tactics.offensivePreferences.map(p => ({
56
+ rotation: p.rotation,
57
+ order: p.order.map((a) => a.id)
52
58
  }))
53
59
  };
54
60
  }
@@ -74,6 +80,12 @@ function transformToObject(model, roster) {
74
80
  fatigueBand: d.fatigueBand,
75
81
  conditions: d.conditions,
76
82
  conditionLogic: d.conditionLogic
83
+ })),
84
+ rotationSystem: model.rotation_system,
85
+ designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
86
+ offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
87
+ rotation: p.rotation,
88
+ order: p.order.map((id) => findPlayer(id, roster))
77
89
  }))
78
90
  });
79
91
  }
@@ -0,0 +1,25 @@
1
+ import { CourtPosition } from '../match';
2
+ import { Role } from '../player';
3
+ import { RotationSystemEnum } from './rotation-system';
4
+ export declare enum BaseContext {
5
+ SERVE_RECEIVE = "SERVE_RECEIVE",
6
+ BASE = "BASE"
7
+ }
8
+ export interface BasePlayer {
9
+ readonly playerId: string;
10
+ readonly position: CourtPosition;
11
+ readonly roles: Role[];
12
+ }
13
+ export interface BaseResolverInput {
14
+ readonly system: RotationSystemEnum;
15
+ readonly designatedSetterIds: string[];
16
+ readonly players: BasePlayer[];
17
+ readonly liberoId?: string;
18
+ }
19
+ export type BaseMap = Map<string, CourtPosition>;
20
+ export interface ResolvedBases {
21
+ readonly serveReceive: BaseMap;
22
+ readonly base: BaseMap;
23
+ }
24
+ export declare function rallySetterId(system: RotationSystemEnum, designatedSetterIds: string[], players: BasePlayer[]): string | undefined;
25
+ export declare function resolveBases(input: BaseResolverInput): ResolvedBases;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseContext = void 0;
4
+ exports.rallySetterId = rallySetterId;
5
+ exports.resolveBases = resolveBases;
6
+ const match_1 = require("../match");
7
+ const player_1 = require("../player");
8
+ const rotation_system_1 = require("./rotation-system");
9
+ const lineup_function_1 = require("./lineup-function");
10
+ // The rally phase a base lookup is resolved against. Two contexts are enough in the 6-zone model:
11
+ // - SERVE_RECEIVE: first contact when receiving serve. The passers (libero + outsides) hold the back row; the
12
+ // setter(s) and the opposite are hidden in the front (not passers). Serves are aimed at the back-row passers,
13
+ // so the setter / penetrating setter / opposite never receive by default (they can be deliberately hunted).
14
+ // - BASE: open play (offense AND defense). In the discrete 6-zone model these coincide: every player is at their
15
+ // one specialist lane whether attacking or digging. There is no visual layer and no benefit to splitting them.
16
+ var BaseContext;
17
+ (function (BaseContext) {
18
+ BaseContext["SERVE_RECEIVE"] = "SERVE_RECEIVE";
19
+ BaseContext["BASE"] = "BASE";
20
+ })(BaseContext || (exports.BaseContext = BaseContext = {}));
21
+ // The specialist zone a function occupies given its current row. Outsides play left (z4 front / z5 back), middles
22
+ // the middle (z3 / z6), setter and opposite the right (z2 / z1). Because each same-zone pair (setter+opposite, the
23
+ // two middles, the two outsides) sits 3 rotations apart, exactly one is front and one is back at any rotation, so
24
+ // the six players always map to the six distinct zones (a bijection).
25
+ function specialistZone(fn, front) {
26
+ switch (fn) {
27
+ case player_1.RoleEnum.OUTSIDE_HITTER: return front ? match_1.CourtPosition.LEFT_FRONT : match_1.CourtPosition.LEFT_BACK;
28
+ case player_1.RoleEnum.MIDDLE_BLOCKER: return front ? match_1.CourtPosition.MIDDLE_FRONT : match_1.CourtPosition.MIDDLE_BACK;
29
+ case player_1.RoleEnum.OPPOSITE_HITTER:
30
+ case player_1.RoleEnum.SETTER: return front ? match_1.CourtPosition.RIGHT_FRONT : match_1.CourtPosition.RIGHT_BACK;
31
+ default: return front ? match_1.CourtPosition.MIDDLE_FRONT : match_1.CourtPosition.MIDDLE_BACK;
32
+ }
33
+ }
34
+ // Who takes the second touch this rally: 5-1 the single setter; 4-2 the front-row setter; 6-2 the back-row
35
+ // (penetrating) setter. Returns undefined for 6-0 or when no designated setter is on court (the sim picks a
36
+ // front-row setter by stat in that case).
37
+ function rallySetterId(system, designatedSetterIds, players) {
38
+ const setters = designatedSetterIds
39
+ .map(id => players.find(p => p.playerId === id))
40
+ .filter((p) => p != null);
41
+ if (setters.length === 0)
42
+ return undefined;
43
+ switch (system) {
44
+ case rotation_system_1.RotationSystemEnum.FIVE_ONE:
45
+ return setters[0].playerId;
46
+ case rotation_system_1.RotationSystemEnum.FOUR_TWO:
47
+ return (setters.find(s => (0, match_1.isFrontRow)(s.position)) ?? setters[0]).playerId;
48
+ case rotation_system_1.RotationSystemEnum.SIX_TWO:
49
+ return (setters.find(s => !(0, match_1.isFrontRow)(s.position)) ?? setters[0]).playerId;
50
+ default:
51
+ return undefined;
52
+ }
53
+ }
54
+ function identityBases(players) {
55
+ const make = () => new Map(players.map(p => [p.playerId, p.position]));
56
+ return { serveReceive: make(), base: make() };
57
+ }
58
+ // Resolve the two context base maps for the six on-court players. 6-0 (or a misconfigured lineup) keeps the
59
+ // rotational layout in both contexts (= pre-feature behavior). Each returned map is a bijection over {1..6}; the
60
+ // sim asserts this after applying it.
61
+ function resolveBases(input) {
62
+ const { system, designatedSetterIds, players, liberoId } = input;
63
+ if (system === rotation_system_1.RotationSystemEnum.SIX_ZERO)
64
+ return identityBases(players);
65
+ const mainSetter = players.find(p => p.playerId === designatedSetterIds[0]);
66
+ if (mainSetter == null)
67
+ return identityBases(players);
68
+ // BASE (open play): every player releases to their specialist lane.
69
+ const base = new Map();
70
+ for (const p of players) {
71
+ const fn = (0, lineup_function_1.slotFunctionAt)(system, mainSetter.position, p.position);
72
+ base.set(p.playerId, specialistZone(fn, (0, match_1.isFrontRow)(p.position)));
73
+ }
74
+ return { serveReceive: buildServeReceive(system, designatedSetterIds, players, mainSetter, liberoId), base };
75
+ }
76
+ // The serve-receive base. Only the back row handles the serve, so the three back-row zones go to the actual
77
+ // passers (the libero + the two outside-hitter slots) and the three front zones hide the non-passers (the
78
+ // setter(s), the opposite, and a middle). This keeps the opposite -- which shares the right lane with the setter
79
+ // in open play -- out of the passing lanes; a server can still deliberately hunt the hidden setter or opposite
80
+ // (handled sim-side). Without a libero on court the back-row middle fills the third passing slot. Always a
81
+ // bijection over {1..6} (three front + three back, distinct zones).
82
+ function buildServeReceive(system, designatedSetterIds, players, mainSetter, liberoId) {
83
+ // Zone order, declared in-function so CourtPosition is read at call time, not module load: the service<->match
84
+ // import cycle leaves CourtPosition unbound while this module is first evaluated, so a module-level array would
85
+ // crash. Hidden players fill the front (rally-setter first -> RIGHT_FRONT); passers fill the back (libero first
86
+ // -> MIDDLE_BACK).
87
+ const frontZones = [match_1.CourtPosition.RIGHT_FRONT, match_1.CourtPosition.LEFT_FRONT, match_1.CourtPosition.MIDDLE_FRONT];
88
+ const backZones = [match_1.CourtPosition.MIDDLE_BACK, match_1.CourtPosition.LEFT_BACK, match_1.CourtPosition.RIGHT_BACK];
89
+ const setterId = rallySetterId(system, designatedSetterIds, players);
90
+ const fnOf = (p) => (0, lineup_function_1.slotFunctionAt)(system, mainSetter.position, p.position);
91
+ const passers = [];
92
+ const middles = [];
93
+ const hidden = []; // setter(s) + opposite
94
+ for (const p of players) {
95
+ if (p.playerId === liberoId || fnOf(p) === player_1.RoleEnum.OUTSIDE_HITTER)
96
+ passers.push(p);
97
+ else if (fnOf(p) === player_1.RoleEnum.MIDDLE_BLOCKER)
98
+ middles.push(p);
99
+ else
100
+ hidden.push(p);
101
+ }
102
+ // Fill to three passers from the middles (back-row first, so a front-row middle stays hidden); the rest hide.
103
+ middles.sort((a, b) => Number((0, match_1.isFrontRow)(a.position)) - Number((0, match_1.isFrontRow)(b.position)));
104
+ while (passers.length < 3 && middles.length > 0)
105
+ passers.push(middles.shift());
106
+ hidden.push(...middles);
107
+ hidden.sort((a, b) => Number(b.playerId === setterId) - Number(a.playerId === setterId));
108
+ passers.sort((a, b) => Number(b.playerId === liberoId) - Number(a.playerId === liberoId));
109
+ const sr = new Map();
110
+ hidden.forEach((p, i) => sr.set(p.playerId, frontZones[i]));
111
+ passers.forEach((p, i) => sr.set(p.playerId, backZones[i]));
112
+ return sr;
113
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const match_1 = require("../match");
4
+ const player_1 = require("../player");
5
+ const rotation_system_1 = require("./rotation-system");
6
+ const lineup_function_1 = require("./lineup-function");
7
+ const base_position_1 = require("./base-position");
8
+ // Balanced 5-1 at rotation 1 (setter serving from RIGHT_BACK = 1): S, OH1, MB1, OPP, OH2, MB2 around the court.
9
+ function baseLineup() {
10
+ return [
11
+ { playerId: 'S', position: match_1.CourtPosition.RIGHT_BACK, roles: [player_1.RoleEnum.SETTER] },
12
+ { playerId: 'OH1', position: match_1.CourtPosition.RIGHT_FRONT, roles: [player_1.RoleEnum.OUTSIDE_HITTER] },
13
+ { playerId: 'MB1', position: match_1.CourtPosition.MIDDLE_FRONT, roles: [player_1.RoleEnum.MIDDLE_BLOCKER] },
14
+ { playerId: 'OPP', position: match_1.CourtPosition.LEFT_FRONT, roles: [player_1.RoleEnum.OPPOSITE_HITTER] },
15
+ { playerId: 'OH2', position: match_1.CourtPosition.LEFT_BACK, roles: [player_1.RoleEnum.OUTSIDE_HITTER] },
16
+ { playerId: 'MB2', position: match_1.CourtPosition.MIDDLE_BACK, roles: [player_1.RoleEnum.MIDDLE_BLOCKER] }
17
+ ];
18
+ }
19
+ // 5-1 with the libero on court in the back-row middle slot (replacing MB2).
20
+ function lineupWithLibero() {
21
+ return {
22
+ players: [
23
+ { playerId: 'S', position: match_1.CourtPosition.RIGHT_BACK, roles: [player_1.RoleEnum.SETTER] },
24
+ { playerId: 'OH1', position: match_1.CourtPosition.RIGHT_FRONT, roles: [player_1.RoleEnum.OUTSIDE_HITTER] },
25
+ { playerId: 'MB1', position: match_1.CourtPosition.MIDDLE_FRONT, roles: [player_1.RoleEnum.MIDDLE_BLOCKER] },
26
+ { playerId: 'OPP', position: match_1.CourtPosition.LEFT_FRONT, roles: [player_1.RoleEnum.OPPOSITE_HITTER] },
27
+ { playerId: 'OH2', position: match_1.CourtPosition.LEFT_BACK, roles: [player_1.RoleEnum.OUTSIDE_HITTER] },
28
+ { playerId: 'LIB', position: match_1.CourtPosition.MIDDLE_BACK, roles: [player_1.RoleEnum.LIBERO] }
29
+ ],
30
+ liberoId: 'LIB'
31
+ };
32
+ }
33
+ function rotate(players, times) {
34
+ let out = players;
35
+ for (let i = 0; i < times; i++)
36
+ out = out.map(p => ({ ...p, position: (0, match_1.rotatePosition)(p.position) }));
37
+ return out;
38
+ }
39
+ function isBijection(m) {
40
+ const zones = [...m.values()];
41
+ return zones.length === 6 && new Set(zones).size === 6 && zones.every(z => z >= 1 && z <= 6);
42
+ }
43
+ const SETTERS = {
44
+ [rotation_system_1.RotationSystemEnum.FIVE_ONE]: ['S'],
45
+ [rotation_system_1.RotationSystemEnum.FOUR_TWO]: ['S', 'OPP'],
46
+ [rotation_system_1.RotationSystemEnum.SIX_TWO]: ['S', 'OPP'],
47
+ [rotation_system_1.RotationSystemEnum.SIX_ZERO]: []
48
+ };
49
+ describe('resolveBases() bijection', () => {
50
+ for (const system of Object.values(rotation_system_1.RotationSystemEnum)) {
51
+ for (let r = 0; r < 6; r++) {
52
+ it(`${system} rotation ${r}: all three contexts are bijections over {1..6}`, () => {
53
+ const players = rotate(baseLineup(), r);
54
+ const bases = (0, base_position_1.resolveBases)({ system, designatedSetterIds: SETTERS[system], players });
55
+ expect(isBijection(bases.serveReceive)).toBe(true);
56
+ expect(isBijection(bases.base)).toBe(true);
57
+ });
58
+ }
59
+ }
60
+ });
61
+ describe('resolveBases() 6-0', () => {
62
+ it('keeps the rotational layout in every context (identity)', () => {
63
+ const players = rotate(baseLineup(), 3);
64
+ const bases = (0, base_position_1.resolveBases)({ system: rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetterIds: [], players });
65
+ for (const p of players) {
66
+ expect(bases.base.get(p.playerId)).toBe(p.position);
67
+ expect(bases.serveReceive.get(p.playerId)).toBe(p.position);
68
+ }
69
+ });
70
+ });
71
+ describe('resolveBases() serve-receive hides the setter', () => {
72
+ it('5-1 setter is always at RIGHT_FRONT in serve-receive, every rotation', () => {
73
+ for (let r = 0; r < 6; r++) {
74
+ const players = rotate(baseLineup(), r);
75
+ const bases = (0, base_position_1.resolveBases)({ system: rotation_system_1.RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players });
76
+ expect(bases.serveReceive.get('S')).toBe(match_1.CourtPosition.RIGHT_FRONT);
77
+ }
78
+ });
79
+ });
80
+ describe('resolveBases() serve-receive hides the opposite (regression: opposites were receiving most serves)', () => {
81
+ it('5-1 opposite is hidden in the front, never in the serve-receive back row, every rotation', () => {
82
+ for (let r = 0; r < 6; r++) {
83
+ const players = rotate(baseLineup(), r);
84
+ const bases = (0, base_position_1.resolveBases)({ system: rotation_system_1.RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players });
85
+ expect((0, match_1.isFrontRow)(bases.serveReceive.get('OPP'))).toBe(true);
86
+ }
87
+ });
88
+ it('5-1 serve-receive back row holds exactly the passers -- never the setter or the opposite', () => {
89
+ for (let r = 0; r < 6; r++) {
90
+ const players = rotate(baseLineup(), r);
91
+ const bases = (0, base_position_1.resolveBases)({ system: rotation_system_1.RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players });
92
+ const backRow = [...bases.serveReceive.entries()].filter(([, z]) => !(0, match_1.isFrontRow)(z)).map(([id]) => id);
93
+ expect(backRow).toHaveLength(3);
94
+ expect(backRow).not.toContain('S');
95
+ expect(backRow).not.toContain('OPP');
96
+ }
97
+ });
98
+ it('the on-court libero is always a passer (back row) while the opposite stays hidden', () => {
99
+ const { players, liberoId } = lineupWithLibero();
100
+ const bases = (0, base_position_1.resolveBases)({ system: rotation_system_1.RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players, liberoId });
101
+ expect((0, match_1.isFrontRow)(bases.serveReceive.get(liberoId))).toBe(false);
102
+ expect((0, match_1.isFrontRow)(bases.serveReceive.get('OPP'))).toBe(true);
103
+ });
104
+ });
105
+ describe('rallySetterId()', () => {
106
+ it('5-1 returns the single setter in every rotation', () => {
107
+ for (let r = 0; r < 6; r++) {
108
+ const players = rotate(baseLineup(), r);
109
+ expect((0, base_position_1.rallySetterId)(rotation_system_1.RotationSystemEnum.FIVE_ONE, ['S'], players)).toBe('S');
110
+ }
111
+ });
112
+ it('4-2 returns the front-row setter, 6-2 returns the back-row setter', () => {
113
+ for (let r = 0; r < 6; r++) {
114
+ const players = rotate(baseLineup(), r);
115
+ const front = players.find(p => (p.playerId === 'S' || p.playerId === 'OPP') && p.position >= 2 && p.position <= 4);
116
+ const back = players.find(p => (p.playerId === 'S' || p.playerId === 'OPP') && (p.position === 1 || p.position === 5 || p.position === 6));
117
+ expect((0, base_position_1.rallySetterId)(rotation_system_1.RotationSystemEnum.FOUR_TWO, ['S', 'OPP'], players)).toBe(front?.playerId);
118
+ expect((0, base_position_1.rallySetterId)(rotation_system_1.RotationSystemEnum.SIX_TWO, ['S', 'OPP'], players)).toBe(back?.playerId);
119
+ }
120
+ });
121
+ it('6-0 has no rally setter', () => {
122
+ expect((0, base_position_1.rallySetterId)(rotation_system_1.RotationSystemEnum.SIX_ZERO, [], baseLineup())).toBeUndefined();
123
+ });
124
+ });
125
+ describe('getLineupFunctions()', () => {
126
+ it('assigns the balanced 5-1 functions', () => {
127
+ const fns = (0, lineup_function_1.getLineupFunctions)(rotation_system_1.RotationSystemEnum.FIVE_ONE, ['S'], baseLineup());
128
+ expect(fns.get('S')).toBe(player_1.RoleEnum.SETTER);
129
+ expect(fns.get('OH1')).toBe(player_1.RoleEnum.OUTSIDE_HITTER);
130
+ expect(fns.get('MB1')).toBe(player_1.RoleEnum.MIDDLE_BLOCKER);
131
+ expect(fns.get('OPP')).toBe(player_1.RoleEnum.OPPOSITE_HITTER);
132
+ expect(fns.get('OH2')).toBe(player_1.RoleEnum.OUTSIDE_HITTER);
133
+ expect(fns.get('MB2')).toBe(player_1.RoleEnum.MIDDLE_BLOCKER);
134
+ });
135
+ it('maps the opposite slot to SETTER in 6-2/4-2 (the second setter)', () => {
136
+ const fns = (0, lineup_function_1.getLineupFunctions)(rotation_system_1.RotationSystemEnum.SIX_TWO, ['S', 'OPP'], baseLineup());
137
+ expect(fns.get('OPP')).toBe(player_1.RoleEnum.SETTER);
138
+ expect(fns.get('S')).toBe(player_1.RoleEnum.SETTER);
139
+ });
140
+ it('is rotation-invariant', () => {
141
+ const r0 = (0, lineup_function_1.getLineupFunctions)(rotation_system_1.RotationSystemEnum.FIVE_ONE, ['S'], baseLineup());
142
+ const r3 = (0, lineup_function_1.getLineupFunctions)(rotation_system_1.RotationSystemEnum.FIVE_ONE, ['S'], rotate(baseLineup(), 3));
143
+ for (const id of ['S', 'OH1', 'MB1', 'OPP', 'OH2', 'MB2']) {
144
+ expect(r3.get(id)).toBe(r0.get(id));
145
+ }
146
+ });
147
+ it('6-0 assigns no functions', () => {
148
+ expect((0, lineup_function_1.getLineupFunctions)(rotation_system_1.RotationSystemEnum.SIX_ZERO, [], baseLineup()).size).toBe(0);
149
+ });
150
+ });
151
+ describe('out-of-position penalty', () => {
152
+ it('penalises a specialist played in a slot whose role they lack', () => {
153
+ // A pure middle blocker placed in the opposite slot (function OPPOSITE_HITTER) is out of position.
154
+ expect((0, lineup_function_1.isOutOfPosition)([player_1.RoleEnum.MIDDLE_BLOCKER], player_1.RoleEnum.OPPOSITE_HITTER)).toBe(true);
155
+ expect((0, lineup_function_1.outOfPositionPenalty)([player_1.RoleEnum.MIDDLE_BLOCKER], player_1.RoleEnum.OPPOSITE_HITTER)).toBeCloseTo(lineup_function_1.OUT_OF_POSITION_PENALTY);
156
+ });
157
+ it('does not penalise a versatile player who holds the slot role', () => {
158
+ expect((0, lineup_function_1.isOutOfPosition)([player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.OPPOSITE_HITTER], player_1.RoleEnum.OPPOSITE_HITTER)).toBe(false);
159
+ expect((0, lineup_function_1.outOfPositionPenalty)([player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.OPPOSITE_HITTER], player_1.RoleEnum.OPPOSITE_HITTER)).toBe(1);
160
+ });
161
+ it('never penalises when there is no slot function (6-0 / unknown)', () => {
162
+ expect((0, lineup_function_1.isOutOfPosition)([player_1.RoleEnum.MIDDLE_BLOCKER], undefined)).toBe(false);
163
+ expect((0, lineup_function_1.outOfPositionPenalty)([player_1.RoleEnum.MIDDLE_BLOCKER], undefined)).toBe(1);
164
+ });
165
+ });
166
+ describe('slotFunctionAt()', () => {
167
+ it('is rotation-invariant for each player', () => {
168
+ const setterAtR0 = match_1.CourtPosition.RIGHT_BACK;
169
+ const setterAtR3 = (0, match_1.rotatePosition)((0, match_1.rotatePosition)((0, match_1.rotatePosition)(setterAtR0)));
170
+ // OPP sits opposite the setter (offset 3): OPPOSITE in 5-1.
171
+ expect((0, lineup_function_1.slotFunctionAt)(rotation_system_1.RotationSystemEnum.FIVE_ONE, setterAtR0, match_1.CourtPosition.LEFT_FRONT)).toBe(player_1.RoleEnum.OPPOSITE_HITTER);
172
+ const oppAtR3 = (0, match_1.rotatePosition)((0, match_1.rotatePosition)((0, match_1.rotatePosition)(match_1.CourtPosition.LEFT_FRONT)));
173
+ expect((0, lineup_function_1.slotFunctionAt)(rotation_system_1.RotationSystemEnum.FIVE_ONE, setterAtR3, oppAtR3)).toBe(player_1.RoleEnum.OPPOSITE_HITTER);
174
+ });
175
+ });
@@ -6,3 +6,7 @@ export * from './team-name';
6
6
  export * from './energy-band';
7
7
  export * from './pinch-condition';
8
8
  export * from './designated-sub';
9
+ export * from './rotation-system';
10
+ export * from './lineup-function';
11
+ export * from './base-position';
12
+ export * from './offensive-preference';
@@ -22,3 +22,7 @@ __exportStar(require("./team-name"), exports);
22
22
  __exportStar(require("./energy-band"), exports);
23
23
  __exportStar(require("./pinch-condition"), exports);
24
24
  __exportStar(require("./designated-sub"), exports);
25
+ __exportStar(require("./rotation-system"), exports);
26
+ __exportStar(require("./lineup-function"), exports);
27
+ __exportStar(require("./base-position"), exports);
28
+ __exportStar(require("./offensive-preference"), exports);
@@ -0,0 +1,13 @@
1
+ import { CourtPosition } from '../match';
2
+ import { Role, RoleEnum } from '../player';
3
+ import { RotationSystemEnum } from './rotation-system';
4
+ export type LineupFunction = RoleEnum;
5
+ export declare const OUT_OF_POSITION_PENALTY: 0.85;
6
+ export interface LineupSlot {
7
+ readonly playerId: string;
8
+ readonly position: CourtPosition;
9
+ }
10
+ export declare function slotFunctionAt(system: RotationSystemEnum, setterPosition: CourtPosition, position: CourtPosition): RoleEnum;
11
+ export declare function getLineupFunctions(system: RotationSystemEnum, designatedSetterIds: string[], starters: LineupSlot[]): Map<string, RoleEnum>;
12
+ export declare function isOutOfPosition(roles: Role[], fn: RoleEnum | undefined): boolean;
13
+ export declare function outOfPositionPenalty(roles: Role[], fn: RoleEnum | undefined): number;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OUT_OF_POSITION_PENALTY = void 0;
4
+ exports.slotFunctionAt = slotFunctionAt;
5
+ exports.getLineupFunctions = getLineupFunctions;
6
+ exports.isOutOfPosition = isOutOfPosition;
7
+ exports.outOfPositionPenalty = outOfPositionPenalty;
8
+ const player_1 = require("../player");
9
+ const rotation_system_1 = require("./rotation-system");
10
+ // Flat multiplier applied to EVERY action a player performs while played out of their preferred position.
11
+ // Roles are a player's preference/specialisation (decoupled from raw stats), so this is the single standalone
12
+ // signal for "played out of position" and is not double-counting any stat disadvantage. Single tunable knob.
13
+ exports.OUT_OF_POSITION_PENALTY = 0.85;
14
+ // The balanced-lineup function pattern by rotational offset from the main setter (in serving order):
15
+ // 0 setter, 1 outside, 2 middle, 3 opposite (the second setter in 4-2/6-2), 4 outside, 5 middle.
16
+ // Offset is rotation-invariant (the whole team rotates together), so a player's slot function is constant for the
17
+ // match until a substitution changes the personnel.
18
+ const PATTERN = [
19
+ player_1.RoleEnum.SETTER,
20
+ player_1.RoleEnum.OUTSIDE_HITTER,
21
+ player_1.RoleEnum.MIDDLE_BLOCKER,
22
+ player_1.RoleEnum.OPPOSITE_HITTER,
23
+ player_1.RoleEnum.OUTSIDE_HITTER,
24
+ player_1.RoleEnum.MIDDLE_BLOCKER
25
+ ];
26
+ // The slot function for a player at `position`, given the main setter sits at `setterPosition`, in `system`.
27
+ // In 4-2/6-2 the opposite slot (offset 3) is the second setter, so it maps to SETTER rather than OPPOSITE.
28
+ function slotFunctionAt(system, setterPosition, position) {
29
+ const offset = (((position - setterPosition) % 6) + 6) % 6;
30
+ const fn = PATTERN[offset];
31
+ if (fn === player_1.RoleEnum.OPPOSITE_HITTER &&
32
+ (system === rotation_system_1.RotationSystemEnum.FOUR_TWO || system === rotation_system_1.RotationSystemEnum.SIX_TWO)) {
33
+ return player_1.RoleEnum.SETTER;
34
+ }
35
+ return fn;
36
+ }
37
+ // Map each starter's id -> the role their lineup slot requires. Empty for 6-0 (no functions => no penalty), or if
38
+ // the main setter is not among the starters (misconfigured -> degrade gracefully to no penalty).
39
+ // Designated setters are forced to SETTER so a slightly off lineup never penalises a correctly-rostered setter
40
+ // (validation enforces the opposite-slot rule upstream).
41
+ function getLineupFunctions(system, designatedSetterIds, starters) {
42
+ const functions = new Map();
43
+ if (system === rotation_system_1.RotationSystemEnum.SIX_ZERO)
44
+ return functions;
45
+ const mainSetter = starters.find(s => s.playerId === designatedSetterIds[0]);
46
+ if (mainSetter == null)
47
+ return functions;
48
+ for (const slot of starters) {
49
+ functions.set(slot.playerId, slotFunctionAt(system, mainSetter.position, slot.position));
50
+ }
51
+ for (const id of designatedSetterIds) {
52
+ if (functions.has(id))
53
+ functions.set(id, player_1.RoleEnum.SETTER);
54
+ }
55
+ return functions;
56
+ }
57
+ // True when a player's slot requires a role they do not hold (i.e. they are played out of position). A missing
58
+ // function (6-0, or unknown player) is never out of position.
59
+ function isOutOfPosition(roles, fn) {
60
+ if (fn == null)
61
+ return false;
62
+ return !roles.includes(fn);
63
+ }
64
+ // The action multiplier for a player given their slot function: penalised if out of position, else neutral.
65
+ function outOfPositionPenalty(roles, fn) {
66
+ return isOutOfPosition(roles, fn) ? exports.OUT_OF_POSITION_PENALTY : 1;
67
+ }
@@ -0,0 +1,5 @@
1
+ import { Player } from '../player';
2
+ export interface OffensivePreference {
3
+ readonly rotation: number;
4
+ readonly order: Player[];
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ export declare enum RotationSystemEnum {
2
+ FIVE_ONE = "5-1",
3
+ FOUR_TWO = "4-2",
4
+ SIX_TWO = "6-2",
5
+ SIX_ZERO = "6-0"
6
+ }
7
+ export type RotationSystem = RotationSystemEnum;
8
+ export declare function setterCountFor(system: RotationSystemEnum): number;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RotationSystemEnum = void 0;
4
+ exports.setterCountFor = setterCountFor;
5
+ // The offensive rotation system a team runs. Drives base-positioning, who sets each rally (the rally-setter),
6
+ // and the out-of-position penalty. '6-0' (treat '6-6' as an alias) is the neutral default: no designated setter,
7
+ // base positions = the rotational layout, so an unconfigured team plays exactly as before this feature.
8
+ var RotationSystemEnum;
9
+ (function (RotationSystemEnum) {
10
+ RotationSystemEnum["FIVE_ONE"] = "5-1";
11
+ RotationSystemEnum["FOUR_TWO"] = "4-2";
12
+ RotationSystemEnum["SIX_TWO"] = "6-2";
13
+ RotationSystemEnum["SIX_ZERO"] = "6-0";
14
+ })(RotationSystemEnum || (exports.RotationSystemEnum = RotationSystemEnum = {}));
15
+ // How many designated setters each system expects: 5-1 has one; 4-2 and 6-2 have two (main + the opposite slot,
16
+ // which acts as the second setter); 6-0 has none.
17
+ function setterCountFor(system) {
18
+ switch (system) {
19
+ case RotationSystemEnum.FIVE_ONE: return 1;
20
+ case RotationSystemEnum.FOUR_TWO: return 2;
21
+ case RotationSystemEnum.SIX_TWO: return 2;
22
+ case RotationSystemEnum.SIX_ZERO: return 0;
23
+ default: return 0;
24
+ }
25
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { Player } from '../../player';
3
3
  import { EnergyBand } from '../energy-band';
4
+ import { RotationSystemEnum } from '../rotation-system';
4
5
  export declare const TacticsInputSchema: z.ZodObject<{
5
6
  lineup: z.ZodObject<{
6
7
  4: z.ZodCustom<Player, Player>;
@@ -47,5 +48,11 @@ export declare const TacticsInputSchema: z.ZodObject<{
47
48
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
48
49
  }, z.core.$strip>>>;
49
50
  substitutionBand: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<typeof EnergyBand>, z.ZodLiteral<"NEVER">]>>;
51
+ rotationSystem: z.ZodDefault<z.ZodEnum<typeof RotationSystemEnum>>;
52
+ designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
53
+ offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
54
+ rotation: z.ZodNumber;
55
+ order: z.ZodArray<z.ZodCustom<Player, Player>>;
56
+ }, z.core.$strip>>>;
50
57
  }, z.core.$strip>;
51
58
  export type TacticsInput = z.infer<typeof TacticsInputSchema>;