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
Atul Varma 2021-07-17 14:39:19 -04:00 zatwierdzone przez GitHub
rodzic 34fb3080fa
commit e4b401f97f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
14 zmienionych plików z 147 dodań i 12 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -2,3 +2,5 @@
never_be_nested = true
attach_to = ["leg", "arm", "horn"]
creature_frequency_multiplier = 40

Wyświetl plik

@ -5,3 +5,5 @@ never_be_nested = true
rotate_clockwise = true
attach_to = ["leg", "arm", "horn"]
creature_frequency_multiplier = 50

Wyświetl plik

@ -2,3 +2,5 @@
never_be_nested = true
attach_to = ["leg", "arm", "horn"]
creature_frequency_multiplier = 50

Wyświetl plik

@ -5,3 +5,4 @@ never_be_nested = true
attach_to = ["tail", "leg", "arm", "horn", "crown"]

Wyświetl plik

@ -2,3 +2,5 @@
never_be_nested = true
attach_to = ["leg", "arm", "horn"]
creature_frequency_multiplier = 40

Wyświetl plik

@ -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]);
});

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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) =>

Wyświetl plik

@ -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.'
);
});
});
});

Wyświetl plik

@ -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(

Wyświetl plik

@ -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,

Wyświetl plik

@ -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"!');
});
});

Wyświetl plik

@ -25,9 +25,8 @@
"moduleNameMapper": {
"\\.(css)$": "<rootDir>/__mocks__/css-mock.js"
},
"testPathIgnorePatterns": [
"dist",
".cache"
"collectCoverageFrom": [
"lib/**/*.{ts,tsx}"
]
},
"dependencies": {