Add random pattern selection (-r / --pattern random)
Pick a different movement pattern every time a sweep is triggered, so the motion varies across the day instead of repeating one shape. - strategies.ts: add the `random` sentinel, `SELECTABLE_PATTERN_NAMES`, `isSelectablePattern`, and `createRandomPicker`. `random` is deliberately NOT a registry entry: it has no path of its own, so `STRATEGIES` stays a total lookup and `PATTERN_NAMES` keeps listing only real generators. The picker is a closure over `last`, giving a uniform draw that never returns the same pattern twice in a row. Building CANONICAL_PATTERNS from the selectable list makes both validation boundaries accept `random` (and loose spellings) for free, and extends the normalization-collision assertion to cover the sentinel. - cli.ts: add `-r`/`--random` plus an exported `selectPattern` holding the conflict rule. `-r` is sugar for `--pattern random`, so the two agreeing is a no-op while `-r -p arc` is rejected as contradictory. The flag folds into `pattern`, so ConfigOverrides, resolveConfig, and move.ts are untouched. `parseCliArgs` now takes its argv as an optional parameter so the flag surface is testable without process.argv. - keeper.ts: resolve `random` via the picker once per trigger, before the loop-mode branch, so a pick holds for a whole loop run rather than changing mid-run. runKeeper builds one picker for the process, so the no-repeat memory spans sweeps minutes apart. Because the pick is a real strategy, --verbose logs the concrete pattern name and a pick with an infinite loopPath still bounces edge-to-edge under --loop. - config.ts / configFile.ts: accept the sentinel where a pattern is valid, and quote the selectable list in errors. No `random` boolean config key — the file spells it "pattern": "random". executor.ts and move.ts needed no changes. Tests: new tests/cli.test.ts (the file had no coverage before) covering the flag surface and the conflict rule; picker tests pinning the no-repeat and full-registry-coverage properties; keeper tests pinning once-per-trigger and once-per-loop-run.
This commit is contained in:
@@ -119,8 +119,12 @@ Options:
|
||||
-d, --step-delay <ms> Pause between synthetic steps. Default: 50.
|
||||
-p, --pattern <name> Movement strategy. Default: line.
|
||||
One of: line, diagonal, jitter, walk, arc,
|
||||
figureEight. Each pattern defines its own
|
||||
size and speed.
|
||||
figureEight, random. Each pattern defines
|
||||
its own size and speed.
|
||||
-r, --random Shorthand for --pattern random. Picks a
|
||||
different pattern for each sweep, never the
|
||||
same one twice in a row. In loop mode the
|
||||
pick holds for the whole loop run.
|
||||
-V, --verbose Log every sweep and interrupt
|
||||
(default prints only the startup banner).
|
||||
-l, --loop Loop mode: once a sweep is triggered,
|
||||
@@ -191,7 +195,7 @@ doesn't set.
|
||||
All keys are optional; supply only the ones you want to override. Keys
|
||||
and units mirror the CLI flags exactly: `moveInterval` and
|
||||
`checkInterval` are seconds, `stepDelay` is milliseconds, `pattern` is a
|
||||
movement strategy name, `verbose` and `loop` are booleans.
|
||||
movement strategy name (or `"random"`), `verbose` and `loop` are booleans.
|
||||
|
||||
> The obsolete `stepCount` / `stepSize` keys (removed in 1.3.0) are
|
||||
> tolerated for backward compatibility: they're ignored with a one-line
|
||||
@@ -223,9 +227,11 @@ The loader is strict:
|
||||
- Root must be a JSON object.
|
||||
- Unknown keys are rejected (catches typos like `"movInterval"`).
|
||||
- Numeric values must be finite and strictly positive.
|
||||
- `pattern` must resolve to a registered strategy name. Matching ignores
|
||||
case and separators (`-`, `_`, spaces), so `figure-eight` and `figureEight`
|
||||
are equivalent.
|
||||
- `pattern` must resolve to a registered strategy name, or to `random`.
|
||||
Matching ignores case and separators (`-`, `_`, spaces), so `figure-eight`
|
||||
and `figureEight` are equivalent. There is no `random` boolean key — the
|
||||
CLI's `-r` is sugar for `--pattern random`, and the file spells it the
|
||||
same way.
|
||||
- `verbose` must be a boolean.
|
||||
- `loop` must be a boolean.
|
||||
|
||||
@@ -287,7 +293,8 @@ and everything but the raw nut.js call is unit-testable:
|
||||
injectable, so tests drive the loop and executor with a fake.
|
||||
- `src/strategies.ts` holds the pure movement patterns — each a generator
|
||||
of target points given a start, screen size, config, and RNG — plus the
|
||||
registry and name validation. Adding a pattern is one pure function.
|
||||
registry, name validation, and the `random` picker. Adding a pattern is
|
||||
one pure function.
|
||||
- `src/executor.ts` is the single `executePath` driver: it rounds targets,
|
||||
reflects any off-screen coordinate back inside, paces steps, detects
|
||||
real-user interruption, and restores the cursor on a clean sweep.
|
||||
@@ -299,7 +306,7 @@ Defaults live in `src/config.ts` as `DEFAULT_CONFIG`:
|
||||
| `moveInterval` | `4 * 60_000` | `-m`, `--move-interval` | Idle time (ms) required before a synthetic sweep fires. |
|
||||
| `checkInterval` | `10_000` | `-c`, `--check-interval` | How often (ms) the main loop polls the cursor for real activity. |
|
||||
| `stepDelay` | `50` | `-d`, `--step-delay` | Pause (ms) between individual synthetic steps in a sweep. |
|
||||
| `pattern` | `"line"` | `-p`, `--pattern` | Movement strategy name (see Movement strategies below). |
|
||||
| `pattern` | `"line"` | `-p`, `--pattern`, `-r` | Movement strategy name, or `random` (see Movement strategies below). |
|
||||
|
||||
`-m` and `-c` are accepted in seconds at the CLI; `resolveConfig` converts
|
||||
to milliseconds before handing the resolved `Config` to `runKeeper`.
|
||||
@@ -319,6 +326,8 @@ to milliseconds before handing the resolved `Config` to `runKeeper`.
|
||||
1. `simulateActivity` snapshots the starting position and current screen
|
||||
dimensions (re-read every sweep so monitor changes are handled), looks
|
||||
up `config.pattern` in the strategy registry, and builds a `MoveContext`.
|
||||
When `config.pattern` is `random` — the one name the registry doesn't
|
||||
contain — the strategy comes from the picker instead, once per trigger.
|
||||
2. It hands the strategy and context to `executePath`, which drives the
|
||||
sweep. For each target the strategy yields:
|
||||
- Round to whole pixels and reflect any off-screen coordinate back inside
|
||||
@@ -346,7 +355,8 @@ or multi-monitor displays isn't misread as the user grabbing the mouse.
|
||||
|
||||
### Movement strategies
|
||||
|
||||
`config.pattern` selects one of the generators in `src/strategies.ts`:
|
||||
`config.pattern` selects one of the generators in `src/strategies.ts` (or
|
||||
`random`, which picks one for you):
|
||||
|
||||
| Name | Motion | Steps | Size |
|
||||
| ------------- | ------------------------------------------------------------- | ----- | ----------- |
|
||||
@@ -356,6 +366,35 @@ or multi-monitor displays isn't misread as the user grabbing the mouse.
|
||||
| `walk` | Cumulative random walk; bounces off the screen edges. | 200 | ±4px/step |
|
||||
| `arc` | Smooth quadratic-Bézier curve to a random on-screen point. | 120 | ~300px |
|
||||
| `figureEight` | Traces a figure-eight (lemniscate) and returns to the start. | 90 | ~250px wide |
|
||||
| `random` | Meta-selection: a different one of the above per sweep. | — | — |
|
||||
|
||||
### Random (`-r` / `--pattern random`)
|
||||
|
||||
`random` isn't a movement pattern of its own — it's a selection that resolves
|
||||
to one of the real patterns above each time a sweep fires:
|
||||
|
||||
```sh
|
||||
move -r # a different pattern every sweep
|
||||
move --pattern random -V # verbose names the pattern each sweep picked
|
||||
move -r --loop # one random pick, looped until you move the mouse
|
||||
```
|
||||
|
||||
Two rules make it predictable:
|
||||
|
||||
- **Never twice in a row.** Consecutive sweeps always use different patterns,
|
||||
so the motion visibly varies instead of occasionally repeating itself.
|
||||
- **One pick per trigger.** In loop mode a single trigger runs many cycles;
|
||||
the pattern is chosen once and holds for that whole run rather than
|
||||
changing mid-run.
|
||||
|
||||
Because the pick is a real strategy, it behaves exactly as if you'd named it:
|
||||
`--verbose` logs the concrete pattern (`Simulating activity (arc)...`), and a
|
||||
pick with an infinite loop path (`line`, `diagonal`) bounces edge-to-edge
|
||||
under `--loop` just as selecting it directly would.
|
||||
|
||||
`-r` and `--pattern` state the same setting two ways, so passing both is
|
||||
rejected (exit `2`) unless they agree — `move -r -p arc` is an error, while
|
||||
`move -r -p random` is a harmless no-op.
|
||||
|
||||
Every pattern is kept on-screen the same way: the executor reflects any
|
||||
coordinate that would fall past a screen edge back inside, so motion bounces
|
||||
@@ -423,7 +462,7 @@ move --help
|
||||
| `src/errors.ts` | Shared error types (`CliError`). |
|
||||
| `src/keeper.ts` | Idle-watch loop + per-sweep glue (selects a strategy, calls the executor). |
|
||||
| `src/device.ts` | `Device` I/O seam over nut.js (`Point`, `createNutDevice`); the only nut.js importer. |
|
||||
| `src/strategies.ts` | Pure movement-pattern generators, the strategy registry, and name validation. |
|
||||
| `src/strategies.ts` | Pure movement-pattern generators, the strategy registry, name validation, and the `random` picker. |
|
||||
| `src/executor.ts` | `executePath` driver: on-screen reflection, pacing, interrupt detection, restore. |
|
||||
| `docs/execution-happy-path.md` | Sequence diagram + invariants for a clean sweep. |
|
||||
| `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. |
|
||||
|
||||
Reference in New Issue
Block a user