Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Options:
## The game's own pathing

Since 1.3.0. Only in the editor at scmjs.dev: it needs the
[scm-sim](https://github.com/scm-js/plugin-scm-sim) plugin, version 0.7.0 or later, and
[scm-sim](https://github.com/scm-js/plugin-scm-sim) plugin, version 0.7.0 or later (0.8.0 for the measures), and
the game engine served beside the editor. A copy of the game from StarEdit's download is
enough. Elsewhere, for example in the desktop app, the section does not appear.

Expand Down Expand Up @@ -124,14 +124,40 @@ the ticked units between them.
The details panel lists every walk with its time at Fastest. *Copy report* includes them.
An edit to the map marks the walks out of date; walk the map again to bring them up to date.

### Chokes and mining

Since 1.4.0, with scm-sim 0.8.0 or later, the same section can have the game measure two
more things. Both are repeatable: measuring the same map twice gives the same numbers.

**Chokes ▸ Measure** puts a group of twelve units (Dragoons to begin with; the list beside
the button chooses) through every choke the panel lists, those up to *Chokes up to* wide.
For each choke the group stands about eight tiles before it, in one of the two areas it
joins. One of them walks to about eight tiles past it alone, then all twelve walk there
together, and each is timed as it gets through the choke. A choke is judged by the time
for ten of the twelve against the one alone: that is the time the group spends queueing.
The few that go round or wait come through much later, and are counted as stragglers. On
Lost Civilization, twelve Dragoons queue 5 to 17 seconds at the 2.5-tile passages and 2 to
4 seconds at chokes 4 to 8 tiles wide. The list is sorted by queueing time, and the map
shows it beside each choke's width, in red past three seconds.

**Mining ▸ Measure** mines at every start location. Each gets a Nexus on its spot and
Probes sent to its mineral fields, nearest first, as players split them: first 8, then
16, then 24. After 25 seconds to settle, two game minutes are counted, and the result is
minerals per minute at each count. *Add a base…* asks for one click on the middle of where
a town hall would stand and measures that base too. The panel warns when one start
location mines noticeably less than another (five per cent or more at some worker count):
on Lost Civilization, Player 4's start mines 10 % less than Player 1's with 24 workers.
Gas is not measured, since the game cannot be made to build an Assimilator for the
purpose.

## Layout

| | |
| --- | --- |
| `plugin.json` | the manifest the editor reads (name, version, `entry`, `icon`, the API version it needs) |
| `plugin.ts` | `activate(api)`: building the grid from the open map, the overlay bitmaps, the overlay (`api.ui.overlay`) and the two panels |
| `game.ts` | the game's own pathing: the walks matched against the estimate, the findings, the wording (pure, tested) |
| `vendor/scm-sim.d.ts` | a copy of the scm-sim plugin's service types (`contract.d.ts` at v0.7.0) |
| `game.ts` | the game's own pathing and measures: the walks matched against the estimate, the findings, a choke's two ends, a group's queueing and stragglers, the mining gap between starts (pure, tested) |
| `vendor/scm-sim.d.ts` | a copy of the scm-sim plugin's service types (`contract.d.ts` at v0.8.0) |
| `analysis.ts` | the pure part: the minitile grid, clearance (an exact Euclidean distance transform), islands, the watershed into areas and chokes, seams, the start-to-start routes, the text report |
| `i18n.ts`, `ko.ts` | the plugin's words in the editor's language: `t()` / `msg()` and the Korean catalogue |
| `dist/plugin.js` | the bundle the editor loads; `npm run build` writes it, CI commits it |
Expand Down
269 changes: 260 additions & 9 deletions dist/plugin.js

Large diffs are not rendered by default.

76 changes: 74 additions & 2 deletions game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* own estimate of each route and turned into findings for the panels and the report. Pure,
* with its own tests; the editor side is in `plugin.ts`.
*/
import type { PathingResult, PathingWalk } from "./vendor/scm-sim";
import type { Analysis } from "./analysis";
import type { MeasureBaseResult, MeasureGroupResult, PathingResult, PathingWalk } from "./vendor/scm-sim";
import { CELL_PX, type Analysis, type Choke } from "./analysis";
import { t } from "./i18n";

/** The units the game can walk here (units.dat ids): Probe, Zergling, Dragoon, Siege Tank, Ultralisk. Their names come from the editor. */
Expand Down Expand Up @@ -116,3 +116,75 @@ export function walkedFrom(result: PathingResult, bothWays: boolean): number[] {
const slots = result.starts.map((s) => s.owner);
return bothWays ? slots : slots.slice(0, -1);
}

/* ── Measures: groups through chokes, and each base's mining ── */

/**
* Two points a group walks between to cross a choke: `reach` cells out on each side, along a straight line through
* its middle that stays on open ground, ending in the two areas the choke joins (from area `a` to area `b`). Of the
* lines that do, the one ending furthest from walls. Shorter reaches are tried when none does; null when nothing fits.
* Map pixels.
*/
export function chokeEnds(a: Analysis, c: Choke, reach = 32): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
const px = (p: { x: number; y: number }) => ({ x: p.x * CELL_PX + CELL_PX / 2, y: p.y * CELL_PX + CELL_PX / 2 });
for (const r of [reach, reach * 0.75, reach * 0.5, reach * 0.35].map(Math.round)) {
let best: { from: { x: number; y: number }; to: { x: number; y: number } } | null = null, score = -1;
for (let k = 0; k < 32; k++) {
const dx = Math.cos((k * Math.PI) / 16), dy = Math.sin((k * Math.PI) / 16);
const end = (sign: number) => {
for (let step = 1; step <= r; step++) {
const x = Math.round(c.x + dx * step * sign), y = Math.round(c.y + dy * step * sign);
if (x < 0 || y < 0 || x >= a.w || y >= a.h || !a.open[y * a.w + x]) return null;
}
return { x: Math.round(c.x + dx * r * sign), y: Math.round(c.y + dy * r * sign) };
};
const p = end(1), q = end(-1);
if (!p || !q) continue;
const ap = a.area[p.y * a.w + p.x], aq = a.area[q.y * a.w + q.x];
const pair = ap === c.a && aq === c.b ? { from: p, to: q } : ap === c.b && aq === c.a ? { from: q, to: p } : null;
if (!pair) continue;
const room = a.alt[p.y * a.w + p.x] + a.alt[q.y * a.w + q.x];
if (room > score) { score = room; best = { from: px(pair.from), to: px(pair.to) }; }
}
if (best) return best;
}
return null;
}

/** The share of a group whose time stands for it: a few stragglers go round or wait, and would decide the last. */
export const MOST = 10 / 12;

/**
* How a group got through a choke: the frames for most of it (`MOST`, ten of twelve) against one alone, the time it
* spent queueing (the difference), and the stragglers (those through after twice that, or not at all). Null when one
* alone or most of the group did not get through.
*/
export function groupThrough(g: MeasureGroupResult): { most: number; queue: number; stragglers: number; count: number } | null {
const count = g.made, need = Math.max(1, Math.ceil(count * MOST));
if (g.alone === null || g.times.length < need) return null;
const most = g.times[need - 1];
return { most, queue: Math.max(0, most - g.alone), stragglers: count - g.times.filter((f) => f <= 2 * most).length, count: need };
}

/** The frames a group spent queueing (`groupThrough`); null when it did not get through. */
export function queueFrames(g: MeasureGroupResult): number | null {
return groupThrough(g)?.queue ?? null;
}

/**
* The widest gap in mining between the bases of two start locations (their `id` a player slot), at the worker count
* where it is widest, when it is at least `least` (a share: 0.05 is five per cent). Null when the starts mine alike.
*/
export function miningGap(bases: readonly MeasureBaseResult[], least = 0.05): { slow: number; fast: number; workers: number; share: number } | null {
const starts = bases.filter((b) => b.id !== undefined && b.rates.length);
let best: { slow: number; fast: number; workers: number; share: number } | null = null;
const counts = [...new Set(starts.flatMap((b) => b.rates.map((r) => r.workers)))];
for (const workers of counts) {
const at = starts.map((b) => ({ id: b.id!, rate: b.rates.find((r) => r.workers === workers)?.perMinute ?? 0 })).filter((x) => x.rate > 0);
if (at.length < 2) continue;
const fast = at.reduce((x, y) => (y.rate > x.rate ? y : x)), slow = at.reduce((x, y) => (y.rate < x.rate ? y : x));
const share = (fast.rate - slow.rate) / fast.rate;
if (share >= least && (!best || share > best.share)) best = { slow: slow.id, fast: fast.id, workers, share };
}
return best;
}
27 changes: 27 additions & 0 deletions ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,31 @@ export const KO: Record<string, string> = {
"The game could not place a {unit} at the point picked": "게임이 고른 지점에 {unit|을} 놓지 못했습니다",
"The game could not place a {unit} near {from}'s start location, so it walked nothing from there": "게임이 {from}의 시작 위치 근처에 {unit|을} 놓지 못해 그곳에서는 아무것도 걷지 않았습니다",
"not placed": "놓지 못함",
"Walkability: the measuring was stopped": "이동 가능 지형: 측정을 멈췄습니다",
"Walkability: no choke to measure": "이동 가능 지형: 측정할 길목이 없습니다",
"Walkability: {n, plural, one {# choke} other {# chokes}} measured, the slowest queueing {s} s": "이동 가능 지형: 길목 {n}개를 측정했습니다. 가장 오래 밀린 곳은 {s}초",
"Walkability: {n, plural, one {# base} other {# bases}} measured": "이동 가능 지형: 기지 {n}개를 측정했습니다",
"Click the middle of where the town hall would stand": "본진 건물이 설 자리의 가운데를 클릭하세요",
"{player}'s start location": "{player}의 시작 위치",
"The base at {x}, {y}": "{x}, {y}의 기지",
"no town hall fits here": "본진 건물을 놓을 수 없음",
"no mineral fields nearby": "가까운 미네랄 필드 없음",
"{slow}'s start location mines {pct} % less than {fast}'s with {n} workers": "일꾼 {n}기일 때 {slow}의 시작 위치가 {fast}의 시작 위치보다 {pct}% 덜 캡니다",
"did not get through": "지나가지 못함",
"Mining per game minute (8, 16 and 24 workers)": "게임 시간 1분당 채취량 (일꾼 8, 16, 24기)",
"/min": "/분",
"Measure": "측정",
"Put a group of twelve through every choke listed, and one alone, and time them": "목록의 모든 길목에 12기 무리와 1기를 각각 지나가게 하고 시간을 잽니다",
"12 ×": "12 ×",
"Mining": "채취",
"Mine at every start location with 8, 16 and 24 workers, and count the minerals per minute": "모든 시작 위치에서 일꾼 8, 16, 24기로 채취하고 분당 미네랄을 셉니다",
"Add a base…": "기지 추가…",
"Click where a town hall would stand, and measure the mining there too": "본진 건물이 설 자리를 클릭하면 그곳의 채취량도 측정합니다",
"Measuring {done} of {total}…": "{total}개 중 {done}개 측정 중…",
"+{q} s ({alone} s alone)": "+{q}초 (혼자 {alone}초)",
"Chokes, twelve {unit} at once": "길목, {unit} 12기 동시 통과",
"Minerals per minute with 8, 16 and 24 workers": "일꾼 8, 16, 24기일 때 분당 미네랄",
", {n, plural, one {# straggler} other {# stragglers}}": ", 뒤처진 유닛 {n}기",
"{alone} s alone, {most} s for {n} of {count}: {q} s queueing": "혼자 {alone}초, {count}기 중 {n}기 {most}초: {q}초 밀림",
"Chokes, a group of twelve {unit} against one alone, timed till ten of them are through": "길목, {unit} 12기 무리와 1기 비교 (10기가 지날 때까지)",
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scmjs-plugin-walkability",
"version": "1.3.0",
"version": "1.4.0",
"private": true,
"description": "Walkability — a plugin for the scmJS StarCraft map editor.",
"author": "Jeany",
Expand Down
4 changes: 2 additions & 2 deletions plugin.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "Walkability",
"id": "walkability",
"version": "1.3.0",
"description": "Reads the ground the way a unit does: islands, unreachable pockets, the areas a map divides into and the chokes between them with their widths, cliff seams with no ramp, and the ground distance between every pair of start locations, drawn over the map. At scmjs.dev it can also have the game itself walk the map, with the game's own regions and pathfinding.",
"version": "1.4.0",
"description": "Reads the ground the way a unit does: islands, unreachable pockets, the areas a map divides into and the chokes between them with their widths, cliff seams with no ramp, and the ground distance between every pair of start locations, drawn over the map. At scmjs.dev it can also have the game itself walk the map, with the game's own regions and pathfinding, time groups through the chokes, and measure each base's mining.",
"author": "Jeany",
"homepage": "https://github.com/scm-js/plugin-walkability",
"entry": "plugin.ts",
Expand Down
Loading
Loading