Add multiple complexity levels for creature generation. (#28)

Fixes #18 by adding a random creature complexity slider. Currently slider values are 0, 1, 2, 3, 4, and bonkers.
pull/48/head
Atul Varma 2021-02-22 21:50:14 -05:00 zatwierdzone przez GitHub
rodzic 593bcc8b9e
commit 154f1fe61a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
5 zmienionych plików z 136 dodań i 6 usunięć

Wyświetl plik

@ -6,12 +6,17 @@ import {
SvgSymbolContext,
SvgSymbolData,
} from "../svg-symbol";
import { AttachmentPointType, PointWithNormal } from "../specs";
import {
AttachmentPointType,
iterAttachmentPoints,
PointWithNormal,
} from "../specs";
import { getAttachmentTransforms } from "../attach";
import { scalePointXY } from "../point";
import { Point } from "../../vendor/bezier-js";
import { Random } from "../random";
import { SymbolContextWidget } from "../symbol-context-widget";
import { range } from "../util";
const SYMBOL_MAP = new Map(
SvgVocabulary.map((symbol) => [symbol.name, symbol])
@ -84,6 +89,7 @@ type CreatureSymbolProps = AttachmentIndices & {
data: SvgSymbolData;
children?: AttachmentChildren;
attachTo?: AttachmentPointType;
indices?: number[];
};
function getAttachmentIndices(ai: AttachmentIndices): number[] {
@ -131,7 +137,7 @@ const CreatureSymbol: React.FC<CreatureSymbolProps> = (props) => {
);
}
const attachmentIndices = getAttachmentIndices(props);
const attachmentIndices = props.indices || getAttachmentIndices(props);
const children: JSX.Element[] = [];
for (let attachIndex of attachmentIndices) {
@ -234,6 +240,33 @@ const Leg = createCreatureSymbol("leg");
const Tail = createCreatureSymbol("tail");
function getSymbolWithAttachments(
numAttachmentKinds: number,
rng: Random
): JSX.Element {
const children: JSX.Element[] = [];
const root = rng.choice(SvgVocabulary);
if (root.specs) {
const attachmentKinds = rng.uniqueChoices(
Array.from(iterAttachmentPoints(root.specs)).map((point) => point.type),
numAttachmentKinds
);
for (let kind of attachmentKinds) {
const attachment = rng.choice(SvgVocabulary);
const indices = range(root.specs[kind]?.length ?? 0);
children.push(
<CreatureSymbol
data={attachment}
key={children.length}
attachTo={kind}
indices={indices}
/>
);
}
}
return <CreatureSymbol data={root} children={children} />;
}
const EYE_CREATURE = (
<Eye>
<Arm attachTo="arm" left>
@ -262,6 +295,15 @@ function randomlyReplaceParts(rng: Random, creature: JSX.Element): JSX.Element {
});
}
type CreatureGenerator = (rng: Random) => JSX.Element;
const COMPLEXITY_LEVEL_GENERATORS: CreatureGenerator[] = [
...range(5).map((i) => getSymbolWithAttachments.bind(null, i)),
(rng) => randomlyReplaceParts(rng, EYE_CREATURE),
];
const MAX_COMPLEXITY_LEVEL = COMPLEXITY_LEVEL_GENERATORS.length - 1;
function getSvgMarkup(el: SVGSVGElement): string {
return [
`<?xml version="1.0" encoding="utf-8"?>`,
@ -348,7 +390,9 @@ export const CreaturePage: React.FC<{}> = () => {
const [bgColor, setBgColor] = useState("#cccccc");
const [randomSeed, setRandomSeed] = useState<number | null>(null);
const [symbolCtx, setSymbolCtx] = useState(createSvgSymbolContext());
const [complexity, setComplexity] = useState(MAX_COMPLEXITY_LEVEL);
const defaultCtx = useContext(CreatureContext);
const newRandomSeed = () => setRandomSeed(Date.now());
const ctx: CreatureContextType = {
...defaultCtx,
...symbolCtx,
@ -357,7 +401,7 @@ export const CreaturePage: React.FC<{}> = () => {
const creature =
randomSeed === null
? EYE_CREATURE
: randomlyReplaceParts(new Random(randomSeed), EYE_CREATURE);
: COMPLEXITY_LEVEL_GENERATORS[complexity](new Random(randomSeed));
const handleSvgExport = () =>
exportSvg(getDownloadFilename(randomSeed), svgRef);
@ -373,7 +417,22 @@ export const CreaturePage: React.FC<{}> = () => {
/>{" "}
</SymbolContextWidget>
<p>
<button accessKey="r" onClick={() => setRandomSeed(Date.now())}>
<label htmlFor="complexity">Random creature complexity: </label>
<input
type="range"
min={0}
max={MAX_COMPLEXITY_LEVEL}
step={1}
value={complexity}
onChange={(e) => {
setComplexity(parseInt(e.target.value));
newRandomSeed();
}}
/>{" "}
{complexity === MAX_COMPLEXITY_LEVEL ? "bonkers" : complexity}
</p>
<p>
<button accessKey="r" onClick={newRandomSeed}>
<u>R</u>andomize!
</button>{" "}
<button onClick={() => window.location.reload()}>Reset</button>{" "}

28
lib/random.test.ts 100644
Wyświetl plik

@ -0,0 +1,28 @@
import { Random } from "./random";
describe("choice()", () => {
it("works", () => {
expect(new Random().choice([1])).toBe(1);
expect(new Random(134).choice([1, 5, 9, 7])).toBe(5);
});
it("throws an error if passed an empty array", () => {
expect(() => new Random().choice([])).toThrow(
/Cannot choose randomly from an empty array/
);
});
});
describe("uniqueChoices()", () => {
it("works", () => {
expect(new Random(3).uniqueChoices([1, 2, 3], 2)).toEqual([1, 3]);
});
it("returns fewer choices than asked if necessary", () => {
expect(new Random().uniqueChoices([1, 1, 1], 5)).toEqual([1]);
});
it("returns an empty array if needed", () => {
expect(new Random().uniqueChoices([], 5)).toEqual([]);
});
});

Wyświetl plik

@ -36,10 +36,34 @@ export class Random {
}
/**
* Return a random item from the given array.
* Return a random item from the given array. If the array is
* empty, an exception is thrown.
*/
choice<T>(array: T[]): T {
if (array.length === 0) {
throw new Error("Cannot choose randomly from an empty array!");
}
const idx = Math.floor(this.next() * array.length);
return array[idx];
}
/**
* Attempt to randomly choose *at most* the given number of unique items from
* the array. If the array is too small, fewer items are returned.
*/
uniqueChoices<T>(array: T[], howMany: number): T[] {
let choicesLeft = [...array];
const result: T[] = [];
for (let i = 0; i < howMany; i++) {
if (choicesLeft.length === 0) {
break;
}
const choice = this.choice(choicesLeft);
choicesLeft = choicesLeft.filter((item) => item !== choice);
result.push(choice);
}
return result;
}
}

Wyświetl plik

@ -1,4 +1,4 @@
import { flatten, float, rad2deg } from "./util";
import { flatten, float, rad2deg, range } from "./util";
describe("float", () => {
it("converts strings", () => {
@ -25,3 +25,9 @@ test("rad2deg() works", () => {
expect(rad2deg(Math.PI - 0.0000001)).toBeCloseTo(180);
expect(rad2deg(2 * Math.PI)).toBe(360);
});
test("range() works", () => {
expect(range(0)).toEqual([]);
expect(range(1)).toEqual([0]);
expect(range(5)).toEqual([0, 1, 2, 3, 4]);
});

Wyświetl plik

@ -27,3 +27,16 @@ export function flatten<T>(arr: T[][]): T[] {
export function rad2deg(radians: number): number {
return (radians * 180) / Math.PI;
}
/**
* Return an array containing the numbers from 0 to one
* less than the given value, increasing.
*/
export function range(count: number): number[] {
const result: number[] = [];
for (let i = 0; i < count; i++) {
result.push(i);
}
return result;
}