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:
+38
-7
@@ -25,7 +25,14 @@
|
||||
|
||||
import { createNutDevice, type Device, type Point } from "./device.ts";
|
||||
import { executePath, type Logger, type SweepOutcome } from "./executor.ts";
|
||||
import { DEFAULT_PATTERN, STRATEGIES, type MoveContext } from "./strategies.ts";
|
||||
import {
|
||||
createRandomPicker,
|
||||
DEFAULT_PATTERN,
|
||||
RANDOM_PATTERN,
|
||||
STRATEGIES,
|
||||
type MoveContext,
|
||||
type MovementStrategy,
|
||||
} from "./strategies.ts";
|
||||
|
||||
import type { Config } from "./config.ts";
|
||||
|
||||
@@ -54,6 +61,14 @@ function makeLogger(verbose: boolean): Logger {
|
||||
* `config.pattern` falls back to the default strategy defensively; validation
|
||||
* at the CLI / config-file boundary should prevent that from ever happening.
|
||||
*
|
||||
* `pattern: "random"` isn't a registry key — it asks for a fresh pattern per
|
||||
* sweep, so `pickRandom` supplies one here. The pick happens once, before the
|
||||
* loop-mode branch below, which is what makes a random selection hold for an
|
||||
* entire loop run rather than changing under the user mid-run; the picker's
|
||||
* own no-repeat memory then spans sweeps, since the keeper holds one picker
|
||||
* for the life of the process. Because the pick is a real strategy, the log
|
||||
* lines below and in `executePath` name the concrete pattern, not "random".
|
||||
*
|
||||
* Single-sweep mode (`config.loop === false`) runs exactly one sweep via
|
||||
* `executePath`, which owns on-screen reflection, pacing, interrupt
|
||||
* detection, and restore-on-clean — unchanged from before loop mode existed.
|
||||
@@ -66,10 +81,18 @@ function makeLogger(verbose: boolean): Logger {
|
||||
* position each cycle. Per-cycle event logs are suppressed to avoid unbounded
|
||||
* output — one line brackets the run at each end.
|
||||
*/
|
||||
async function simulateActivity(config: Config, log: Logger, device: Device): Promise<void> {
|
||||
async function simulateActivity(
|
||||
config: Config,
|
||||
log: Logger,
|
||||
device: Device,
|
||||
pickRandom: () => MovementStrategy,
|
||||
): Promise<void> {
|
||||
const width: number = await device.width();
|
||||
const height: number = await device.height();
|
||||
const strategy = STRATEGIES[config.pattern] ?? STRATEGIES[DEFAULT_PATTERN]!;
|
||||
const strategy: MovementStrategy =
|
||||
config.pattern === RANDOM_PATTERN
|
||||
? pickRandom()
|
||||
: (STRATEGIES[config.pattern] ?? STRATEGIES[DEFAULT_PATTERN]!);
|
||||
|
||||
if (!config.loop) {
|
||||
const start: Point = await device.getPosition();
|
||||
@@ -119,10 +142,18 @@ async function simulateActivity(config: Config, log: Logger, device: Device): Pr
|
||||
* cursor, and on a user-interrupted sweep the next iteration sees the
|
||||
* user's new position and correctly resets the clock.
|
||||
*
|
||||
* @param config - Resolved runtime config.
|
||||
* @param device - I/O device; defaults to the production nut.js device.
|
||||
* @param config - Resolved runtime config.
|
||||
* @param device - I/O device; defaults to the production nut.js device.
|
||||
* @param pickRandom - Supplies a strategy when `config.pattern` is `random`.
|
||||
* Created once here (not per sweep) so its no-repeat
|
||||
* memory spans the whole run; injectable so tests can
|
||||
* drive a deterministic sequence.
|
||||
*/
|
||||
export async function runKeeper(config: Config, device?: Device): Promise<void> {
|
||||
export async function runKeeper(
|
||||
config: Config,
|
||||
device?: Device,
|
||||
pickRandom: () => MovementStrategy = createRandomPicker(),
|
||||
): Promise<void> {
|
||||
const dev: Device = device ?? (await createNutDevice());
|
||||
|
||||
const log = makeLogger(config.verbose);
|
||||
@@ -144,7 +175,7 @@ export async function runKeeper(config: Config, device?: Device): Promise<void>
|
||||
}
|
||||
|
||||
if (now - lastActivity >= config.moveInterval) {
|
||||
await simulateActivity(config, log, dev);
|
||||
await simulateActivity(config, log, dev, pickRandom);
|
||||
// The sweep either restored the cursor to its start (clean) or
|
||||
// left it where the user moved it (interrupt). Either way, reset
|
||||
// the clock and require another full moveInterval of inactivity
|
||||
|
||||
Reference in New Issue
Block a user