Implement new Avro schema for creatures (#211)

The creature state stored in #210, while quite compact, is also very fragile, because any minor change in our creature generation algorithm or even vocabulary will completely throw off existing serializations.

This attempts to serialize the actual _creature_ created by the generator rather than its random number seed, which will be a larger serialization but also one that is more resilient to future changes in the algorithm and/or vocabulary.  It should also pave the way for tweaking generated creatures.

Note that the previous `v1` schema implemented in #210 is no longer supported--i.e., links that use it will now be broken.  Hopefully this is OK because it hasn't been around for very long, but it was just way too much work to migrate that schema to the new `v2` format.
pull/212/head
Atul Varma 2021-08-15 14:41:05 -04:00 zatwierdzone przez GitHub
rodzic 25bff780a7
commit beb12345a1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
3 zmienionych plików z 166 dodań i 47 usunięć

Wyświetl plik

@ -34,6 +34,7 @@ import { Checkbox } from "../../checkbox";
import {
CompositionContextWidget,
createSvgCompositionContext,
SvgCompositionContext,
} from "../../svg-composition-context";
import { Page } from "../../page";
import { RandomizerWidget } from "../../randomizer-widget";
@ -206,8 +207,8 @@ const MAX_COMPLEXITY_LEVEL = COMPLEXITY_LEVEL_GENERATORS.length - 1;
const INITIAL_COMPLEXITY_LEVEL = 2;
function getDownloadBasename(randomSeed: number) {
return `mystic-symbolic-creature-${randomSeed}`;
function getDownloadBasename(rootSymbolName: string) {
return `mystic-symbolic-creature-${rootSymbolName}`;
}
function creatureHasSymbol(
@ -244,55 +245,56 @@ function repeatUntilSymbolIsIncluded(
return createCreature(rng);
}
export const CREATURE_DESIGN_DEFAULTS = {
randomSeed: 0,
randomlyInvert: true,
complexity: INITIAL_COMPLEXITY_LEVEL,
alwaysIncludeSymbol: EMPTY_SVG_SYMBOL_DATA,
compCtx: createSvgCompositionContext(),
export type CreatureDesign = {
compCtx: SvgCompositionContext;
creature: CreatureSymbol;
};
export type CreatureDesign = typeof CREATURE_DESIGN_DEFAULTS;
export const CREATURE_DESIGN_DEFAULTS: CreatureDesign = {
compCtx: createSvgCompositionContext(),
creature: {
data: ROOT_SYMBOLS[0],
invertColors: false,
attachments: [],
nests: [],
},
};
export const CreaturePageWithDefaults: React.FC<
ComponentWithShareableStateProps<CreatureDesign>
> = ({ defaults, onChange }) => {
const svgRef = useRef<SVGSVGElement>(null);
const [randomSeed, setRandomSeed] = useState(defaults.randomSeed);
const [randomlyInvert, setRandomlyInvert] = useState(defaults.randomlyInvert);
const [randomlyInvert, setRandomlyInvert] = useState(true);
const [compCtx, setCompCtx] = useState(defaults.compCtx);
const [complexity, setComplexity] = useState(defaults.complexity);
const [complexity, setComplexity] = useState(INITIAL_COMPLEXITY_LEVEL);
const [creature, setCreature] = useState(defaults.creature);
const defaultCtx = useContext(CreatureContext);
const newRandomSeed = () => setRandomSeed(Date.now());
const ctx: CreatureContextType = noFillIfShowingSpecs({
...defaultCtx,
...compCtx,
});
const [alwaysInclude, setAlwaysInclude] = useState<SvgSymbolData>(
defaults.alwaysIncludeSymbol
);
const creature = useMemo(
() =>
const newRandomCreature = () => {
setCreature(
repeatUntilSymbolIsIncluded(
alwaysInclude,
new Random(randomSeed),
new Random(Date.now()),
(rng) =>
COMPLEXITY_LEVEL_GENERATORS[complexity]({
rng,
randomlyInvert,
})
),
[alwaysInclude, complexity, randomSeed, randomlyInvert]
)
);
};
const ctx: CreatureContextType = noFillIfShowingSpecs({
...defaultCtx,
...compCtx,
});
const [alwaysInclude, setAlwaysInclude] = useState<SvgSymbolData>(
EMPTY_SVG_SYMBOL_DATA
);
const design: CreatureDesign = useMemo(
() => ({
randomSeed,
randomlyInvert,
complexity,
alwaysIncludeSymbol: alwaysInclude,
creature,
compCtx,
}),
[randomSeed, randomlyInvert, complexity, alwaysInclude, compCtx]
[creature, compCtx]
);
useDebouncedEffect(
@ -313,7 +315,7 @@ export const CreaturePageWithDefaults: React.FC<
value={complexity}
onChange={(value) => {
setComplexity(value);
newRandomSeed();
newRandomCreature();
}}
/>
</div>
@ -326,7 +328,7 @@ export const CreaturePageWithDefaults: React.FC<
</div>
<RandomizerWidget
onColorsChange={(colors) => setCompCtx({ ...compCtx, ...colors })}
onSymbolsChange={newRandomSeed}
onSymbolsChange={newRandomCreature}
>
<div className="thingy">
<VocabularyWidget
@ -339,7 +341,7 @@ export const CreaturePageWithDefaults: React.FC<
</RandomizerWidget>
<div className="thingy">
<ExportWidget
basename={getDownloadBasename(randomSeed)}
basename={getDownloadBasename(creature.data.name)}
svgRef={svgRef}
/>
</div>

Wyświetl plik

@ -2,10 +2,6 @@
"type": "record",
"name": "AvroCreatureDesign",
"fields": [
{ "name": "randomSeed", "type": "long" },
{ "name": "randomlyInvert", "type": "boolean" },
{ "name": "complexity", "type": "int" },
{ "name": "alwaysIncludeSymbol", "type": "string" },
{
"name": "compCtx",
"type": {
@ -19,6 +15,46 @@
{ "name": "disableGradients", "type": "boolean", "default": true }
]
}
},
{
"name": "creature",
"type": {
"name": "AvroCreatureSymbol",
"type": "record",
"fields": [
{ "name": "symbol", "type": "string" },
{ "name": "invertColors", "type": "boolean" },
{
"name": "attachments",
"type": {
"type": "array",
"items": {
"name": "AvroAttachedCreatureSymbol",
"type": "record",
"fields": [
{ "name": "base", "type": "AvroCreatureSymbol" },
{ "name": "attachTo", "type": "int" },
{ "name": "indices", "type": "bytes" }
]
}
}
},
{
"name": "nests",
"type": {
"type": "array",
"items": {
"name": "AvroNestedCreatureSymbol",
"type": "record",
"fields": [
{ "name": "base", "type": "AvroCreatureSymbol" },
{ "name": "indices", "type": "bytes" }
]
}
}
}
]
}
}
]
}

Wyświetl plik

@ -1,29 +1,107 @@
import * as avro from "avro-js";
import { CreatureDesign } from "./core";
import { AvroCreatureDesign } from "./creature-design.avsc";
import {
AvroAttachedCreatureSymbol,
AvroCreatureDesign,
AvroCreatureSymbol,
AvroNestedCreatureSymbol,
} from "./creature-design.avsc";
import { fromBase64, toBase64 } from "../../base64";
import CreatureAvsc from "./creature-design.avsc.json";
import { SvgVocabularyWithBlank } from "../../svg-vocabulary";
import { Packer, SvgCompositionContextPacker } from "../../serialization";
import {
AttachedCreatureSymbol,
CreatureSymbol,
NestedCreatureSymbol,
} from "../../creature-symbol";
import { SvgVocabulary } from "../../svg-vocabulary";
import { ATTACHMENT_POINT_TYPES } from "../../specs";
const LATEST_VERSION = "v1";
const LATEST_VERSION = "v2";
const avroCreatureDesign = avro.parse<AvroCreatureDesign>(CreatureAvsc);
const DesignConfigPacker: Packer<CreatureDesign, AvroCreatureDesign> = {
const ATTACHMENT_POINT_MAPPING = new Map(
ATTACHMENT_POINT_TYPES.map((name, i) => {
return [name, i];
})
);
const NestedCreatureSymbolPacker: Packer<
NestedCreatureSymbol,
AvroNestedCreatureSymbol
> = {
pack: (value) => {
return {
base: CreatureSymbolPacker.pack(value),
indices: Buffer.from(value.indices),
};
},
unpack: (value) => {
return {
...CreatureSymbolPacker.unpack(value.base),
indices: Array.from(value.indices),
};
},
};
const AttachedCreatureSymbolPacker: Packer<
AttachedCreatureSymbol,
AvroAttachedCreatureSymbol
> = {
pack: (value) => {
const attachTo = ATTACHMENT_POINT_MAPPING.get(value.attachTo);
if (attachTo === undefined) {
throw new Error(`Invalid attachment type "${value.attachTo}"`);
}
return {
base: CreatureSymbolPacker.pack(value),
attachTo,
indices: Buffer.from(value.indices),
};
},
unpack: (value) => {
const attachTo = ATTACHMENT_POINT_TYPES[value.attachTo];
if (attachTo === undefined) {
throw new Error(`Invalid attachment type "${value.attachTo}"`);
}
return {
...CreatureSymbolPacker.unpack(value.base),
attachTo,
indices: Array.from(value.indices),
};
},
};
const CreatureSymbolPacker: Packer<CreatureSymbol, AvroCreatureSymbol> = {
pack: (value) => {
return {
...value,
alwaysIncludeSymbol: value.alwaysIncludeSymbol.name,
compCtx: SvgCompositionContextPacker.pack(value.compCtx),
symbol: value.data.name,
attachments: value.attachments.map(AttachedCreatureSymbolPacker.pack),
nests: value.nests.map(NestedCreatureSymbolPacker.pack),
};
},
unpack: (value) => {
return {
...value,
alwaysIncludeSymbol: SvgVocabularyWithBlank.get(
value.alwaysIncludeSymbol
),
data: SvgVocabulary.get(value.symbol),
attachments: value.attachments.map(AttachedCreatureSymbolPacker.unpack),
nests: value.nests.map(NestedCreatureSymbolPacker.unpack),
};
},
};
const DesignConfigPacker: Packer<CreatureDesign, AvroCreatureDesign> = {
pack: (value) => {
return {
creature: CreatureSymbolPacker.pack(value.creature),
compCtx: SvgCompositionContextPacker.pack(value.compCtx),
};
},
unpack: (value) => {
return {
creature: CreatureSymbolPacker.unpack(value.creature),
compCtx: SvgCompositionContextPacker.unpack(value.compCtx),
};
},
@ -35,7 +113,10 @@ export function serializeCreatureDesign(value: CreatureDesign): string {
}
export function deserializeCreatureDesign(value: string): CreatureDesign {
const [_version, serialized] = value.split(".", 2);
const [version, serialized] = value.split(".", 2);
if (version === "v1") {
throw new Error(`Sorry, we no longer support loading v1 creatures!`);
}
const buf = fromBase64(serialized);
return DesignConfigPacker.unpack(avroCreatureDesign.fromBuffer(buf));
}