@cs2dak/contract 0.2.1 → 1.1.0
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/LICENSE +7 -0
- package/package.json +3 -3
- package/src/analysis.ts +228 -0
- package/src/bracket.ts +68 -0
- package/src/cohort.ts +60 -0
- package/src/contract.test.ts +624 -0
- package/src/demo-package.ts +38 -0
- package/src/dependency-boundaries.test.ts +27 -0
- package/src/duel.ts +105 -0
- package/src/event-package.test.ts +48 -0
- package/src/event-package.ts +163 -0
- package/src/evidence.ts +22 -0
- package/src/index.ts +20 -599
- package/src/leaderboard.ts +102 -0
- package/src/map-intelligence.ts +202 -0
- package/src/map-role.ts +317 -0
- package/src/player.ts +107 -0
- package/src/qa.ts +21 -0
- package/src/radar-field.ts +79 -0
- package/src/scoring.ts +87 -0
- package/src/series.ts +77 -0
- package/src/team.ts +72 -0
- package/src/trails.ts +56 -0
- package/src/upstream.ts +110 -0
- package/src/veto.ts +44 -0
- package/src/workspace.ts +341 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { PrismResult } from "@rivalhub/rival-rating";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { teamKeySchema } from "cs2-demo-format";
|
|
4
|
+
import { seasonCohortBundleSchema } from "./cohort.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 赛季排行榜展示模型。由 @cs2dak/presentation 从 SeasonCohortBundle 派生,
|
|
8
|
+
* 吸收 RivalHub 排行榜的产品信息架构(分栏、标签、默认排序),但:
|
|
9
|
+
* - 不含数据库/权限/路由语义(规则 8);身份用中立的 playerKey/teamKeys。
|
|
10
|
+
* - 缺失指标保持 null,不伪造 0(规则 6)。
|
|
11
|
+
* - 不计算评分公式;rrV1/accountRR/prism 均来自 cohort 已接线的 rival-rating 结果(规则 5)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 排行榜可展示的指标 key。值统一存原始量纲,缩放与单位由 format 决定。 */
|
|
15
|
+
export const leaderboardMetricKeySchema = z.enum([
|
|
16
|
+
"maps",
|
|
17
|
+
// 评分门面
|
|
18
|
+
"rivalhubRR", // accountRR(绝对刻度 RivalHub RR)
|
|
19
|
+
"hltvRating", // rrV1(HLTV Rating 2.0 量纲,已被逆向验证)
|
|
20
|
+
// core
|
|
21
|
+
"adr",
|
|
22
|
+
"kd",
|
|
23
|
+
"kpr",
|
|
24
|
+
"hsPercent",
|
|
25
|
+
// impact(产量家族:每 100 回合 X 次)
|
|
26
|
+
"firstKillPer100",
|
|
27
|
+
"multiKillPer100",
|
|
28
|
+
"clutchPer100",
|
|
29
|
+
"openingDuelWinRate",
|
|
30
|
+
// advanced
|
|
31
|
+
"kast",
|
|
32
|
+
"utilityDamagePerRound",
|
|
33
|
+
"awpKillsPerRound",
|
|
34
|
+
"awpKillRate",
|
|
35
|
+
"flashAssistPerRound",
|
|
36
|
+
"tradeKillRate"
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 渲染格式。所有百分比类指标在 builder 中已统一为 0–100 刻度,故只有一个 percent 格式:
|
|
41
|
+
* - integer: 整数
|
|
42
|
+
* - rating: 2 位小数(RR / HLTV)
|
|
43
|
+
* - adr: 1 位小数
|
|
44
|
+
* - ratio: 2 位小数(K/D、KPR、每 100 回合产量 FK/MK/C、每回合计数)
|
|
45
|
+
* - percent: 值已是 0–100,1 位小数 + %(HS% / KAST% / Entry% / AWP%)
|
|
46
|
+
*/
|
|
47
|
+
export const leaderboardFormatSchema = z.enum([
|
|
48
|
+
"integer",
|
|
49
|
+
"rating",
|
|
50
|
+
"adr",
|
|
51
|
+
"ratio",
|
|
52
|
+
"percent"
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export const leaderboardColumnSchema = z.object({
|
|
56
|
+
key: leaderboardMetricKeySchema,
|
|
57
|
+
label: z.string(),
|
|
58
|
+
format: leaderboardFormatSchema,
|
|
59
|
+
/** 列说明 / tooltip;无则 null。 */
|
|
60
|
+
description: z.string().nullable()
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const leaderboardViewKeySchema = z.enum(["core", "impact", "advanced"]);
|
|
64
|
+
|
|
65
|
+
export const leaderboardViewSchema = z.object({
|
|
66
|
+
key: leaderboardViewKeySchema,
|
|
67
|
+
label: z.string(),
|
|
68
|
+
/** 默认排序列(始终降序)。 */
|
|
69
|
+
defaultSort: leaderboardMetricKeySchema,
|
|
70
|
+
columns: z.array(leaderboardColumnSchema)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const seasonLeaderboardRowSchema = z.object({
|
|
74
|
+
playerKey: z.string(),
|
|
75
|
+
name: z.string(),
|
|
76
|
+
steamIds: z.array(z.string()),
|
|
77
|
+
externalUserId: z.string().nullable(),
|
|
78
|
+
teamKeys: z.array(teamKeySchema),
|
|
79
|
+
mapCount: z.number().int().positive(),
|
|
80
|
+
confidence: z.number().min(0).max(1),
|
|
81
|
+
/** 每个指标 key 都有值;不可得为 null(不伪造 0)。 */
|
|
82
|
+
metrics: z.record(leaderboardMetricKeySchema, z.number().nullable()),
|
|
83
|
+
/** PRISM 风格画像,仅表达风格,不进入排序;缺失为 null。 */
|
|
84
|
+
prism: z.custom<PrismResult>().nullable()
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const seasonLeaderboardModelSchema = z.object({
|
|
88
|
+
version: z.literal("cs2-demo-analysis-kit/leaderboard-0.1"),
|
|
89
|
+
weightsVersion: z.string(),
|
|
90
|
+
matchCount: z.number().int().nonnegative(),
|
|
91
|
+
provenance: seasonCohortBundleSchema.shape.provenance,
|
|
92
|
+
views: z.array(leaderboardViewSchema),
|
|
93
|
+
rows: z.array(seasonLeaderboardRowSchema)
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export type LeaderboardMetricKey = z.infer<typeof leaderboardMetricKeySchema>;
|
|
97
|
+
export type LeaderboardFormat = z.infer<typeof leaderboardFormatSchema>;
|
|
98
|
+
export type LeaderboardColumn = z.infer<typeof leaderboardColumnSchema>;
|
|
99
|
+
export type LeaderboardViewKey = z.infer<typeof leaderboardViewKeySchema>;
|
|
100
|
+
export type LeaderboardView = z.infer<typeof leaderboardViewSchema>;
|
|
101
|
+
export type SeasonLeaderboardRow = z.infer<typeof seasonLeaderboardRowSchema>;
|
|
102
|
+
export type SeasonLeaderboardModel = z.infer<typeof seasonLeaderboardModelSchema>;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { playerIndexSchema, sideSchema, steamId64Schema, teamKeySchema } from "./upstream.js";
|
|
3
|
+
|
|
4
|
+
/** Bump when the compact map-intelligence producer changes its factual meaning. */
|
|
5
|
+
export const MAP_INTELLIGENCE_FACT_VERSION = 6;
|
|
6
|
+
export const OPENING_RESPONSIBILITY_WINDOW_VERSION = 1;
|
|
7
|
+
export const CT_ROTATION_FACT_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
const availabilitySchema = z.enum(["available", "degraded", "missing"]);
|
|
10
|
+
|
|
11
|
+
export const mapIntelligenceAvailabilitySchema = z.object({
|
|
12
|
+
replay: availabilitySchema,
|
|
13
|
+
nav: availabilitySchema,
|
|
14
|
+
callouts: availabilitySchema,
|
|
15
|
+
shots: availabilitySchema,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const tacticalRegionSchema = z.enum(["a", "b", "mid"]);
|
|
19
|
+
export const ctRotationAvailabilitySchema = mapIntelligenceAvailabilitySchema.extend({
|
|
20
|
+
combatTimeline: availabilitySchema,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const positionGroupDwellSchema = z.object({
|
|
24
|
+
positionGroupId: z.string().min(1),
|
|
25
|
+
seconds: z.number().nonnegative(),
|
|
26
|
+
share: z.number().min(0).max(1),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const responsibilityWindowSchema = z.object({
|
|
30
|
+
version: z.literal(OPENING_RESPONSIBILITY_WINDOW_VERSION),
|
|
31
|
+
startTick: z.number().int().nonnegative(),
|
|
32
|
+
endTick: z.number().int().nonnegative(),
|
|
33
|
+
configuredSeconds: z.number().positive(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const openingPathPointSchema = z.object({
|
|
37
|
+
tick: z.number().int().nonnegative(),
|
|
38
|
+
callout: z.string().nullable(),
|
|
39
|
+
positionGroupId: z.string().nullable(),
|
|
40
|
+
x: z.number(), y: z.number(), z: z.number(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** A compact, locatable period where a player was separated from their team component. */
|
|
44
|
+
export const isolationSegmentSchema = z.object({
|
|
45
|
+
startTick: z.number().int().nonnegative(),
|
|
46
|
+
endTick: z.number().int().nonnegative(),
|
|
47
|
+
seconds: z.number().nonnegative(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** A conservative isolated-to-stable-group transition inside one round. */
|
|
51
|
+
export const delayedConvergenceSchema = z.object({
|
|
52
|
+
tick: z.number().int().nonnegative(),
|
|
53
|
+
priorIsolationSeconds: z.number().nonnegative(),
|
|
54
|
+
joinedComponentSize: z.number().int().min(3),
|
|
55
|
+
persistenceSeconds: z.number().positive(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const playerPositionRoundFactSchema = z.object({
|
|
59
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
60
|
+
matchId: z.string().min(1),
|
|
61
|
+
mapName: z.string().min(1),
|
|
62
|
+
roundNumber: z.number().int().positive(),
|
|
63
|
+
teamKey: teamKeySchema,
|
|
64
|
+
side: sideSchema,
|
|
65
|
+
playerIndex: playerIndexSchema,
|
|
66
|
+
steamId64: steamId64Schema,
|
|
67
|
+
economyType: z.enum(["pistol", "eco", "semi", "force", "full"]).nullable(),
|
|
68
|
+
openingWindow: responsibilityWindowSchema.nullable(),
|
|
69
|
+
openingEligibleSeconds: z.number().nonnegative().nullable(),
|
|
70
|
+
openingPositionGroupDwell: z.array(positionGroupDwellSchema),
|
|
71
|
+
openingMeanComponentSize: z.number().positive().nullable(),
|
|
72
|
+
openingIsolationSeconds: z.number().nonnegative().nullable(),
|
|
73
|
+
openingUtilityUseCount: z.number().int().nonnegative(),
|
|
74
|
+
openingPath: z.array(openingPathPointSchema).max(8),
|
|
75
|
+
/** Full-round movement/action coverage, from freeze end until death or round end. */
|
|
76
|
+
eligibleSeconds: z.number().nonnegative().nullable(),
|
|
77
|
+
positionGroupDwell: z.array(positionGroupDwellSchema),
|
|
78
|
+
unresolvedCalloutSeconds: z.number().nonnegative().nullable(),
|
|
79
|
+
calloutCoverage: z.number().min(0).max(1).nullable(),
|
|
80
|
+
meanNearestTeammateDistance: z.number().nonnegative().nullable(),
|
|
81
|
+
meanTeamCentroidDistance: z.number().nonnegative().nullable(),
|
|
82
|
+
meanComponentSize: z.number().positive().nullable(),
|
|
83
|
+
isolationSegments: z.array(isolationSegmentSchema),
|
|
84
|
+
rejoinTicks: z.array(z.number().int().nonnegative()),
|
|
85
|
+
delayedConvergences: z.array(delayedConvergenceSchema),
|
|
86
|
+
movementSync: z.number().min(-1).max(1).nullable(),
|
|
87
|
+
utilityUseCount: z.number().int().nonnegative(),
|
|
88
|
+
freezeAwpOwnership: z.boolean().nullable(),
|
|
89
|
+
activeAwpSeconds: z.number().nonnegative().nullable(),
|
|
90
|
+
awpShots: z.number().int().nonnegative().nullable(),
|
|
91
|
+
awpKills: z.number().int().nonnegative().nullable(),
|
|
92
|
+
availability: mapIntelligenceAvailabilitySchema,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const teamShapeWindowSchema = z.object({
|
|
96
|
+
startTick: z.number().int().nonnegative(),
|
|
97
|
+
endTick: z.number().int().nonnegative(),
|
|
98
|
+
coverageSeconds: z.number().nonnegative(),
|
|
99
|
+
/** Descending component sizes, e.g. [4, 1] for a 4+1. */
|
|
100
|
+
componentSizes: z.array(z.number().int().positive()).min(1),
|
|
101
|
+
partition: z.string().min(1),
|
|
102
|
+
componentPlayerIndices: z.array(z.array(playerIndexSchema).min(1)).min(1),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const teamShapeRoundFactSchema = z.object({
|
|
106
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
107
|
+
matchId: z.string().min(1),
|
|
108
|
+
mapName: z.string().min(1),
|
|
109
|
+
roundNumber: z.number().int().positive(),
|
|
110
|
+
teamKey: teamKeySchema,
|
|
111
|
+
side: sideSchema,
|
|
112
|
+
openingWindow: responsibilityWindowSchema.nullable(),
|
|
113
|
+
openingWindows: z.array(teamShapeWindowSchema),
|
|
114
|
+
/** Full-round component continuity windows. */
|
|
115
|
+
coverageSeconds: z.number().nonnegative().nullable(),
|
|
116
|
+
windows: z.array(teamShapeWindowSchema),
|
|
117
|
+
availability: mapIntelligenceAvailabilitySchema,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const teamAwpRoundFactSchema = z.object({
|
|
121
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
122
|
+
matchId: z.string().min(1),
|
|
123
|
+
mapName: z.string().min(1),
|
|
124
|
+
roundNumber: z.number().int().positive(),
|
|
125
|
+
teamKey: teamKeySchema,
|
|
126
|
+
side: sideSchema,
|
|
127
|
+
economyType: z.enum(["pistol", "eco", "semi", "force", "full"]),
|
|
128
|
+
opponentEconomyType: z.enum(["pistol", "eco", "semi", "force", "full"]),
|
|
129
|
+
scorePhase: z.enum(["first_half", "second_half", "overtime"]),
|
|
130
|
+
won: z.boolean(),
|
|
131
|
+
roundStartAwpPlayerIndices: z.array(playerIndexSchema),
|
|
132
|
+
doubleAwpActiveSeconds: z.number().nonnegative().nullable(),
|
|
133
|
+
awpActiveSeconds: z.number().nonnegative().nullable(),
|
|
134
|
+
awpShots: z.number().int().nonnegative().nullable(),
|
|
135
|
+
awpKills: z.number().int().nonnegative().nullable(),
|
|
136
|
+
awpDamage: z.number().nonnegative().nullable(),
|
|
137
|
+
openingKills: z.number().int().nonnegative(),
|
|
138
|
+
openingDeaths: z.number().int().nonnegative(),
|
|
139
|
+
availability: mapIntelligenceAvailabilitySchema,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/** One CT player-round observation. It records response facts, never a role label. */
|
|
143
|
+
export const ctRotationRoundFactSchema = z.object({
|
|
144
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
145
|
+
factVersion: z.literal(CT_ROTATION_FACT_VERSION),
|
|
146
|
+
matchId: z.string().min(1),
|
|
147
|
+
mapName: z.string().min(1),
|
|
148
|
+
roundNumber: z.number().int().positive(),
|
|
149
|
+
teamKey: teamKeySchema,
|
|
150
|
+
side: z.literal("ct"),
|
|
151
|
+
playerIndex: playerIndexSchema,
|
|
152
|
+
steamId64: steamId64Schema,
|
|
153
|
+
initialPositionGroupId: z.string().min(1).nullable(),
|
|
154
|
+
initialRegion: tacticalRegionSchema.nullable(),
|
|
155
|
+
initialPositionGroupShare: z.number().min(0).max(1).nullable(),
|
|
156
|
+
initialResponsibilityResolved: z.boolean(),
|
|
157
|
+
initialWindowEligibleSeconds: z.number().nonnegative().nullable(),
|
|
158
|
+
firstOwnAreaContactTick: z.number().int().nonnegative().nullable(),
|
|
159
|
+
firstOtherAreaContactTick: z.number().int().nonnegative().nullable(),
|
|
160
|
+
firstTeamContactTick: z.number().int().nonnegative().nullable(),
|
|
161
|
+
leftInitialPositionGroupTick: z.number().int().nonnegative().nullable(),
|
|
162
|
+
/** Signed: negative means the stable departure preceded the first observed other-area contact. */
|
|
163
|
+
leaveDelayAfterFirstOtherAreaContactSeconds: z.number().nullable(),
|
|
164
|
+
firstStableDestinationPositionGroupId: z.string().min(1).nullable(),
|
|
165
|
+
firstStableDestinationRegion: tacticalRegionSchema.nullable(),
|
|
166
|
+
crossedResponsibilityArea: z.boolean().nullable(),
|
|
167
|
+
returnedToInitialPositionGroup: z.boolean().nullable(),
|
|
168
|
+
transitToStableDestinationSeconds: z.number().nonnegative().nullable(),
|
|
169
|
+
/** Competition rank by observed departure tick; simultaneous departures share a rank. */
|
|
170
|
+
crossAreaDepartureOrder: z.number().int().positive().nullable(),
|
|
171
|
+
firstCrossAreaDeparture: z.boolean().nullable(),
|
|
172
|
+
priorCrossAreaDeparturesAlive: z.number().int().nonnegative().nullable(),
|
|
173
|
+
initialAreaStillCovered: z.boolean().nullable(),
|
|
174
|
+
deathTick: z.number().int().nonnegative().nullable(),
|
|
175
|
+
censoredByDeath: z.boolean().nullable(),
|
|
176
|
+
roundEndTick: z.number().int().nonnegative(),
|
|
177
|
+
availability: ctRotationAvailabilitySchema,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/** Compact per-match replay-derived map facts. It intentionally contains no role conclusion. */
|
|
181
|
+
export const matchMapIntelligenceFactsSchema = z.object({
|
|
182
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
183
|
+
matchId: z.string().min(1),
|
|
184
|
+
mapName: z.string().min(1),
|
|
185
|
+
playerPositionRounds: z.array(playerPositionRoundFactSchema),
|
|
186
|
+
teamShapeRounds: z.array(teamShapeRoundFactSchema),
|
|
187
|
+
teamAwpRounds: z.array(teamAwpRoundFactSchema),
|
|
188
|
+
ctRotationRounds: z.array(ctRotationRoundFactSchema),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export type MapIntelligenceAvailability = z.infer<typeof mapIntelligenceAvailabilitySchema>;
|
|
192
|
+
export type PositionGroupDwell = z.infer<typeof positionGroupDwellSchema>;
|
|
193
|
+
export type ResponsibilityWindow = z.infer<typeof responsibilityWindowSchema>;
|
|
194
|
+
export type IsolationSegment = z.infer<typeof isolationSegmentSchema>;
|
|
195
|
+
export type DelayedConvergence = z.infer<typeof delayedConvergenceSchema>;
|
|
196
|
+
export type PlayerPositionRoundFact = z.infer<typeof playerPositionRoundFactSchema>;
|
|
197
|
+
export type TeamShapeWindow = z.infer<typeof teamShapeWindowSchema>;
|
|
198
|
+
export type TeamShapeRoundFact = z.infer<typeof teamShapeRoundFactSchema>;
|
|
199
|
+
export type TeamAwpRoundFact = z.infer<typeof teamAwpRoundFactSchema>;
|
|
200
|
+
export type CtRotationAvailability = z.infer<typeof ctRotationAvailabilitySchema>;
|
|
201
|
+
export type CtRotationRoundFact = z.infer<typeof ctRotationRoundFactSchema>;
|
|
202
|
+
export type MatchMapIntelligenceFacts = z.infer<typeof matchMapIntelligenceFactsSchema>;
|
package/src/map-role.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { evidenceRefSchema } from "./evidence.js";
|
|
3
|
+
|
|
4
|
+
/** Bump when aggregation or role-selection semantics change. */
|
|
5
|
+
export const MAP_ROLE_EVIDENCE_VERSION = 4;
|
|
6
|
+
export const T_RESPONSIBILITY_RESEARCH_PROJECTION_VERSION = "cs2-demo-analysis-kit/t-responsibility-research-projection-1.0";
|
|
7
|
+
|
|
8
|
+
/** The supported active-duty pool. Unknown maps are deliberately not generalized. */
|
|
9
|
+
export const supportedMapNameSchema = z.enum([
|
|
10
|
+
"de_ancient", "de_anubis", "de_dust2", "de_inferno", "de_mirage", "de_nuke", "de_overpass"
|
|
11
|
+
]);
|
|
12
|
+
export const mapRoleStatusSchema = z.enum(["ready", "mixed", "insufficient", "unknown"]);
|
|
13
|
+
export const inferredMapRoleSchema = z.enum(["awper", "anchor", "opener", "closer"]);
|
|
14
|
+
export const declaredRoleSchema = z.enum(["igl", "awper", "anchor", "opener", "closer"]);
|
|
15
|
+
export const weaponDutySchema = z.enum(["primary_awper", "secondary_awper", "situational_awper", "rifler"]);
|
|
16
|
+
export const teamResponsibilitySchema = z.enum([
|
|
17
|
+
"pack", "extremity", "late_joining", "stable_default",
|
|
18
|
+
"anchor_tendency", "component_mobile", "independent_mobile", "stable_position",
|
|
19
|
+
"mixed", "unknown",
|
|
20
|
+
]);
|
|
21
|
+
export const roleModifierSchema = z.enum(["utility_supportive", "positionally_stable", "spatially_isolated", "component_mobile"]);
|
|
22
|
+
export const tResponsibilityResearchCandidateSchema = z.enum(["pack", "lurker", "flexible"]);
|
|
23
|
+
|
|
24
|
+
export const tResponsibilityResearchFeaturesSchema = z.object({
|
|
25
|
+
dominantGroupStability: z.number().min(0).max(1).nullable(),
|
|
26
|
+
teamRelativeGroupShare: z.number().min(-1).max(1).nullable(),
|
|
27
|
+
openingIsolatedShare: z.number().min(0).max(1).nullable(),
|
|
28
|
+
isolationShare: z.number().min(0).max(1).nullable(),
|
|
29
|
+
delayedConvergenceShare: z.number().min(0).max(1).nullable(),
|
|
30
|
+
movementSync: z.number().min(-1).max(1).nullable(),
|
|
31
|
+
positionTopShare: z.number().min(0).max(1).nullable(),
|
|
32
|
+
openingLargestShare: z.number().min(0).max(1).nullable(),
|
|
33
|
+
fullLargestShare: z.number().min(0).max(1).nullable(),
|
|
34
|
+
meanTeamCentroidDistance: z.number().nonnegative().nullable(),
|
|
35
|
+
openingPathDisplacement: z.number().nonnegative().nullable(),
|
|
36
|
+
openingPathTransitions: z.number().nonnegative().nullable(),
|
|
37
|
+
openingPositionEntropy: z.number().min(0).max(1).nullable(),
|
|
38
|
+
fullPositionEntropy: z.number().min(0).max(1).nullable(),
|
|
39
|
+
rejoinsPerMinute: z.number().nonnegative().nullable(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Product-neutral declaration scope. Storage ids, namespaces and timestamps intentionally do not belong here. */
|
|
43
|
+
const declarationScopeSchema = z.object({
|
|
44
|
+
playerKey: z.string().min(1),
|
|
45
|
+
source: z.enum(["user", "self_report", "organizer", "event_package", "trusted_metadata"]),
|
|
46
|
+
mapName: supportedMapNameSchema.optional(),
|
|
47
|
+
teamKey: z.string().min(1).optional(),
|
|
48
|
+
validFrom: z.string().datetime().optional(),
|
|
49
|
+
validTo: z.string().datetime().optional(),
|
|
50
|
+
provenance: z.string().min(1),
|
|
51
|
+
}).strict();
|
|
52
|
+
|
|
53
|
+
/** A declared tactical role. priority belongs only to this kind of declaration. */
|
|
54
|
+
export const mainRoleDeclarationSchema = declarationScopeSchema.extend({
|
|
55
|
+
kind: z.literal("main_role"),
|
|
56
|
+
role: declaredRoleSchema,
|
|
57
|
+
priority: z.enum(["primary", "secondary"]),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** A declared weapon duty. It is deliberately independent from tactical-role priority. */
|
|
61
|
+
export const weaponDutyDeclarationSchema = declarationScopeSchema.extend({
|
|
62
|
+
kind: z.literal("weapon_duty"),
|
|
63
|
+
weaponDuty: weaponDutySchema,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const roleDeclarationSchema = z.discriminatedUnion("kind", [mainRoleDeclarationSchema, weaponDutyDeclarationSchema]).superRefine((value, context) => {
|
|
67
|
+
if (value.validFrom != null && value.validTo != null && value.validFrom > value.validTo) context.addIssue({ code: z.ZodIssueCode.custom, message: "validFrom must not be after validTo" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const roleEvidenceLocatorSchema = z.object({
|
|
71
|
+
matchId: z.string().min(1),
|
|
72
|
+
roundNumber: z.number().int().positive(),
|
|
73
|
+
positionGroupId: z.string().min(1).optional(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Same-event research projection. It is intentionally separate from
|
|
78
|
+
* PlayerMapRoleEvidence.responsibility and is not a production role conclusion.
|
|
79
|
+
*/
|
|
80
|
+
export const tResponsibilityResearchProjectionSchema = z.object({
|
|
81
|
+
version: z.literal(T_RESPONSIBILITY_RESEARCH_PROJECTION_VERSION),
|
|
82
|
+
modelId: z.literal("cologne-major-2026/parsimonious-v1-full-fit"),
|
|
83
|
+
playerKey: z.string().min(1),
|
|
84
|
+
teamKey: z.string().min(1),
|
|
85
|
+
side: z.literal("t"),
|
|
86
|
+
status: mapRoleStatusSchema,
|
|
87
|
+
candidate: tResponsibilityResearchCandidateSchema.nullable(),
|
|
88
|
+
packScore: z.number().min(0).max(1).nullable(),
|
|
89
|
+
lurkerScore: z.number().min(0).max(1).nullable(),
|
|
90
|
+
confidence: z.number().min(0).max(1).nullable(),
|
|
91
|
+
sample: z.object({
|
|
92
|
+
observedRounds: z.number().int().nonnegative(),
|
|
93
|
+
eligibleRounds: z.number().int().nonnegative(),
|
|
94
|
+
eligibleSeconds: z.number().nonnegative(),
|
|
95
|
+
matchCount: z.number().int().nonnegative(),
|
|
96
|
+
mapCount: z.number().int().nonnegative(),
|
|
97
|
+
dataQuality: z.number().min(0).max(1).nullable(),
|
|
98
|
+
}),
|
|
99
|
+
features: tResponsibilityResearchFeaturesSchema,
|
|
100
|
+
matchIds: z.array(z.string().min(1)),
|
|
101
|
+
representativeRounds: z.array(roleEvidenceLocatorSchema).max(8),
|
|
102
|
+
basis: z.array(z.string()),
|
|
103
|
+
limitations: z.array(z.string()),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export const mapPositionGroupEvidenceSchema = z.object({
|
|
107
|
+
positionGroupId: z.string().min(1),
|
|
108
|
+
seconds: z.number().nonnegative(),
|
|
109
|
+
share: z.number().min(0).max(1),
|
|
110
|
+
roundCount: z.number().int().nonnegative(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const awpResponsibilityEvidenceSchema = z.object({
|
|
114
|
+
duty: weaponDutySchema,
|
|
115
|
+
eligibleRounds: z.number().int().nonnegative(),
|
|
116
|
+
freezeOwnershipRounds: z.number().int().nonnegative(),
|
|
117
|
+
activeSeconds: z.number().nonnegative().nullable(),
|
|
118
|
+
shots: z.number().int().nonnegative().nullable(),
|
|
119
|
+
kills: z.number().int().nonnegative().nullable(),
|
|
120
|
+
teamActiveShare: z.number().min(0).max(1).nullable(),
|
|
121
|
+
/** Concentration/exclusivity of observed AWP active time inside this team/map/side cell. */
|
|
122
|
+
usageConcentration: z.number().min(0).max(1).nullable(),
|
|
123
|
+
matchConsistency: z.number().min(0).max(1).nullable(),
|
|
124
|
+
qualifiedLongGunRounds: z.number().int().nonnegative(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** One identity × canonical team × map × side evidence cell. No role conclusion is stored here. */
|
|
128
|
+
export const playerMapRoleEvidenceSchema = z.object({
|
|
129
|
+
version: z.literal(MAP_ROLE_EVIDENCE_VERSION),
|
|
130
|
+
playerKey: z.string().min(1),
|
|
131
|
+
teamKey: z.string().min(1),
|
|
132
|
+
mapName: supportedMapNameSchema,
|
|
133
|
+
side: z.enum(["t", "ct"]),
|
|
134
|
+
status: mapRoleStatusSchema,
|
|
135
|
+
confidence: z.number().min(0).max(1),
|
|
136
|
+
sample: z.object({
|
|
137
|
+
observedRounds: z.number().int().nonnegative(),
|
|
138
|
+
eligibleRounds: z.number().int().nonnegative(),
|
|
139
|
+
eligibleSeconds: z.number().nonnegative(),
|
|
140
|
+
matchCount: z.number().int().nonnegative(),
|
|
141
|
+
dataQuality: z.number().min(0).max(1),
|
|
142
|
+
coverage: z.number().min(0).max(1).nullable(),
|
|
143
|
+
}),
|
|
144
|
+
/** Complete compact match coverage for aggregation and declaration time scope; not an evidence sample. */
|
|
145
|
+
matchIds: z.array(z.string().min(1)),
|
|
146
|
+
positionGroups: z.array(mapPositionGroupEvidenceSchema),
|
|
147
|
+
spatial: z.object({
|
|
148
|
+
dominantGroupStability: z.number().min(0).max(1).nullable(),
|
|
149
|
+
teamRelativeGroupShare: z.number().min(-1).max(1).nullable(),
|
|
150
|
+
isolationSeconds: z.number().nonnegative().nullable(),
|
|
151
|
+
isolationShare: z.number().min(0).max(1).nullable(),
|
|
152
|
+
rejoinCount: z.number().int().nonnegative().nullable(),
|
|
153
|
+
delayedConvergenceRoundShare: z.number().min(0).max(1).nullable(),
|
|
154
|
+
movementSync: z.number().min(-1).max(1).nullable(),
|
|
155
|
+
openingMainComponentShare: z.number().min(0).max(1).nullable(),
|
|
156
|
+
openingNoUniqueCoreShare: z.number().min(0).max(1).nullable(),
|
|
157
|
+
openingIsolatedShare: z.number().min(0).max(1).nullable(),
|
|
158
|
+
formationShares: z.record(z.number().min(0).max(1)),
|
|
159
|
+
}),
|
|
160
|
+
support: z.object({
|
|
161
|
+
utilityUses: z.number().int().nonnegative(),
|
|
162
|
+
openingUtilityUses: z.number().int().nonnegative(),
|
|
163
|
+
utilityUsePerRound: z.number().nonnegative(),
|
|
164
|
+
openingUtilityUsePerRound: z.number().nonnegative(),
|
|
165
|
+
}),
|
|
166
|
+
responsibility: teamResponsibilitySchema,
|
|
167
|
+
modifiers: z.array(roleModifierSchema),
|
|
168
|
+
awp: awpResponsibilityEvidenceSchema,
|
|
169
|
+
representativeRounds: z.array(roleEvidenceLocatorSchema).max(5),
|
|
170
|
+
basis: z.array(z.string()),
|
|
171
|
+
limitations: z.array(z.string()),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export const teamMapResponsibilityEvidenceSchema = z.object({
|
|
175
|
+
version: z.literal(MAP_ROLE_EVIDENCE_VERSION),
|
|
176
|
+
teamKey: z.string().min(1),
|
|
177
|
+
mapName: supportedMapNameSchema,
|
|
178
|
+
side: z.enum(["t", "ct"]),
|
|
179
|
+
status: mapRoleStatusSchema,
|
|
180
|
+
confidence: z.number().min(0).max(1),
|
|
181
|
+
players: z.array(playerMapRoleEvidenceSchema),
|
|
182
|
+
positionOverlap: z.array(z.object({ positionGroupId: z.string().min(1), playerKeys: z.array(z.string().min(1)).min(2), share: z.number().min(0).max(1) })),
|
|
183
|
+
positionConcentration: z.number().min(0).max(1).nullable(),
|
|
184
|
+
unstableCoverage: z.boolean(),
|
|
185
|
+
representativeRounds: z.array(roleEvidenceLocatorSchema).max(5),
|
|
186
|
+
basis: z.array(z.string()),
|
|
187
|
+
limitations: z.array(z.string()),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export const playerMapRoleProfileSchema = z.object({
|
|
191
|
+
version: z.literal("cs2-demo-analysis-kit/player-map-role-profile-5.0"),
|
|
192
|
+
playerKey: z.string().min(1),
|
|
193
|
+
teamKey: z.string().min(1),
|
|
194
|
+
declaredRoles: z.array(mainRoleDeclarationSchema),
|
|
195
|
+
declaredWeaponDuties: z.array(weaponDutyDeclarationSchema),
|
|
196
|
+
inferredPrimaryRole: inferredMapRoleSchema.nullable(),
|
|
197
|
+
runnerUpRole: inferredMapRoleSchema.nullable(),
|
|
198
|
+
separationMargin: z.number().min(0).max(1).nullable(),
|
|
199
|
+
roleEvidenceScores: z.object({ awper: z.number().min(0).max(1), anchor: z.number().min(0).max(1), opener: z.number().min(0).max(1), closer: z.number().min(0).max(1) }),
|
|
200
|
+
headlineRole: z.union([inferredMapRoleSchema, z.literal("IGL"), z.literal("IGL / AWPer")]).nullable(),
|
|
201
|
+
status: mapRoleStatusSchema,
|
|
202
|
+
confidence: z.number().min(0).max(1),
|
|
203
|
+
weaponDuty: weaponDutySchema.nullable(),
|
|
204
|
+
positionGroupDisplay: z.array(z.object({
|
|
205
|
+
mapName: supportedMapNameSchema,
|
|
206
|
+
side: z.enum(["t", "ct"]),
|
|
207
|
+
positionGroupId: z.string().min(1),
|
|
208
|
+
displayName: z.string().min(1),
|
|
209
|
+
officialName: z.string().nullable(),
|
|
210
|
+
resolved: z.boolean(),
|
|
211
|
+
})),
|
|
212
|
+
roleAlignments: z.array(z.object({
|
|
213
|
+
declaration: mainRoleDeclarationSchema,
|
|
214
|
+
declaredPrimary: declaredRoleSchema.nullable(),
|
|
215
|
+
declaredSecondary: z.array(declaredRoleSchema),
|
|
216
|
+
inferredPrimary: inferredMapRoleSchema.nullable(),
|
|
217
|
+
overall: z.enum(["aligned", "partially_aligned", "different_observation", "not_comparable"]),
|
|
218
|
+
tSide: z.string(),
|
|
219
|
+
ctSide: z.string(),
|
|
220
|
+
disagreementReasons: z.array(z.string()),
|
|
221
|
+
sampleLimitations: z.array(z.string()),
|
|
222
|
+
})),
|
|
223
|
+
weaponDutyAlignments: z.array(z.object({
|
|
224
|
+
declaration: weaponDutyDeclarationSchema,
|
|
225
|
+
observedWeaponDuty: weaponDutySchema.nullable(),
|
|
226
|
+
overall: z.enum(["aligned", "different_observation", "not_comparable"]),
|
|
227
|
+
sampleLimitations: z.array(z.string()),
|
|
228
|
+
})),
|
|
229
|
+
perMapEvidence: z.array(playerMapRoleEvidenceSchema),
|
|
230
|
+
evidence: z.array(evidenceRefSchema),
|
|
231
|
+
basis: z.array(z.string()),
|
|
232
|
+
limitations: z.array(z.string()),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
export const teamMapRoleMatrixSchema = z.object({
|
|
236
|
+
version: z.literal("cs2-demo-analysis-kit/team-map-role-matrix-4.0"),
|
|
237
|
+
teamKey: z.string().min(1),
|
|
238
|
+
mapName: supportedMapNameSchema,
|
|
239
|
+
side: z.enum(["t", "ct"]),
|
|
240
|
+
status: mapRoleStatusSchema,
|
|
241
|
+
confidence: z.number().min(0).max(1),
|
|
242
|
+
players: z.array(z.object({
|
|
243
|
+
playerKey: z.string().min(1),
|
|
244
|
+
primaryPositionGroups: z.array(mapPositionGroupEvidenceSchema.extend({ displayName: z.string().min(1), officialName: z.string().nullable(), resolved: z.boolean() })),
|
|
245
|
+
responsibility: teamResponsibilitySchema,
|
|
246
|
+
modifiers: z.array(roleModifierSchema),
|
|
247
|
+
sampleRounds: z.number().int().nonnegative(),
|
|
248
|
+
confidence: z.number().min(0).max(1),
|
|
249
|
+
weaponDuty: weaponDutySchema.nullable(),
|
|
250
|
+
evidence: z.array(evidenceRefSchema),
|
|
251
|
+
})),
|
|
252
|
+
positionOverlap: teamMapResponsibilityEvidenceSchema.shape.positionOverlap,
|
|
253
|
+
positionConcentration: z.number().min(0).max(1).nullable(),
|
|
254
|
+
unstableCoverage: z.boolean(),
|
|
255
|
+
representativeRounds: z.array(evidenceRefSchema),
|
|
256
|
+
basis: z.array(z.string()),
|
|
257
|
+
limitations: z.array(z.string()),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const countBreakdownSchema = z.object({ key: z.string().min(1), rounds: z.number().int().nonnegative() });
|
|
261
|
+
export const doubleAwpAnalysisSchema = z.object({
|
|
262
|
+
version: z.literal("cs2-demo-analysis-kit/double-awp-analysis-2.0"),
|
|
263
|
+
teamKey: z.string().min(1),
|
|
264
|
+
side: z.enum(["t", "ct"]),
|
|
265
|
+
status: mapRoleStatusSchema,
|
|
266
|
+
qualifiedRoundCount: z.number().int().nonnegative(),
|
|
267
|
+
doubleAwpRoundCount: z.number().int().nonnegative(),
|
|
268
|
+
eligibleRoundShare: z.number().min(0).max(1).nullable(),
|
|
269
|
+
combinations: z.array(z.object({ playerKeys: z.array(z.string().min(1)).min(2), rounds: z.number().int().positive() })),
|
|
270
|
+
mapDistribution: z.array(countBreakdownSchema),
|
|
271
|
+
scorePhaseDistribution: z.array(countBreakdownSchema),
|
|
272
|
+
economyDistribution: z.array(countBreakdownSchema),
|
|
273
|
+
opponentEconomyDistribution: z.array(countBreakdownSchema),
|
|
274
|
+
wins: z.number().int().nonnegative(),
|
|
275
|
+
winRate: z.number().min(0).max(1).nullable(),
|
|
276
|
+
openingKills: z.number().int().nonnegative(),
|
|
277
|
+
openingDeaths: z.number().int().nonnegative(),
|
|
278
|
+
roundStartAwpOwnerships: z.number().int().nonnegative(),
|
|
279
|
+
activeAwpSeconds: z.number().nonnegative().nullable(),
|
|
280
|
+
doubleAwpActiveSeconds: z.number().nonnegative().nullable(),
|
|
281
|
+
awpKills: z.number().int().nonnegative().nullable(),
|
|
282
|
+
awpDamage: z.number().nonnegative().nullable(),
|
|
283
|
+
evidence: z.array(evidenceRefSchema),
|
|
284
|
+
basis: z.array(z.string()),
|
|
285
|
+
limitations: z.array(z.string()),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
export const playerMapPoolRowSchema = z.object({
|
|
289
|
+
mapName: supportedMapNameSchema,
|
|
290
|
+
matchCount: z.number().int().nonnegative(), roundCount: z.number().int().nonnegative(), wins: z.number().int().nonnegative(), losses: z.number().int().nonnegative(), winRate: z.number().min(0).max(1).nullable(),
|
|
291
|
+
rr: z.number().nonnegative().nullable(), adr: z.number().nonnegative().nullable(), kast: z.number().min(0).max(100).nullable(),
|
|
292
|
+
openingKills: z.number().int().nonnegative(), openingDeaths: z.number().int().nonnegative(),
|
|
293
|
+
mainWeapon: z.string().nullable(), globalWeaponDuty: weaponDutySchema.nullable(), mapSideAwpUsage: z.array(z.object({ side: z.enum(["t", "ct"]), duty: weaponDutySchema, qualifiedRounds: z.number().int().nonnegative(), activeSeconds: z.number().nonnegative().nullable() })),
|
|
294
|
+
tPositionGroup: z.string().nullable(), ctPositionGroup: z.string().nullable(), tResponsibility: teamResponsibilitySchema, ctResponsibility: teamResponsibilitySchema,
|
|
295
|
+
sampleQuality: z.number().min(0).max(1), confidence: z.number().min(0).max(1), evidence: z.array(evidenceRefSchema),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
export type SupportedMapName = z.infer<typeof supportedMapNameSchema>;
|
|
299
|
+
export type MapRoleStatus = z.infer<typeof mapRoleStatusSchema>;
|
|
300
|
+
export type InferredMapRole = z.infer<typeof inferredMapRoleSchema>;
|
|
301
|
+
export type DeclaredRole = z.infer<typeof declaredRoleSchema>;
|
|
302
|
+
export type WeaponDuty = z.infer<typeof weaponDutySchema>;
|
|
303
|
+
export type TeamResponsibility = z.infer<typeof teamResponsibilitySchema>;
|
|
304
|
+
export type RoleModifier = z.infer<typeof roleModifierSchema>;
|
|
305
|
+
export type TResponsibilityResearchCandidate = z.infer<typeof tResponsibilityResearchCandidateSchema>;
|
|
306
|
+
export type TResponsibilityResearchFeatures = z.infer<typeof tResponsibilityResearchFeaturesSchema>;
|
|
307
|
+
export type TResponsibilityResearchProjection = z.infer<typeof tResponsibilityResearchProjectionSchema>;
|
|
308
|
+
export type RoleDeclaration = z.infer<typeof roleDeclarationSchema>;
|
|
309
|
+
export type MainRoleDeclaration = z.infer<typeof mainRoleDeclarationSchema>;
|
|
310
|
+
export type WeaponDutyDeclaration = z.infer<typeof weaponDutyDeclarationSchema>;
|
|
311
|
+
export type RoleEvidenceLocator = z.infer<typeof roleEvidenceLocatorSchema>;
|
|
312
|
+
export type PlayerMapRoleEvidence = z.infer<typeof playerMapRoleEvidenceSchema>;
|
|
313
|
+
export type TeamMapResponsibilityEvidence = z.infer<typeof teamMapResponsibilityEvidenceSchema>;
|
|
314
|
+
export type PlayerMapRoleProfile = z.infer<typeof playerMapRoleProfileSchema>;
|
|
315
|
+
export type TeamMapRoleMatrix = z.infer<typeof teamMapRoleMatrixSchema>;
|
|
316
|
+
export type DoubleAwpAnalysis = z.infer<typeof doubleAwpAnalysisSchema>;
|
|
317
|
+
export type PlayerMapPoolRow = z.infer<typeof playerMapPoolRowSchema>;
|