hoi4-agent-tools 4.4.0 → 4.6.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/hoi4_agent_tools/core/managed-outputs.js +26 -6
  3. package/dist/hoi4_agent_tools/core/source/encoding.js +56 -2
  4. package/dist/hoi4_agent_tools/core/source/parser.js +16 -1
  5. package/dist/hoi4_agent_tools/focus/drift.js +34 -0
  6. package/dist/hoi4_agent_tools/focus/service.js +9 -3
  7. package/dist/hoi4_agent_tools/focus/tool-operations.js +2 -0
  8. package/dist/hoi4_agent_tools/map/border-quality.js +25 -7
  9. package/dist/hoi4_agent_tools/map/derived.js +16 -3
  10. package/dist/hoi4_agent_tools/map/gameplay/crossing-validation.js +268 -0
  11. package/dist/hoi4_agent_tools/map/gameplay/crossings.js +961 -0
  12. package/dist/hoi4_agent_tools/map/gameplay/development.js +168 -0
  13. package/dist/hoi4_agent_tools/map/gameplay/economy.js +75 -0
  14. package/dist/hoi4_agent_tools/map/gameplay/facts.js +264 -0
  15. package/dist/hoi4_agent_tools/map/gameplay/inland-sea.js +66 -0
  16. package/dist/hoi4_agent_tools/map/gameplay/metrics.js +132 -0
  17. package/dist/hoi4_agent_tools/map/gameplay/names.js +419 -0
  18. package/dist/hoi4_agent_tools/map/gameplay/ports.js +244 -0
  19. package/dist/hoi4_agent_tools/map/gameplay/prepare.js +27 -0
  20. package/dist/hoi4_agent_tools/map/gameplay/rail-cost.js +77 -0
  21. package/dist/hoi4_agent_tools/map/gameplay/sites.js +61 -0
  22. package/dist/hoi4_agent_tools/map/geography/resolve.js +15 -4
  23. package/dist/hoi4_agent_tools/map/model.js +2 -1
  24. package/dist/hoi4_agent_tools/map/operations.js +282 -38
  25. package/dist/hoi4_agent_tools/map/reference-preparation.js +4 -1
  26. package/dist/hoi4_agent_tools/map/relief/hydrology.js +316 -192
  27. package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +43 -0
  28. package/dist/hoi4_agent_tools/map/service.js +93 -16
  29. package/dist/hoi4_agent_tools/map/state-restructure.js +378 -0
  30. package/dist/hoi4_agent_tools/map/territory-graph.js +238 -0
  31. package/dist/hoi4_agent_tools/map/territory-operations.js +944 -0
  32. package/dist/hoi4_agent_tools/map/territory-quality.js +189 -0
  33. package/dist/hoi4_agent_tools/map/territory.js +1108 -0
  34. package/dist/hoi4_agent_tools/map/tool-operations.js +119 -1
  35. package/dist/hoi4_agent_tools/map/validation.js +4 -0
  36. package/dist/hoi4_agent_tools/map/world-context.js +23 -0
  37. package/dist/hoi4_agent_tools/map/world-outputs.js +22 -10
  38. package/dist/hoi4_agent_tools/map/world-realms.js +152 -0
  39. package/dist/hoi4_agent_tools/map/world.js +400 -145
  40. package/dist/hoi4_agent_tools/schemas/geography.js +12 -1
  41. package/dist/hoi4_agent_tools/schemas/map.js +277 -34
  42. package/dist/hoi4_agent_tools/version.js +1 -1
  43. package/docs/focus.md +6 -0
  44. package/docs/map.md +132 -4
  45. package/npm-shrinkwrap.json +2 -2
  46. package/package.json +1 -1
  47. package/schemas/configuration.schema.json +1 -1
  48. package/schemas/continuous-focus-palette.schema.json +1 -1
  49. package/schemas/custom-weighted-pool.schema.json +1 -1
  50. package/schemas/decision-inspect-input.schema.json +1 -1
  51. package/schemas/error-log-input.schema.json +1 -1
  52. package/schemas/event-feature-manifest.schema.json +1 -1
  53. package/schemas/focus-plan.schema.json +1 -1
  54. package/schemas/focus-planning-sidecar.schema.json +1 -1
  55. package/schemas/geography-model.schema.json +13 -1
  56. package/schemas/gui-animation-source.schema.json +1 -1
  57. package/schemas/gui-helper.schema.json +1 -1
  58. package/schemas/gui-scenario.schema.json +1 -1
  59. package/schemas/helper-expansion-request.schema.json +1 -1
  60. package/schemas/helper-expansion-summary.schema.json +1 -1
  61. package/schemas/impact-inspect-input.schema.json +1 -1
  62. package/schemas/map-operation.schema.json +1541 -786
  63. package/schemas/mechanic-test-input.schema.json +1 -1
  64. package/schemas/mod-index-input.schema.json +1 -1
  65. package/schemas/operation-result.schema.json +1 -1
  66. package/schemas/package-check-input.schema.json +1 -1
  67. package/schemas/package-manifest.schema.json +1 -1
  68. package/schemas/probability-analysis-result.schema.json +1 -1
  69. package/schemas/probability-compare-input.schema.json +1 -1
  70. package/schemas/probability-evaluate-input.schema.json +1 -1
  71. package/schemas/probability-inspect-input.schema.json +1 -1
  72. package/schemas/probability-render-input.schema.json +1 -1
  73. package/schemas/probability-scenario-set.schema.json +1 -1
  74. package/schemas/probability-sequence-input.schema.json +1 -1
  75. package/schemas/probability-simulate-input.schema.json +1 -1
  76. package/schemas/probability-sweep-input.schema.json +1 -1
  77. package/schemas/reference-context-input.schema.json +1 -1
  78. package/schemas/reference-read-input.schema.json +1 -1
  79. package/schemas/reference-search-input.schema.json +1 -1
  80. package/schemas/reference-sheets.schema.json +1 -1
  81. package/schemas/scenario-suite.schema.json +1 -1
  82. package/schemas/scenario-test-input.schema.json +1 -1
  83. package/schemas/script-validate-input.schema.json +1 -1
  84. package/schemas/shared-scenario.schema.json +1 -1
  85. package/schemas/source-lookup-input.schema.json +1 -1
  86. package/server.json +2 -2
@@ -0,0 +1,268 @@
1
+ import { assignments, firstScalar, parseClausewitz } from '../../core/source/index.js';
2
+ import { defaultMapSelectorValue } from '../model.js';
3
+ import { closestApproach } from './facts.js';
4
+ /** Widest land-to-land crossing the validator accepts as a strait (the base game's is 30). */
5
+ export const STRAIT_VALIDATOR_LIMIT_PX = 48;
6
+ /** Total adjacency rows above which a map is reported as unusually dense with barriers. */
7
+ export const ADJACENCY_ROW_LIMIT = 2_000;
8
+ const ANALYSED_ROWS = 400;
9
+ function normalized(value) {
10
+ return value.replaceAll('\\', '/').replace(/^\.\//u, '').toLowerCase();
11
+ }
12
+ function rulesFile(index) {
13
+ let name = 'adjacency_rules.txt';
14
+ try {
15
+ name = defaultMapSelectorValue(index.defaultMapFile, 'adjacency_rules', name);
16
+ }
17
+ catch {
18
+ // An untrustworthy selector is reported elsewhere; the default name is still worth checking.
19
+ }
20
+ for (const root of index.sourceRoots.map) {
21
+ const found = index.activeFiles.byRelativePath.get(normalized(`${root}/${name}`));
22
+ if (found !== undefined)
23
+ return found;
24
+ }
25
+ return undefined;
26
+ }
27
+ /** The rules a file defines; `duplicates` lists names defined more than once. */
28
+ export function parseAdjacencyRules(file) {
29
+ const document = parseClausewitz(file.bytes, file.displayPath);
30
+ const rules = [];
31
+ const seen = new Set();
32
+ const duplicates = [];
33
+ for (const entry of assignments(document.root, 'adjacency_rule')) {
34
+ if (entry.value.type !== 'block')
35
+ continue;
36
+ const name = firstScalar(entry.value, 'name')?.value ?? '';
37
+ const required = [];
38
+ const list = assignments(entry.value, 'required_provinces')[0]?.value;
39
+ if (list?.type === 'block')
40
+ for (const item of list.entries)
41
+ if (item.type === 'scalar' && /^\d+$/u.test(item.value))
42
+ required.push(Number(item.value));
43
+ const icon = firstScalar(entry.value, 'icon')?.value;
44
+ rules.push({
45
+ name,
46
+ required,
47
+ ...(icon !== undefined && /^\d+$/u.test(icon) ? { icon: Number(icon) } : {}),
48
+ });
49
+ if (name !== '') {
50
+ if (seen.has(name))
51
+ duplicates.push(name);
52
+ seen.add(name);
53
+ }
54
+ }
55
+ return { rules, duplicates };
56
+ }
57
+ function bounded(items, limit = 8) {
58
+ return items.slice(0, limit);
59
+ }
60
+ function* checkRules(index, report) {
61
+ const file = rulesFile(index);
62
+ if (file === undefined)
63
+ return;
64
+ const { rules, duplicates } = parseAdjacencyRules(file);
65
+ yield;
66
+ const location = {
67
+ path: file.displayPath,
68
+ start: { line: 1, column: 1, offset: 0 },
69
+ end: { line: 1, column: 1, offset: 0 },
70
+ };
71
+ for (const name of bounded(duplicates))
72
+ report({
73
+ code: 'MAP_ADJACENCY_RULE_DUPLICATE',
74
+ severity: 'error',
75
+ message: `Adjacency rule ${name} is defined more than once`,
76
+ location,
77
+ details: { rule: name },
78
+ });
79
+ const defined = new Map(rules.map((rule) => [rule.name, rule]));
80
+ for (const rule of rules) {
81
+ const problems = [];
82
+ if (rule.name === '')
83
+ problems.push('has no name');
84
+ if (rule.required.length < 2)
85
+ problems.push('lists fewer than two required_provinces');
86
+ const unknown = rule.required.filter((id) => !index.definitionsById.has(id));
87
+ if (unknown.length > 0)
88
+ problems.push(`names unknown province(s) ${bounded(unknown).join(', ')}`);
89
+ if (rule.icon !== undefined && !index.definitionsById.has(rule.icon))
90
+ problems.push(`has an icon on unknown province ${rule.icon}`);
91
+ if (problems.length > 0)
92
+ report({
93
+ code: 'MAP_ADJACENCY_RULE_INVALID',
94
+ severity: 'error',
95
+ message: `Adjacency rule ${rule.name === '' ? '(unnamed)' : rule.name} ${problems.join(' and ')}`,
96
+ location,
97
+ details: { rule: rule.name, problems },
98
+ });
99
+ }
100
+ yield;
101
+ const used = new Set();
102
+ for (const row of index.adjacencies) {
103
+ if (row.rule === '')
104
+ continue;
105
+ used.add(row.rule);
106
+ if (!defined.has(row.rule))
107
+ report({
108
+ code: 'MAP_ADJACENCY_RULE_MISSING',
109
+ severity: 'error',
110
+ message: `Adjacency ${row.from}-${row.to} uses rule ${row.rule}, which adjacency_rules.txt does not define`,
111
+ location,
112
+ details: { rule: row.rule, from: row.from, to: row.to },
113
+ });
114
+ }
115
+ const unused = rules.filter((rule) => rule.name !== '' && !used.has(rule.name));
116
+ if (unused.length > 0)
117
+ report({
118
+ code: 'MAP_ADJACENCY_RULE_UNUSED',
119
+ severity: 'info',
120
+ message: `${unused.length} adjacency rule(s) are defined but no adjacency row uses them`,
121
+ location,
122
+ details: { rules: bounded(unused.map((rule) => rule.name)) },
123
+ });
124
+ }
125
+ /** Pixels of one province, from its bounding box. */
126
+ function pixelsOf(index, id) {
127
+ const raster = index.raster;
128
+ const geometry = raster?.geometry.get(id);
129
+ if (raster === undefined || geometry === undefined)
130
+ return [];
131
+ const out = [];
132
+ const stride = Math.max(1, Math.ceil(Math.sqrt(geometry.pixelCount / 4_000)));
133
+ for (let y = geometry.minY; y <= geometry.maxY; y += 1)
134
+ for (let x = geometry.minX; x <= geometry.maxX; x += 1)
135
+ if ((x + y) % stride === 0 && raster.provinceIds[y * raster.width + x] === id)
136
+ out.push(y * raster.width + x);
137
+ return out;
138
+ }
139
+ function* checkRows(index, report) {
140
+ const raster = index.raster;
141
+ if (raster === undefined)
142
+ return;
143
+ const impassable = index.adjacencies.filter(({ type }) => type === 'impassable');
144
+ const isolated = [];
145
+ const nonLand = [];
146
+ for (const row of impassable) {
147
+ const from = index.definitionsById.get(row.from);
148
+ const to = index.definitionsById.get(row.to);
149
+ if (from === undefined || to === undefined)
150
+ continue;
151
+ if (!(raster.adjacency.get(row.from)?.has(row.to) ?? false))
152
+ isolated.push(`${row.from}-${row.to}`);
153
+ if (from.type !== 'land' || to.type !== 'land')
154
+ nonLand.push(`${row.from}-${row.to}`);
155
+ }
156
+ yield;
157
+ // The base game has one such row among 93, so a stray row is information and a pattern a warning.
158
+ if (isolated.length > 0)
159
+ report({
160
+ code: 'MAP_IMPASSABLE_NOT_ADJACENT',
161
+ severity: isolated.length >= 3 && isolated.length > impassable.length * 0.05 ? 'warning' : 'info',
162
+ message: `${isolated.length} impassable row(s) join provinces that do not touch, so they block nothing`,
163
+ details: { rows: bounded(isolated), count: isolated.length },
164
+ });
165
+ if (nonLand.length > 0)
166
+ report({
167
+ code: 'MAP_IMPASSABLE_NOT_LAND',
168
+ severity: 'warning',
169
+ message: `${nonLand.length} impassable row(s) join a province that is not land; walls separate land provinces`,
170
+ details: { rows: bounded(nonLand), count: nonLand.length },
171
+ });
172
+ if (index.adjacencies.length > ADJACENCY_ROW_LIMIT)
173
+ report({
174
+ code: 'MAP_ADJACENCY_ROW_LIMIT',
175
+ severity: 'warning',
176
+ message: `adjacencies.csv has ${index.adjacencies.length} rows; the base game has 244. Large walls slow the load and the AI; consider fewer or larger provinces along the wall`,
177
+ details: { rows: index.adjacencies.length, limit: ADJACENCY_ROW_LIMIT },
178
+ });
179
+ // Land-to-land sea crossings narrow enough to be a strait.
180
+ const wide = [];
181
+ let analysed = 0;
182
+ const view = {
183
+ width: raster.width,
184
+ height: raster.height,
185
+ pixel: raster.provinceIds,
186
+ count: 0,
187
+ };
188
+ for (const row of index.adjacencies) {
189
+ if (row.type !== 'sea' || analysed >= ANALYSED_ROWS)
190
+ continue;
191
+ const from = index.definitionsById.get(row.from);
192
+ const to = index.definitionsById.get(row.to);
193
+ if (from?.type !== 'land' || to?.type !== 'land')
194
+ continue;
195
+ if (raster.adjacency.get(row.from)?.has(row.to) ?? false)
196
+ continue;
197
+ analysed += 1;
198
+ const approach = closestApproach(pixelsOf(index, row.from), pixelsOf(index, row.to), view.width, 200);
199
+ if (approach !== undefined && approach.gap > STRAIT_VALIDATOR_LIMIT_PX)
200
+ wide.push(`${row.from}-${row.to} (${Math.round(approach.gap)} px)`);
201
+ if (analysed % 16 === 0)
202
+ yield;
203
+ }
204
+ if (wide.length > 0)
205
+ report({
206
+ code: 'MAP_STRAIT_NOT_NARROW',
207
+ severity: 'warning',
208
+ message: `${wide.length} land crossing(s) span more than ${STRAIT_VALIDATOR_LIMIT_PX} pixels between their provinces (the base game's widest is 30): ${bounded(wide, 4).join(', ')}`,
209
+ details: { crossings: bounded(wide), count: wide.length, limitPx: STRAIT_VALIDATOR_LIMIT_PX },
210
+ });
211
+ }
212
+ function* checkRailways(index, report) {
213
+ if (index.railways.length === 0)
214
+ return;
215
+ const blocked = new Set();
216
+ for (const row of index.adjacencies)
217
+ if (row.type === 'impassable') {
218
+ blocked.add(`${row.from}:${row.to}`);
219
+ blocked.add(`${row.to}:${row.from}`);
220
+ }
221
+ if (blocked.size === 0)
222
+ return;
223
+ const crossing = [];
224
+ for (const railway of index.railways) {
225
+ for (let step = 1; step < railway.provinces.length; step += 1) {
226
+ const from = railway.provinces[step - 1];
227
+ const to = railway.provinces[step];
228
+ if (blocked.has(`${from}:${to}`))
229
+ crossing.push(`${from}-${to}`);
230
+ }
231
+ }
232
+ yield;
233
+ if (crossing.length > 0)
234
+ report({
235
+ code: 'MAP_RAILWAY_CROSSES_IMPASSABLE',
236
+ severity: 'warning',
237
+ message: `${crossing.length} railway step(s) run between provinces separated by an impassable row; a railway cannot cross a wall except at a gate`,
238
+ details: { steps: bounded(crossing), count: crossing.length },
239
+ });
240
+ }
241
+ function* checkUnitPositions(index, report) {
242
+ if (index.unitPositions.length === 0 || index.raster === undefined)
243
+ return;
244
+ const covered = new Set(index.unitPositions.map(({ provinceId }) => provinceId));
245
+ const missing = [];
246
+ for (const definition of index.definitions)
247
+ if (definition.id > 0 &&
248
+ index.raster.geometry.has(definition.id) &&
249
+ definition.type !== 'lake' &&
250
+ !covered.has(definition.id))
251
+ missing.push(definition.id);
252
+ yield;
253
+ // The base game lacks positions for 7 of 13,000 provinces: a few are information, many a warning.
254
+ if (missing.length > 0)
255
+ report({
256
+ code: 'MAP_UNIT_POSITION_MISSING',
257
+ severity: missing.length > Math.max(3, index.definitions.length * 0.01) ? 'warning' : 'info',
258
+ message: `${missing.length} province(s) have no unit-stack positions, so units there have no place to stand`,
259
+ details: { provinces: bounded(missing, 12), count: missing.length },
260
+ });
261
+ }
262
+ /** Run the crossing, wall, railway and position checks; findings go to `report`. */
263
+ export function* validateCrossings(index, report) {
264
+ yield* checkRules(index, report);
265
+ yield* checkRows(index, report);
266
+ yield* checkRailways(index, report);
267
+ yield* checkUnitPositions(index, report);
268
+ }