Collapse bounds policies to reflect-only; drop abort and clamp
The executor kept every commanded point on-screen via a per-strategy
BoundsPolicy of abort / clamp / reflect. Measured against the real
strategies, the other two earned nothing: abort truncated a sweep at the
first edge (line on a narrow screen ran only 90 of 250 steps), and clamp
could park the cursor against an edge (a monotonic ramp stalled 162 steps
in a row) -- both counter to the program's whole purpose of keeping the
cursor moving. reflect bounces off the edge and keeps going, and is
already what line/diagonal need in loop mode. arc's declared clamp was
provably dead code (it clamps its own endpoint, so no sample ever leaves
the screen).
Collapse to reflect-only:
- strategies.ts: remove the BoundsPolicy type and the `bounds` field from
the interface and all six strategies. Keep the local clamp() helper --
it's arc's endpoint geometry, not an on-screen policy; docstring says so.
- executor.ts: resolveTarget loses its policy parameter and its null
return and just reflects both axes; delete clampInt; SweepOutcome drops
"aborted"; ExecuteOptions drops `bounds`; remove the Out of bounds log.
- keeper.ts: loopOpts is now { restore: false, loop: true } -- the
reflect override added with loop mode is redundant.
- tests: drop the abort-outcome, clamp, and bounds-override tests; simplify
fixed() to take no policy; add a regression test that a monotonic ramp
past an edge never yields two identical points in a row (the guarantee
that motivated removing clamp).
Behavior is unchanged for every pattern at normal cursor positions
(verified: line's normal sweep is byte-identical). The only differences
are at a screen edge, where motion now bounces instead of stopping. No
config keys, flags, or pattern names changed.
Docs updated to match, including in-code comments, the README strategies
table (Bounds column removed) and verbose description, the sequence
diagram (resolveTarget signature + getPosition/width ordering + a loop-mode
note), and a CHANGELOG Changed entry.
This commit is contained in:
+31
-70
@@ -7,13 +7,13 @@
|
||||
* *everything else* about carrying a sweep out against a `Device`:
|
||||
*
|
||||
* - round each ideal target to whole pixels,
|
||||
* - keep it on-screen per the strategy's `BoundsPolicy`,
|
||||
* - keep it on-screen by reflecting coordinates that fall past an edge,
|
||||
* - command the cursor and pace it with `stepDelay`,
|
||||
* - detect real-user interruption after each step,
|
||||
* - restore the cursor to the origin on a clean run.
|
||||
*
|
||||
* Writing this once means new patterns inherit correct real-user-wins,
|
||||
* bounds, and restore semantics for free. It's pure with respect to I/O —
|
||||
* on-screen, and restore semantics for free. It's pure with respect to I/O —
|
||||
* all side effects go through the injected `Device`, so it's unit-testable
|
||||
* with a fake.
|
||||
*
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
import type { Config } from "./config.ts";
|
||||
import type { Device, Point } from "./device.ts";
|
||||
import type { BoundsPolicy, MoveContext, MovementStrategy } from "./strategies.ts";
|
||||
import type { MoveContext, MovementStrategy } from "./strategies.ts";
|
||||
|
||||
/**
|
||||
* Minimal log surface used by the executor and the keeper loop.
|
||||
@@ -41,11 +41,10 @@ export interface Logger {
|
||||
/**
|
||||
* How a sweep ended:
|
||||
* - `completed` — full path ran and the cursor was restored to start.
|
||||
* - `interrupted` — real user activity detected mid-sweep; aborted without
|
||||
* snapping back.
|
||||
* - `aborted` — an `abort`-policy target went out of bounds.
|
||||
* - `interrupted` — real user activity detected mid-sweep; the sweep stopped
|
||||
* without snapping back.
|
||||
*/
|
||||
export type SweepOutcome = "completed" | "interrupted" | "aborted";
|
||||
export type SweepOutcome = "completed" | "interrupted";
|
||||
|
||||
/**
|
||||
* Per-call knobs for `executePath`. All optional; the defaults reproduce the
|
||||
@@ -56,11 +55,6 @@ export type SweepOutcome = "completed" | "interrupted" | "aborted";
|
||||
* Default `true`. Loop (`--loop`) mode passes `false`:
|
||||
* chained cycles must not snap back between iterations, and an
|
||||
* infinite `loopPath` never reaches the restore anyway.
|
||||
* - `bounds` — override the strategy's declared `BoundsPolicy`. Loop mode
|
||||
* forces `"reflect"` for every pattern so edge-seeking paths
|
||||
* bounce off the screen instead of aborting (`line`) or
|
||||
* sticking in a corner (`clamp`). Absent, the strategy's own
|
||||
* `bounds` is used, so single-sweep behavior is unchanged.
|
||||
* - `loop` — prefer the strategy's infinite `loopPath` when it defines
|
||||
* one. Falls back to `path` when the strategy has no
|
||||
* `loopPath`, so a plain chained-repeat caller can pass this
|
||||
@@ -68,7 +62,6 @@ export type SweepOutcome = "completed" | "interrupted" | "aborted";
|
||||
*/
|
||||
export interface ExecuteOptions {
|
||||
readonly restore?: boolean;
|
||||
readonly bounds?: BoundsPolicy;
|
||||
readonly loop?: boolean;
|
||||
}
|
||||
|
||||
@@ -81,20 +74,17 @@ export interface ExecuteOptions {
|
||||
const READBACK_TOLERANCE: number = 2;
|
||||
|
||||
/**
|
||||
* Pixels to inset the `clamp` / `reflect` travel range from each screen edge.
|
||||
* Keeps edge-seeking patterns off the literal first/last pixel, where DPI
|
||||
* scaling and multi-monitor boundaries most often make the OS place the
|
||||
* cursor a hair off what we commanded (which the readback check would then
|
||||
* misread as the user). `abort` (used by `line`) is deliberately left on the
|
||||
* full `[0, max - 1]` range, so its behavior is unchanged.
|
||||
* Pixels to inset the travel range from each screen edge. Keeps edge-seeking
|
||||
* patterns off the literal first/last pixel, where DPI scaling and
|
||||
* multi-monitor boundaries most often make the OS place the cursor a hair off
|
||||
* what we commanded (which the readback check would then misread as the user).
|
||||
*/
|
||||
const EDGE_MARGIN: number = 2;
|
||||
|
||||
/**
|
||||
* The inclusive `[lo, hi]` integer range an axis of length `max` may travel
|
||||
* under the `clamp` / `reflect` policies: `[0, max - 1]` inset by
|
||||
* `EDGE_MARGIN` on each side. Screens too small to inset fall back to the
|
||||
* full range so the math never inverts.
|
||||
* The inclusive `[lo, hi]` integer range an axis of length `max` may travel:
|
||||
* `[0, max - 1]` inset by `EDGE_MARGIN` on each side. Screens too small to
|
||||
* inset fall back to the full range so the math never inverts.
|
||||
*/
|
||||
function travelRange(max: number): { lo: number; hi: number } {
|
||||
const hiEdge: number = max - 1;
|
||||
@@ -102,18 +92,11 @@ function travelRange(max: number): { lo: number; hi: number } {
|
||||
return { lo: EDGE_MARGIN, hi: hiEdge - EDGE_MARGIN };
|
||||
}
|
||||
|
||||
/** Round to whole pixels and clamp into the inset travel range for `max`. */
|
||||
function clampInt(v: number, max: number): number {
|
||||
const { lo, hi } = travelRange(max);
|
||||
const r: number = Math.round(v);
|
||||
if (r < lo) return lo;
|
||||
if (r > hi) return hi;
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror `v` into the inset travel range for `max` as a triangle wave, so
|
||||
* values past an edge bounce back inside instead of clamping flat against it.
|
||||
* values past an edge bounce back inside instead of running off it. This is
|
||||
* the sole on-screen policy: a coordinate that overshoots an edge reflects
|
||||
* back in, so a pattern keeps moving instead of parking against the boundary.
|
||||
*/
|
||||
function reflectInt(v: number, max: number): number {
|
||||
const { lo, hi } = travelRange(max);
|
||||
@@ -125,27 +108,11 @@ function reflectInt(v: number, max: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a strategy's ideal target to an on-screen integer pixel under the
|
||||
* given policy. Returns `null` when policy is `abort` and the (rounded)
|
||||
* target lies outside the screen — the signal to stop the sweep.
|
||||
* Resolve a strategy's ideal (possibly fractional, possibly off-screen) target
|
||||
* to an on-screen integer pixel by reflecting each axis into its travel range.
|
||||
*/
|
||||
function resolveTarget(
|
||||
policy: BoundsPolicy,
|
||||
p: Point,
|
||||
width: number,
|
||||
height: number,
|
||||
): Point | null {
|
||||
if (policy === "reflect") {
|
||||
return { x: reflectInt(p.x, width), y: reflectInt(p.y, height) };
|
||||
}
|
||||
if (policy === "clamp") {
|
||||
return { x: clampInt(p.x, width), y: clampInt(p.y, height) };
|
||||
}
|
||||
// abort: round, then reject anything off-screen.
|
||||
const x: number = Math.round(p.x);
|
||||
const y: number = Math.round(p.y);
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return null;
|
||||
return { x, y };
|
||||
function resolveTarget(p: Point, width: number, height: number): Point {
|
||||
return { x: reflectInt(p.x, width), y: reflectInt(p.y, height) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,8 +129,8 @@ function timestamp(): string {
|
||||
* against `device`.
|
||||
*
|
||||
* Contract, per step:
|
||||
* 1. Resolve the ideal target to an on-screen integer (bounds policy).
|
||||
* An `abort`-policy out-of-bounds target ends the sweep (`aborted`).
|
||||
* 1. Resolve the ideal target to an on-screen integer by reflecting it
|
||||
* into the travel range.
|
||||
* 2. Command the cursor there and sleep `config.stepDelay` — also the
|
||||
* user's interrupt window.
|
||||
* 3. Re-read the cursor. If it isn't at the point we just commanded, the
|
||||
@@ -174,9 +141,8 @@ function timestamp(): string {
|
||||
* `options.restore === false` (loop mode), in which case the cursor is
|
||||
* left where the last step put it.
|
||||
*
|
||||
* `options` (all optional, see `ExecuteOptions`) let loop mode reuse
|
||||
* this same driver: `bounds` overrides the strategy's policy (loop mode
|
||||
* forces `reflect`), `loop` selects the strategy's infinite `loopPath`, and
|
||||
* `options` (all optional, see `ExecuteOptions`) let loop mode reuse this
|
||||
* same driver: `loop` selects the strategy's infinite `loopPath`, and
|
||||
* `restore` suppresses the snap-back. Omitting `options` reproduces the
|
||||
* original single-sweep contract exactly.
|
||||
*
|
||||
@@ -192,18 +158,13 @@ export async function executePath(
|
||||
options?: ExecuteOptions,
|
||||
): Promise<SweepOutcome> {
|
||||
const { start, width, height } = ctx;
|
||||
const policy: BoundsPolicy = options?.bounds ?? strategy.bounds;
|
||||
const path: Iterable<Point> =
|
||||
options?.loop && strategy.loopPath ? strategy.loopPath(ctx) : strategy.path(ctx);
|
||||
|
||||
log.event(`Simulating activity (${strategy.name}) at ${timestamp()}...`);
|
||||
|
||||
for (const target of path) {
|
||||
const point: Point | null = resolveTarget(policy, target, width, height);
|
||||
if (point === null) {
|
||||
log.event(`Out of bounds at ${timestamp()}; aborting simulation.`);
|
||||
return "aborted";
|
||||
}
|
||||
const point: Point = resolveTarget(target, width, height);
|
||||
|
||||
await device.setPosition(point);
|
||||
await device.sleep(config.stepDelay);
|
||||
@@ -213,17 +174,17 @@ export async function executePath(
|
||||
Math.abs(current.x - point.x) > READBACK_TOLERANCE ||
|
||||
Math.abs(current.y - point.y) > READBACK_TOLERANCE
|
||||
) {
|
||||
// Cursor isn't where we last put it -> real user activity. Abort
|
||||
// Cursor isn't where we last put it -> real user activity. Stop
|
||||
// without snapping back, so we don't yank it from under the user.
|
||||
//
|
||||
// The comparison allows a small tolerance rather than demanding an
|
||||
// exact match: on scaled (fractional-DPI) or multi-monitor setups
|
||||
// the OS can place the cursor a pixel off the coordinate we
|
||||
// commanded, and the edge-seeking patterns (clamp/reflect/arc)
|
||||
// reach exactly the coordinates where that's most likely. A real
|
||||
// user moves far more than a couple of pixels, so this doesn't
|
||||
// meaningfully weaken real-user-wins.
|
||||
log.event(`User activity detected at ${timestamp()}; aborting simulation.`);
|
||||
// commanded, and edge-seeking patterns reach exactly the
|
||||
// coordinates where that's most likely. A real user moves far more
|
||||
// than a couple of pixels, so this doesn't meaningfully weaken
|
||||
// real-user-wins.
|
||||
log.event(`User activity detected at ${timestamp()}; stopping simulation.`);
|
||||
return "interrupted";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user