Remove stepCount/stepSize; patterns own their geometry
The stepCount and stepSize knobs were two controls for one quantity users actually care about (reach), and the number of steps is an implementation detail nobody meaningfully tunes. Each pattern has a natural size and resolution — a jitter is inherently small, an arc a broad curve — so those now live as constants in each strategy rather than as global config. - strategies.ts: each pattern defines its own step count and size; MoveContext drops `config` down to pure geometry (start/width/height/rng), and the module no longer imports Config at all (dissolving the type-only-import cycle workaround). line stays byte-for-byte: 250 one-pixel steps. - executor.ts: executePath takes `config` for pacing (stepDelay); the path itself needs nothing from it. - config.ts / cli.ts / move.ts / config.default.json: drop stepCount and stepSize from the type, seed, validation, resolver, CLI flags (-n, -s), and help. stepDelay stays as the one pacing lever. - configFile.ts: tolerate the removed keys instead of rejecting them — every pre-1.3.0 install seeded stepCount, so a hard "unknown key" failure on upgrade is avoided. They're ignored with a one-line stderr notice; genuine unknown keys still error. The -n/--step-count CLI flag (shipped since 1.0.0) is now an unknown option; config files degrade gracefully, command lines don't. Stays in the unpushed 1.3.0 release. 64 tests pass.
This commit is contained in:
+32
-40
@@ -9,8 +9,6 @@
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { DEFAULT_CONFIG } from "../src/config.ts";
|
||||
import type { Config } from "../src/config.ts";
|
||||
import type { Point } from "../src/device.ts";
|
||||
import {
|
||||
arc,
|
||||
@@ -42,56 +40,50 @@ function ctxOf(overrides: {
|
||||
start?: Point;
|
||||
width?: number;
|
||||
height?: number;
|
||||
config?: Partial<Config>;
|
||||
rng?: () => number;
|
||||
}): MoveContext {
|
||||
return {
|
||||
start: overrides.start ?? { x: 500, y: 500 },
|
||||
width: overrides.width ?? 1920,
|
||||
height: overrides.height ?? 1080,
|
||||
config: { ...DEFAULT_CONFIG, ...overrides.config },
|
||||
rng: overrides.rng ?? Math.random,
|
||||
};
|
||||
}
|
||||
|
||||
describe("line", () => {
|
||||
test("emits stepCount points along +x with no vertical movement", () => {
|
||||
const pts = [...line.path(ctxOf({ config: { stepCount: 5, stepSize: 1 } }))];
|
||||
expect(pts.length).toBe(5);
|
||||
test("emits its full 250-step, 250px sweep along +x with no vertical drift (preserved default)", () => {
|
||||
const pts = [...line.path(ctxOf({ start: { x: 500, y: 500 } }))];
|
||||
expect(pts.length).toBe(250);
|
||||
expect(pts.every((p) => p.y === 500)).toBe(true);
|
||||
expect(pts.map((p) => p.x)).toEqual([501, 502, 503, 504, 505]);
|
||||
});
|
||||
|
||||
test("honors stepSize for per-step distance", () => {
|
||||
const pts = [...line.path(ctxOf({ config: { stepCount: 3, stepSize: 10 } }))];
|
||||
expect(pts.map((p) => p.x)).toEqual([510, 520, 530]);
|
||||
// 1px per step: 501..750.
|
||||
expect(pts[0]!.x).toBe(501);
|
||||
expect(pts.at(-1)!.x).toBe(750);
|
||||
});
|
||||
|
||||
test("reverses direction when there is no room to the right", () => {
|
||||
const pts = [...line.path(ctxOf({ start: { x: 90, y: 10 }, width: 100, config: { stepCount: 20, stepSize: 1 } }))];
|
||||
const pts = [...line.path(ctxOf({ start: { x: 90, y: 10 }, width: 100 }))];
|
||||
expect(pts[0]!.x).toBe(89);
|
||||
expect(pts.at(-1)!.x).toBe(70);
|
||||
// Heads left: each step decreases x by 1.
|
||||
expect(pts[1]!.x).toBe(88);
|
||||
expect(pts.at(-1)!.x).toBe(90 - 250);
|
||||
});
|
||||
});
|
||||
|
||||
describe("diagonal", () => {
|
||||
test("moves on both axes toward the roomy corner", () => {
|
||||
const pts = [...diagonal.path(ctxOf({ config: { stepCount: 4, stepSize: 2 } }))];
|
||||
expect(pts.length).toBe(4);
|
||||
expect(pts.map((p) => p.x)).toEqual([502, 504, 506, 508]);
|
||||
expect(pts.map((p) => p.y)).toEqual([502, 504, 506, 508]);
|
||||
test("moves 1px on both axes toward the roomy corner for 250 steps", () => {
|
||||
const pts = [...diagonal.path(ctxOf({ start: { x: 500, y: 500 } }))];
|
||||
expect(pts.length).toBe(250);
|
||||
expect(pts[0]!).toEqual({ x: 501, y: 501 });
|
||||
expect(pts.at(-1)!).toEqual({ x: 750, y: 750 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("jitter", () => {
|
||||
test("stays within its radius of start and returns stepCount points", () => {
|
||||
const size = 5;
|
||||
const stepCount = 50;
|
||||
// Radius scales off the sweep length (stepCount * stepSize) / 8, floored at 4.
|
||||
const radius = Math.max(4, (stepCount * size) / 8);
|
||||
test("stays within its fixed radius of start across its fixed step count", () => {
|
||||
const radius = 30; // JITTER_RADIUS
|
||||
const start = { x: 500, y: 500 };
|
||||
const pts = [...jitter.path(ctxOf({ start, config: { stepCount, stepSize: size }, rng: mulberry32(1) }))];
|
||||
expect(pts.length).toBe(stepCount);
|
||||
const pts = [...jitter.path(ctxOf({ start, rng: mulberry32(1) }))];
|
||||
expect(pts.length).toBe(80); // JITTER_STEPS
|
||||
for (const p of pts) {
|
||||
expect(Math.hypot(p.x - start.x, p.y - start.y)).toBeLessThanOrEqual(radius + 1e-9);
|
||||
}
|
||||
@@ -101,35 +93,35 @@ describe("jitter", () => {
|
||||
describe("walk", () => {
|
||||
test("is a cumulative walk; a 0.5-constant rng yields zero net drift", () => {
|
||||
const start = { x: 400, y: 300 };
|
||||
const pts = [...walk.path(ctxOf({ start, config: { stepCount: 10, stepSize: 7 }, rng: () => 0.5 }))];
|
||||
expect(pts.length).toBe(10);
|
||||
const pts = [...walk.path(ctxOf({ start, rng: () => 0.5 }))];
|
||||
expect(pts.length).toBe(200); // WALK_STEPS
|
||||
// (0.5*2 - 1) === 0, so every step delta is zero.
|
||||
expect(pts.every((p) => p.x === start.x && p.y === start.y)).toBe(true);
|
||||
});
|
||||
|
||||
test("accumulates deltas step over step", () => {
|
||||
const pts = [...walk.path(ctxOf({ config: { stepCount: 3, stepSize: 4 }, rng: mulberry32(42) }))];
|
||||
expect(pts.length).toBe(3);
|
||||
test("accumulates finite deltas step over step", () => {
|
||||
const pts = [...walk.path(ctxOf({ rng: mulberry32(42) }))];
|
||||
expect(pts.length).toBe(200);
|
||||
expect(pts.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arc", () => {
|
||||
test("emits stepCount finite points and lands on its endpoint", () => {
|
||||
const pts = [...arc.path(ctxOf({ config: { stepCount: 8, stepSize: 20 }, rng: mulberry32(7) }))];
|
||||
expect(pts.length).toBe(8);
|
||||
test("emits its fixed step count of finite points, deterministic under a fixed seed", () => {
|
||||
const pts = [...arc.path(ctxOf({ rng: mulberry32(7) }))];
|
||||
expect(pts.length).toBe(120); // ARC_STEPS
|
||||
expect(pts.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))).toBe(true);
|
||||
// t = 1 at the final step, so B(1) is the endpoint — a stable point.
|
||||
const a = [...arc.path(ctxOf({ config: { stepCount: 8, stepSize: 20 }, rng: mulberry32(7) }))];
|
||||
expect(pts.at(-1)).toEqual(a.at(-1)!);
|
||||
// Same seed -> same endpoint (t = 1 at the final step is a stable point).
|
||||
const again = [...arc.path(ctxOf({ rng: mulberry32(7) }))];
|
||||
expect(pts.at(-1)).toEqual(again.at(-1)!);
|
||||
});
|
||||
});
|
||||
|
||||
describe("figureEight", () => {
|
||||
test("returns to the start point after one full period", () => {
|
||||
const start = { x: 600, y: 400 };
|
||||
const pts = [...figureEight.path(ctxOf({ start, config: { stepCount: 40, stepSize: 10 } }))];
|
||||
expect(pts.length).toBe(40);
|
||||
const pts = [...figureEight.path(ctxOf({ start }))];
|
||||
expect(pts.length).toBe(90); // FIG8_STEPS
|
||||
expect(pts.at(-1)!.x).toBeCloseTo(start.x, 6);
|
||||
expect(pts.at(-1)!.y).toBeCloseTo(start.y, 6);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user