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
rodzic
593bcc8b9e
commit
154f1fe61a
|
@ -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>{" "}
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
|
|
13
lib/util.ts
13
lib/util.ts
|
@ -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;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue