wows-compute 0.0.0-stage → 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.
@@ -0,0 +1,243 @@
1
+ //#region src/strength/index.ts
2
+ /** Display / analysis order; not a composite score. */
3
+ const ROLE_ORDER = [
4
+ "submarine",
5
+ "aircraft-carrier",
6
+ "destroyer",
7
+ "battleship",
8
+ "cruiser-functional",
9
+ "cruiser-firepower",
10
+ "cruiser",
11
+ "unknown"
12
+ ];
13
+ function isRatedValue(value) {
14
+ return "value" in value && "basis" in value && "exact" in value;
15
+ }
16
+ function normalize(value) {
17
+ if (value === void 0) return {
18
+ pr: null,
19
+ winRate: null,
20
+ battles: null,
21
+ avgDamage: null,
22
+ avgFrags: null,
23
+ hidden: false
24
+ };
25
+ if (isRatedValue(value)) return {
26
+ pr: value,
27
+ winRate: null,
28
+ battles: value.sample.matches,
29
+ avgDamage: null,
30
+ avgFrags: null,
31
+ hidden: false
32
+ };
33
+ return {
34
+ pr: value.pr ?? null,
35
+ winRate: value.winRate ?? null,
36
+ battles: value.battles ?? value.pr?.sample.matches ?? null,
37
+ avgDamage: value.avgDamage ?? null,
38
+ avgFrags: value.avgFrags ?? null,
39
+ hidden: value.hidden ?? false
40
+ };
41
+ }
42
+ /** Maps a roster to scatter-plot points. Bots and hidden players stay in the list. */
43
+ function buildPlayerPoints(roster, ratings, catalog) {
44
+ return roster.participants.map((participant) => {
45
+ const rating = participant.accountId === null ? normalize(void 0) : normalize(ratings.get(participant.accountId));
46
+ return {
47
+ accountId: participant.accountId,
48
+ name: participant.name,
49
+ teamId: null,
50
+ shipId: participant.shipId,
51
+ shipRole: participant.shipId === null ? "unknown" : catalog?.role?.(participant.shipId) ?? "unknown",
52
+ tier: participant.shipId === null ? null : catalog?.tier?.(participant.shipId) ?? null,
53
+ divisionId: null,
54
+ winRate: rating.winRate,
55
+ pr: rating.pr?.value ?? null,
56
+ prExact: rating.pr?.exact ?? false,
57
+ battles: rating.battles,
58
+ isBot: participant.isBot,
59
+ relation: participant.relation,
60
+ avgDamage: rating.avgDamage,
61
+ avgFrags: rating.avgFrags,
62
+ hidden: rating.hidden
63
+ };
64
+ });
65
+ }
66
+ function median(values) {
67
+ if (values.length === 0) return null;
68
+ const sorted = [...values].sort((a, b) => a - b);
69
+ const middle = Math.floor(sorted.length / 2);
70
+ if (sorted.length % 2 === 1) return sorted[middle];
71
+ return (sorted[middle - 1] + sorted[middle]) / 2;
72
+ }
73
+ function referenceLines(points) {
74
+ return {
75
+ winRateMedian: median(points.map((point) => point.winRate).filter((value) => value !== null)),
76
+ prMedian: median(points.map((point) => point.pr).filter((value) => value !== null))
77
+ };
78
+ }
79
+ /**
80
+ * Quadrants relative to the reference lines.
81
+ *
82
+ * q1 high win rate + high PR, q2 low win rate + high PR,
83
+ * q3 low + low, q4 high win rate + low PR.
84
+ */
85
+ function quadrantOf(point, reference) {
86
+ if (point.winRate === null || point.pr === null || reference.winRateMedian === null || reference.prMedian === null) return "unknown";
87
+ const highWinRate = point.winRate >= reference.winRateMedian;
88
+ const highPr = point.pr >= reference.prMedian;
89
+ if (highWinRate) return highPr ? "q1" : "q4";
90
+ return highPr ? "q2" : "q3";
91
+ }
92
+ function roleCoverage(ally, enemy) {
93
+ const coverage = {
94
+ total: 0,
95
+ resolved: 0,
96
+ hidden: 0,
97
+ bot: 0,
98
+ failed: 0
99
+ };
100
+ for (const point of [...ally, ...enemy]) {
101
+ coverage.total += 1;
102
+ if (point.isBot) {
103
+ coverage.bot += 1;
104
+ continue;
105
+ }
106
+ if (point.accountId === null) {
107
+ coverage.hidden += 1;
108
+ continue;
109
+ }
110
+ if (point.pr === null) {
111
+ coverage.failed += 1;
112
+ continue;
113
+ }
114
+ coverage.resolved += 1;
115
+ }
116
+ return coverage;
117
+ }
118
+ function diff(left, right) {
119
+ return left === null || right === null ? null : left - right;
120
+ }
121
+ /**
122
+ * Per-role ally/enemy comparison.
123
+ *
124
+ * Returns differences and samples only; no composite score and no prediction.
125
+ */
126
+ function pairByRole(ally, enemy, profile) {
127
+ const allyByRole = /* @__PURE__ */ new Map();
128
+ const enemyByRole = /* @__PURE__ */ new Map();
129
+ for (const point of ally) {
130
+ const list = allyByRole.get(point.shipRole) ?? [];
131
+ list.push(point);
132
+ allyByRole.set(point.shipRole, list);
133
+ }
134
+ for (const point of enemy) {
135
+ const list = enemyByRole.get(point.shipRole) ?? [];
136
+ list.push(point);
137
+ enemyByRole.set(point.shipRole, list);
138
+ }
139
+ return ROLE_ORDER.filter((role) => allyByRole.has(role) || enemyByRole.has(role)).map((role) => {
140
+ const allies = allyByRole.get(role) ?? [];
141
+ const enemies = enemyByRole.get(role) ?? [];
142
+ const allyMedianPr = median(allies.map((point) => point.pr).filter((value) => value !== null));
143
+ const enemyMedianPr = median(enemies.map((point) => point.pr).filter((value) => value !== null));
144
+ const allyMedianWinRate = median(allies.map((point) => point.winRate).filter((value) => value !== null));
145
+ const enemyMedianWinRate = median(enemies.map((point) => point.winRate).filter((value) => value !== null));
146
+ return {
147
+ role,
148
+ allyCount: allies.length,
149
+ enemyCount: enemies.length,
150
+ allyMedianPr,
151
+ enemyMedianPr,
152
+ allyMedianWinRate,
153
+ enemyMedianWinRate,
154
+ prDiff: diff(allyMedianPr, enemyMedianPr),
155
+ winRateDiff: diff(allyMedianWinRate, enemyMedianWinRate),
156
+ weight: profile?.weights?.[role] ?? null,
157
+ sample: {
158
+ ally: allies.length,
159
+ enemy: enemies.length
160
+ },
161
+ coverage: roleCoverage(allies, enemies)
162
+ };
163
+ });
164
+ }
165
+ function summaryCoverage(points) {
166
+ const coverage = {
167
+ total: 0,
168
+ resolved: 0,
169
+ hidden: 0,
170
+ bot: 0,
171
+ failed: 0
172
+ };
173
+ for (const point of points) {
174
+ coverage.total += 1;
175
+ if (point.isBot) {
176
+ coverage.bot += 1;
177
+ continue;
178
+ }
179
+ if (point.hidden || point.accountId === null) {
180
+ coverage.hidden += 1;
181
+ continue;
182
+ }
183
+ if (point.winRate === null && point.avgDamage === null && point.avgFrags === null && point.pr === null) {
184
+ coverage.failed += 1;
185
+ continue;
186
+ }
187
+ coverage.resolved += 1;
188
+ }
189
+ return coverage;
190
+ }
191
+ function meanOf(points, metric, weighting) {
192
+ let total = 0;
193
+ let weightSum = 0;
194
+ for (const point of points) {
195
+ const value = point[metric];
196
+ if (value === null) continue;
197
+ const weight = weighting === "battles" ? point.battles ?? 0 : 1;
198
+ if (weight <= 0) continue;
199
+ total += value * weight;
200
+ weightSum += weight;
201
+ }
202
+ return weightSum === 0 ? null : total / weightSum;
203
+ }
204
+ /**
205
+ * Reduces normalized player points into one side's overview.
206
+ *
207
+ * Averages only over non-null values; `0` is a valid value. Empty input
208
+ * returns an all-zero summary rather than throwing.
209
+ */
210
+ function summarizePlayers(points, options = {}) {
211
+ const weighting = options.weighting ?? "equal";
212
+ const coverage = summaryCoverage(points);
213
+ const members = points.filter((point) => !point.isBot);
214
+ let battles = 0;
215
+ for (const point of members) if (point.battles !== null) battles += point.battles;
216
+ return {
217
+ total: coverage.total,
218
+ players: coverage.total - coverage.bot,
219
+ battles,
220
+ coverage,
221
+ winRate: meanOf(members, "winRate", weighting),
222
+ avgDamage: meanOf(members, "avgDamage", weighting),
223
+ avgFrags: meanOf(members, "avgFrags", weighting),
224
+ pr: meanOf(members, "pr", weighting),
225
+ prExact: false
226
+ };
227
+ }
228
+ /** Groups points by `relation` and summarizes both sides. */
229
+ function summarizeRoster(points, options = {}) {
230
+ const ally = [];
231
+ const enemy = [];
232
+ let unassigned = 0;
233
+ for (const point of points) if (point.relation === "self" || point.relation === "ally") ally.push(point);
234
+ else if (point.relation === "enemy") enemy.push(point);
235
+ else unassigned += 1;
236
+ return {
237
+ ally: summarizePlayers(ally, options),
238
+ enemy: summarizePlayers(enemy, options),
239
+ unassigned
240
+ };
241
+ }
242
+ //#endregion
243
+ export { quadrantOf as a, summarizeRoster as c, pairByRole as i, buildPlayerPoints as n, referenceLines as o, median as r, summarizePlayers as s, ROLE_ORDER as t };
@@ -0,0 +1,3 @@
1
+ import { _ as pairByRole, a as PlayerSummary, b as summarizePlayers, c as ReferenceLines, d as RosterSummary, f as ShipCatalogLike, g as median, h as buildPlayerPoints, i as PlayerRatings, l as RoleMatchup, m as SummaryOptions, n as PlayerRatingSnapshot, o as Quadrant, p as ShipRole, r as PlayerRatingValue, s as ROLE_ORDER, t as PlayerPoint, u as RoleWeightProfile, v as quadrantOf, x as summarizeRoster, y as referenceLines } from "./index-CkKx_Wtz.mjs";
2
+ import { o as groupDivisions } from "./index-_QacyXDj.mjs";
3
+ export { PlayerPoint, PlayerRatingSnapshot, PlayerRatingValue, PlayerRatings, PlayerSummary, Quadrant, ROLE_ORDER, ReferenceLines, RoleMatchup, RoleWeightProfile, RosterSummary, ShipCatalogLike, ShipRole, SummaryOptions, buildPlayerPoints, groupDivisions, median, pairByRole, quadrantOf, referenceLines, summarizePlayers, summarizeRoster };
@@ -0,0 +1,3 @@
1
+ import { i as groupDivisions } from "./match-J2T9QiGg.mjs";
2
+ import { a as quadrantOf, c as summarizeRoster, i as pairByRole, n as buildPlayerPoints, o as referenceLines, r as median, s as summarizePlayers, t as ROLE_ORDER } from "./strength-CBihFxZD.mjs";
3
+ export { ROLE_ORDER, buildPlayerPoints, groupDivisions, median, pairByRole, quadrantOf, referenceLines, summarizePlayers, summarizeRoster };
@@ -0,0 +1,175 @@
1
+ //#region src/match/types.d.ts
2
+ type MatchGroup = 'pvp' | 'ranked' | 'coop' | 'clan' | 'brawl' | 'event' | 'unknown';
3
+ type Relation = 'self' | 'ally' | 'enemy' | 'unknown';
4
+ type Outcome = 'win' | 'loss' | 'draw' | 'unknown';
5
+ /** Statistical outcome: a draw counts as a loss for both teams. */
6
+ type StatOutcome = 'win' | 'loss';
7
+ type RecordQuality = 'meta-only' | 'partial' | 'complete';
8
+ type ResultValue = string | number | boolean | null | ResultValue[] | {
9
+ [key: string]: ResultValue;
10
+ };
11
+ interface ArenaRoster {
12
+ provisionalId: string;
13
+ battleTime: number | null;
14
+ gameMode: number | null;
15
+ matchGroup: MatchGroup;
16
+ mapId: number | null;
17
+ mapName: string | null;
18
+ clientVersion: string | null;
19
+ durationSec: number | null;
20
+ playersPerTeam: number | null;
21
+ participants: RosterParticipant[];
22
+ raw: unknown;
23
+ }
24
+ interface RosterParticipant {
25
+ accountId: number | null;
26
+ name: string | null;
27
+ clanId: number | null;
28
+ clanTag: string | null;
29
+ shipId: number | null;
30
+ relation: Relation;
31
+ isBot: boolean;
32
+ }
33
+ interface MatchRecord {
34
+ arenaId: string;
35
+ provisionalIds: string[];
36
+ battleTime: number | null;
37
+ gameMode: number | null;
38
+ matchGroup: MatchGroup;
39
+ mapId: number | null;
40
+ mapName: string | null;
41
+ clientVersion: string | null;
42
+ durationSec: number | null;
43
+ winnerTeamId: number | null;
44
+ outcome: Outcome;
45
+ participants: MatchParticipant[];
46
+ quality: RecordQuality;
47
+ ingestedAt: number;
48
+ parsedAt: number;
49
+ mappingVersion: string | null;
50
+ source: MatchSource;
51
+ raw?: unknown;
52
+ }
53
+ interface MatchSource {
54
+ kind: 'replay';
55
+ fileName?: string;
56
+ size?: number;
57
+ mtimeMs?: number;
58
+ }
59
+ interface MatchParticipant {
60
+ accountId: number | null;
61
+ name: string | null;
62
+ clanId: number | null;
63
+ clanTag: string | null;
64
+ shipId: number | null;
65
+ teamId: number | null;
66
+ relation: Relation;
67
+ isBot: boolean;
68
+ divisionId: number | null;
69
+ result: PlayerResult | null;
70
+ }
71
+ interface PlayerResult {
72
+ damage: number | null;
73
+ frags: number | null;
74
+ exp: number | null;
75
+ rawExp: number | null;
76
+ isAlive: boolean | null;
77
+ lifeTimeSec: number | null;
78
+ scoutingDamage: number | null;
79
+ killerAccountId: number | null;
80
+ interactions: Record<string, Record<string, ResultValue>>;
81
+ fields: Record<string, ResultValue>;
82
+ }
83
+ interface ArenaInfoVehicle {
84
+ id?: number | null;
85
+ name?: string | null;
86
+ shipId?: number | null;
87
+ relation?: number | null;
88
+ }
89
+ /** Structural subset of `tempArenaInfo.json`; unknown keys are preserved in `raw`. */
90
+ interface ArenaInfoLike {
91
+ matchGroup?: string | null;
92
+ gameMode?: number | null;
93
+ mapId?: number | null;
94
+ mapName?: string | null;
95
+ clientVersionFromExe?: string | null;
96
+ duration?: number | null;
97
+ battleDuration?: number | null;
98
+ playersPerTeam?: number | null;
99
+ dateTime?: string | null;
100
+ vehicles?: readonly ArenaInfoVehicle[] | null;
101
+ [key: string]: unknown;
102
+ }
103
+ interface ReplayVehicleLike {
104
+ id?: number | null;
105
+ name?: string | null;
106
+ shipId?: number | null;
107
+ relation?: number | null;
108
+ }
109
+ interface ReplayMetaLike {
110
+ dateTime?: string | null;
111
+ mapId?: number | null;
112
+ mapName?: string | null;
113
+ gameMode?: number | null;
114
+ gameType?: string | null;
115
+ matchGroup?: string | null;
116
+ clientVersionFromExe?: string | null;
117
+ duration?: number | null;
118
+ battleDuration?: number | null;
119
+ playersPerTeam?: number | null;
120
+ playerID?: number | null;
121
+ playerName?: string | null;
122
+ vehicles?: readonly ReplayVehicleLike[] | null;
123
+ [key: string]: unknown;
124
+ }
125
+ interface ReplayPlayerLike {
126
+ accountId: number | null;
127
+ name: string | null;
128
+ clanId: number | null;
129
+ clanTag: string | null;
130
+ teamId: number | null;
131
+ shipId: number | null;
132
+ damage: number | null;
133
+ frags: number | null;
134
+ exp: number | null;
135
+ rawExp: number | null;
136
+ isAlive: boolean | null;
137
+ lifeTimeSec: number | null;
138
+ scoutingDamage: number | null;
139
+ killerAccountId: number | null;
140
+ interactions: Record<string, Record<string, ResultValue>>;
141
+ fields: Record<string, ResultValue>;
142
+ }
143
+ interface ReplayCommonLike {
144
+ winnerTeamId?: number | null;
145
+ durationSec?: number | null;
146
+ gameMode?: number | null;
147
+ scenarioName?: string | null;
148
+ startDt?: number | null;
149
+ }
150
+ interface ReplayResultsLike {
151
+ arenaId?: number | null;
152
+ accountId?: number | null;
153
+ mappingVersion?: string | null;
154
+ common?: ReplayCommonLike | null;
155
+ players: readonly ReplayPlayerLike[];
156
+ raw?: unknown;
157
+ }
158
+ /** Structural subset of `ParsedReplay` from `wows-replay-parser`. */
159
+ interface ReplayFacts {
160
+ meta: ReplayMetaLike;
161
+ battleResults: ReplayResultsLike | null;
162
+ }
163
+ interface ArenaKeyInput {
164
+ arenaId?: number | null;
165
+ battleTime: number | null;
166
+ mapId: number | null;
167
+ accountIds: readonly (number | null)[];
168
+ }
169
+ interface Division {
170
+ teamId: number | null;
171
+ divisionId: number;
172
+ participants: MatchParticipant[];
173
+ }
174
+ //#endregion
175
+ export { StatOutcome as S, ReplayPlayerLike as _, Division as a, ResultValue as b, MatchRecord as c, PlayerResult as d, RecordQuality as f, ReplayMetaLike as g, ReplayFacts as h, ArenaRoster as i, MatchSource as l, ReplayCommonLike as m, ArenaInfoVehicle as n, MatchGroup as o, Relation as p, ArenaKeyInput as r, MatchParticipant as s, ArenaInfoLike as t, Outcome as u, ReplayResultsLike as v, RosterParticipant as x, ReplayVehicleLike as y };
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "wows-compute",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.1.0",
4
+ "description": "Pure TypeScript computation kernels for World of Warships match records, ratings and aggregates.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=22.18.0"
9
+ },
10
+ "keywords": [
11
+ "world-of-warships",
12
+ "wows",
13
+ "personal-rating",
14
+ "statistics",
15
+ "compute"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.mts",
20
+ "import": "./dist/index.mjs"
21
+ },
22
+ "./match": {
23
+ "types": "./dist/match.d.mts",
24
+ "import": "./dist/match.mjs"
25
+ },
26
+ "./metrics": {
27
+ "types": "./dist/metrics.d.mts",
28
+ "import": "./dist/metrics.mjs"
29
+ },
30
+ "./strength": {
31
+ "types": "./dist/strength.d.mts",
32
+ "import": "./dist/strength.mjs"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "dependencies": {
40
+ "wows-number-api-sdk": "^1.0.1"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^26.6.5",
44
+ "tsdown": "^0.23.1",
45
+ "typescript": "^7.0.2",
46
+ "vitest": "^5.0.5",
47
+ "wows-replay-parser": "^1.0.1"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/urara-chiya/wows-devkit.git",
52
+ "directory": "packages/wows-compute"
53
+ },
54
+ "homepage": "https://github.com/urara-chiya/wows-devkit#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/urara-chiya/wows-devkit/issues"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "scripts": {
62
+ "build": "tsdown",
63
+ "dev": "tsdown --watch",
64
+ "test": "vitest run",
65
+ "test:watch": "vitest",
66
+ "typecheck": "tsc --noEmit"
67
+ }
6
68
  }