Add a 'creature_frequency_multiplier' TOML property. (#207)
Fixes #191. Currently the implementation is fairly naive, in that a really large value for `creature_frequency_multiplier` will not be handled terribly efficiently, but we can optimize later if needed.pull/210/head
rodzic
34fb3080fa
commit
e4b401f97f
|
@ -45,3 +45,10 @@ never_flip_attachments = false
|
|||
|
||||
# If true, symbol may be used as large background shape in some compositions.
|
||||
background = false
|
||||
|
||||
# Setting this to a positive integer (whole number) will multiply the
|
||||
# likelihood that this symbol will be chosen from a random selection of symbols
|
||||
# by the given amount when this symbol is used in a creature. For example,
|
||||
# setting it to 2 will make it twice as likely to be chosen, setting it to 5
|
||||
# will make it five times more likely, and so on.
|
||||
creature_frequency_multiplier = 1
|
||||
|
|
|
@ -2,3 +2,5 @@
|
|||
never_be_nested = true
|
||||
|
||||
attach_to = ["leg", "arm", "horn"]
|
||||
|
||||
creature_frequency_multiplier = 40
|
|
@ -5,3 +5,5 @@ never_be_nested = true
|
|||
rotate_clockwise = true
|
||||
|
||||
attach_to = ["leg", "arm", "horn"]
|
||||
|
||||
creature_frequency_multiplier = 50
|
|
@ -2,3 +2,5 @@
|
|||
never_be_nested = true
|
||||
|
||||
attach_to = ["leg", "arm", "horn"]
|
||||
|
||||
creature_frequency_multiplier = 50
|
|
@ -5,3 +5,4 @@ never_be_nested = true
|
|||
|
||||
|
||||
attach_to = ["tail", "leg", "arm", "horn", "crown"]
|
||||
|
||||
|
|
|
@ -2,3 +2,5 @@
|
|||
never_be_nested = true
|
||||
|
||||
attach_to = ["leg", "arm", "horn"]
|
||||
|
||||
creature_frequency_multiplier = 40
|
|
@ -0,0 +1,11 @@
|
|||
import { createDistribution } from "./distribution";
|
||||
|
||||
test("createDistribution() works", () => {
|
||||
type Thing = { name: string; freq: number };
|
||||
const one: Thing = { name: "one", freq: 1 };
|
||||
const two: Thing = { name: "two", freq: 2 };
|
||||
|
||||
const dist = createDistribution([one, two], (thing) => thing.freq);
|
||||
|
||||
expect(dist).toEqual([one, two, two]);
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Return a probability distribution of the given Array.
|
||||
*
|
||||
* Each item may be repeated more than once in the return value,
|
||||
* making it more likely for the item to be randomly chosen.
|
||||
*
|
||||
* @param items The list of items to create a distribution from.
|
||||
*
|
||||
* @param getFrequencyMultiplier A function that takes an item
|
||||
* and returns a positive integer specifying how many times
|
||||
* it should be included in the distribution.
|
||||
*/
|
||||
export function createDistribution<T>(
|
||||
items: T[],
|
||||
getFrequencyMultiplier: (item: T) => number
|
||||
) {
|
||||
const result: T[] = [];
|
||||
|
||||
for (let item of items) {
|
||||
const freq = getFrequencyMultiplier(item);
|
||||
for (let i = 0; i < freq; i++) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -32,6 +32,7 @@ import {
|
|||
import { Page } from "../page";
|
||||
import { RandomizerWidget } from "../randomizer-widget";
|
||||
import { VocabularyWidget } from "../vocabulary-widget";
|
||||
import { createDistribution } from "../distribution";
|
||||
|
||||
/**
|
||||
* The minimum number of attachment points that any symbol used as the main body
|
||||
|
@ -41,8 +42,14 @@ import { VocabularyWidget } from "../vocabulary-widget";
|
|||
*/
|
||||
const MIN_ROOT_ATTACHMENT_POINTS = 2;
|
||||
|
||||
const getFilteredDistribution = (predicate: (item: SvgSymbolData) => boolean) =>
|
||||
createDistribution(
|
||||
SvgVocabulary.items.filter(predicate),
|
||||
(s) => s.meta?.creature_frequency_multiplier ?? 1
|
||||
);
|
||||
|
||||
/** Symbols that can be the "root" (i.e., main body) of a creature. */
|
||||
const ROOT_SYMBOLS = SvgVocabulary.items.filter(
|
||||
const ROOT_SYMBOLS = getFilteredDistribution(
|
||||
(data) =>
|
||||
data.meta?.always_be_nested !== true &&
|
||||
Array.from(iterAttachmentPoints(data.specs || {})).length >=
|
||||
|
@ -61,7 +68,7 @@ const ATTACHMENT_SYMBOLS: AttachmentSymbolMap = (() => {
|
|||
const result = {} as AttachmentSymbolMap;
|
||||
|
||||
for (let type of ATTACHMENT_POINT_TYPES) {
|
||||
result[type] = SvgVocabulary.items.filter((data) => {
|
||||
result[type] = getFilteredDistribution((data) => {
|
||||
const { meta } = data;
|
||||
|
||||
if (type === "wildcard") {
|
||||
|
@ -91,7 +98,7 @@ const ATTACHMENT_SYMBOLS: AttachmentSymbolMap = (() => {
|
|||
})();
|
||||
|
||||
/** Symbols that can be nested within any part of a creature. */
|
||||
const NESTED_SYMBOLS = SvgVocabulary.items.filter(
|
||||
const NESTED_SYMBOLS = getFilteredDistribution(
|
||||
// Since we don't currently support recursive nesting, ignore anything that
|
||||
// wants nested children.
|
||||
(data) =>
|
||||
|
|
|
@ -3,6 +3,7 @@ import fs from "fs";
|
|||
import toml from "toml";
|
||||
import {
|
||||
validateAttachTo,
|
||||
validateFrequencyMultiplier,
|
||||
validateSvgSymbolMetadata,
|
||||
} from "./svg-symbol-metadata";
|
||||
import { withMockConsoleLog } from "./test-util";
|
||||
|
@ -77,3 +78,27 @@ describe("validateAttachTo()", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateFrequencyMultiplier()", () => {
|
||||
it("works", () => {
|
||||
expect(validateFrequencyMultiplier(5)).toBe(5);
|
||||
});
|
||||
|
||||
it("enforces minimum value", () => {
|
||||
withMockConsoleLog((mockLog) => {
|
||||
expect(validateFrequencyMultiplier(-1)).toBe(1);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
"Frequency multiplier is less than minimum of 1."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores garbage values", () => {
|
||||
withMockConsoleLog((mockLog) => {
|
||||
expect(validateFrequencyMultiplier("barf")).toBeUndefined;
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'Frequency multiplier "barf" is not a number.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -62,6 +62,15 @@ export type SvgSymbolMetadata = SvgSymbolMetadataBooleans & {
|
|||
* be able to attach to any symbol.
|
||||
*/
|
||||
attach_to?: AttachmentPointType[];
|
||||
|
||||
/**
|
||||
* Setting this to a positive integer (whole number) will multiply the
|
||||
* likelihood that this symbol will be chosen from a random selection of symbols
|
||||
* by the given amount when this symbol is used in a creature. For example,
|
||||
* setting it to 2 will make it twice as likely to be chosen, setting it to 5
|
||||
* will make it five times more likely, and so on.
|
||||
*/
|
||||
creature_frequency_multiplier?: number;
|
||||
};
|
||||
|
||||
export function validateSvgSymbolMetadata(obj: any): {
|
||||
|
@ -81,6 +90,10 @@ export function validateSvgSymbolMetadata(obj: any): {
|
|||
metadata[key] = value;
|
||||
} else if (key === "attach_to") {
|
||||
metadata.attach_to = validateAttachTo(obj[key]);
|
||||
} else if (key === "creature_frequency_multiplier") {
|
||||
metadata.creature_frequency_multiplier = validateFrequencyMultiplier(
|
||||
obj[key]
|
||||
);
|
||||
} else {
|
||||
unknownProperties.push(key);
|
||||
}
|
||||
|
@ -88,6 +101,24 @@ export function validateSvgSymbolMetadata(obj: any): {
|
|||
return { metadata, unknownProperties };
|
||||
}
|
||||
|
||||
const MIN_FREQUENCY_MULTIPLIER = 1;
|
||||
|
||||
export function validateFrequencyMultiplier(
|
||||
value: unknown
|
||||
): number | undefined {
|
||||
if (typeof value === "number") {
|
||||
if (value < MIN_FREQUENCY_MULTIPLIER) {
|
||||
console.log(
|
||||
`Frequency multiplier is less than minimum of ${MIN_FREQUENCY_MULTIPLIER}.`
|
||||
);
|
||||
return MIN_FREQUENCY_MULTIPLIER;
|
||||
}
|
||||
return Math.floor(value);
|
||||
}
|
||||
console.log(`Frequency multiplier "${value}" is not a number.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function validateAttachTo(value: unknown): AttachmentPointType[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(
|
||||
|
|
|
@ -3,7 +3,7 @@ import { SVGProps } from "react";
|
|||
import { BBox } from "../vendor/bezier-js";
|
||||
import { FILL_REPLACEMENT_COLOR, STROKE_REPLACEMENT_COLOR } from "./colors";
|
||||
import { AttachmentPointType, PointWithNormal, Specs } from "./specs";
|
||||
import type { SvgSymbolMetadata } from "./svg-symbol-metadata";
|
||||
import { SvgSymbolMetadata } from "./svg-symbol-metadata";
|
||||
import {
|
||||
UniqueIdMap,
|
||||
URL_FUNC_TO_ANCHOR_RE,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { Vocabulary } from "./vocabulary";
|
||||
|
||||
describe("Vocabulary", () => {
|
||||
type Thing = { name: string; freq: number };
|
||||
const one: Thing = { name: "one", freq: 1 };
|
||||
const two: Thing = { name: "two", freq: 2 };
|
||||
|
||||
const v = new Vocabulary([one, two]);
|
||||
|
||||
it("gets items", () => {
|
||||
expect(v.items).toEqual([one, two]);
|
||||
expect(v.get("one")).toBe(one);
|
||||
expect(v.get("two")).toBe(two);
|
||||
});
|
||||
|
||||
it("raises exception on items not found", () => {
|
||||
expect(() => v.get("boop")).toThrow('Unable to find the item "boop"!');
|
||||
});
|
||||
});
|
|
@ -25,9 +25,8 @@
|
|||
"moduleNameMapper": {
|
||||
"\\.(css)$": "<rootDir>/__mocks__/css-mock.js"
|
||||
},
|
||||
"testPathIgnorePatterns": [
|
||||
"dist",
|
||||
".cache"
|
||||
"collectCoverageFrom": [
|
||||
"lib/**/*.{ts,tsx}"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
Ładowanie…
Reference in New Issue