Transforming action generators #
Rather than writing a generator from scratch, you can often
adapt an existing one, such as the ones provided as defaults.
Action
generators have map, filter, and
flatMap methods that work similarly to those in
Array. Weights are preserved when transforming a
generator. Use isAction to check what kind of
action you’re dealing with, which also narrows its type.
The difference between the two mapping methods is what the
function returns. With map, each action is replaced
by a single other action. With flatMap, each action
is replaced by an array of actions, picked uniformly, or by
another action generator. Returning an empty array from
flatMap drops the action.
Here’s the default clicks generator, restricted
to elements outside of a sidebar:
import { extract, weighted, type Point } from "@antithesishq/bombadil/browser";
import { isAction, type Range } from "@antithesishq/bombadil/actions";
import {
clicks,
inputs,
scroll,
} from "@antithesishq/bombadil/browser/defaults/actions";
const sidebar = extract((state) => {
const element = state.document.querySelector("#sidebar");
if (!element) return null;
const { left, top, right, bottom } = element.getBoundingClientRect();
return { left, top, right, bottom };
});
function inSidebar({ x, y }: Point<Range>): boolean {
const rect = sidebar.current;
return (
rect !== null &&
typeof x === "number" &&
typeof y === "number" &&
x >= rect.left &&
x <= rect.right &&
y >= rect.top &&
y <= rect.bottom
);
}
export const myActions = weighted([
[
100,
clicks.filter(
(action) =>
!(isAction(action, "Click") && inSidebar(action.Click.point)),
),
],
[100, inputs],
[50, scroll],
]);Only export the filtered clicks and not the
original definition. Otherwise Bombadil will still be able to
click in the sidebar.
The map method replaces each action, here
turning every click into a double-click:
export const doubleClicks = clicks.map((action) =>
isAction(action, "Click") ? { DoubleClick: action.Click } : action,
);To enable a generator only in certain states, use
when with a boolean, a cell, or a function
returning a boolean. When the condition is false, the generator
isn’t run at all:
export const clicksUnlessModal = clicks.when(() => !modalOpen.current);