Skip to content

Commit

Permalink
Fix multiple rules application
Browse files Browse the repository at this point in the history
  • Loading branch information
niktekusho committed May 19, 2024
1 parent 093d14d commit 8983d50
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 50 deletions.
170 changes: 165 additions & 5 deletions src/morph/ruleset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ const GOODFile: GOOD = {
location: "Yelan",
lock: true,
},
{
setKey: "MarechausseeHunter",
rarity: 5,
level: 20,
slotKey: "flower",
mainStatKey: "hp",
substats: [
{ key: "eleMas", value: 37 },
{ key: "critRate_", value: 3.1 },
{ key: "enerRech_", value: 21.4 },
{ key: "critDMG_", value: 7.8 },
],
location: "Barbara",
lock: true,
},
],
};

Expand Down Expand Up @@ -219,13 +234,158 @@ test("applyRuleset with single rule that does match should return expected morph
location: "",
lock: true,
},
{
setKey: "MarechausseeHunter",
rarity: 5,
level: 20,
slotKey: "flower",
mainStatKey: "hp",
substats: [
{ key: "eleMas", value: 37 },
{ key: "critRate_", value: 3.1 },
{ key: "enerRech_", value: 21.4 },
{ key: "critDMG_", value: 7.8 },
],
location: "Barbara",
lock: true,
},
],
};
assert.deepEqual(
morphedGOOD,
expectedGOODFile,
"Since the rule doesn't match, the morphed file should be the same"
);
assert.deepEqual(morphedGOOD, expectedGOODFile);
});

test("applyRuleset with 2 rules that both match should return expected morphed GOOD", async () => {
// Arrange
const ruleset: Ruleset = {
name: "test",
rules: [
{
action: {
type: "equip",
to: "Xingqiu",
},
filter: {
characterName: "Yelan",
type: "equippingCharacter",
},
id: 1,
},
{
action: {
type: "equip",
to: "Neuvillette",
},
filter: {
characterName: "Barbara",
type: "equippingCharacter",
},
id: 2,
},
],
};

// Act
const morphedGOOD = applyRuleset(ruleset, GOODFile);

// Assert
const expectedGOODFile: GOOD = {
format: "GOOD",
source: "test",
version: 1,
artifacts: [
{
setKey: "HeartOfDepth",
rarity: 5,
level: 20,
slotKey: "goblet",
mainStatKey: "hydro_dmg_",
substats: [
{ key: "hp_", value: 4.7 },
{ key: "critDMG_", value: 14.8 },
{ key: "hp", value: 538 },
{ key: "critRate_", value: 14.4 },
],
location: "Xingqiu",
lock: true,
},
{
setKey: "EmblemOfSeveredFate",
rarity: 5,
level: 20,
slotKey: "sands",
mainStatKey: "hp_",
substats: [
{ key: "enerRech_", value: 16.8 },
{ key: "atk", value: 16 },
{ key: "eleMas", value: 42 },
{ key: "critRate_", value: 8.9 },
],
location: "Xingqiu",
lock: true,
},
{
setKey: "EmblemOfSeveredFate",
rarity: 5,
level: 20,
slotKey: "circlet",
mainStatKey: "critRate_",
substats: [
{ key: "hp_", value: 15.7 },
{ key: "atk_", value: 10.5 },
{ key: "def_", value: 5.1 },
{ key: "critDMG_", value: 18.7 },
],
location: "Xingqiu",
lock: true,
},
{
setKey: "EmblemOfSeveredFate",
rarity: 5,
level: 20,
slotKey: "plume",
mainStatKey: "atk",
substats: [
{ key: "enerRech_", value: 11.7 },
{ key: "critDMG_", value: 28.8 },
{ key: "hp_", value: 4.1 },
{ key: "hp", value: 448 },
],
location: "Xingqiu",
lock: true,
},
{
setKey: "EmblemOfSeveredFate",
rarity: 5,
level: 20,
slotKey: "flower",
mainStatKey: "hp",
substats: [
{ key: "eleMas", value: 37 },
{ key: "critRate_", value: 3.1 },
{ key: "enerRech_", value: 21.4 },
{ key: "critDMG_", value: 7.8 },
],
location: "Xingqiu",
lock: true,
},
{
setKey: "MarechausseeHunter",
rarity: 5,
level: 20,
slotKey: "flower",
mainStatKey: "hp",
substats: [
{ key: "eleMas", value: 37 },
{ key: "critRate_", value: 3.1 },
{ key: "enerRech_", value: 21.4 },
{ key: "critDMG_", value: 7.8 },
],
location: "Neuvillette",
lock: true,
},
],
};
assert.deepEqual(morphedGOOD, expectedGOODFile);
});

test("validateRuleset returns error when ruleset is not an object", () => {
Expand Down
106 changes: 61 additions & 45 deletions src/morph/ruleset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Artifact, GOOD } from "@/good/good_spec";
import { ActionDefinitionType, actionDefinitionsByType } from "./actions";
import { filterDefinitionsByType } from "./filters";
import { GOOD } from "@/good/good_spec";
import {
ActionDefinitionType,
ActionInstance,
actionDefinitionsByType,
} from "./actions";
import { FilterInstance, filterDefinitionsByType } from "./filters";
import { Rule, validateRule } from "./rule";
import {
ValidationError,
Expand All @@ -10,7 +14,6 @@ import {
createError,
createSuccess,
isBlankString,
isNotBlankString,
isRecord,
} from "./validation";

Expand All @@ -26,34 +29,19 @@ function composePredicatesInOr<PredicateInput>(
predicates.some((predicate) => predicate(predicateInput));
}

function createPredicateFromFilters(filters: Array<Rule["filter"]>) {
const predicates = filters.map((filter) => {
const filterType = filter.type;
// TODO fix this cast
const filterDef =
filterDefinitionsByType[filterType as "equippingCharacter"]!;
return filterDef.predicateFactory(filter);
});
return composePredicatesInOr(predicates);
function createPredicate(filter: FilterInstance) {
const filterType = filter.type;
// TODO fix this cast
const filterDef =
filterDefinitionsByType[filterType as "equippingCharacter"]!;
return filterDef.predicateFactory(filter);
}

function createMutationFromActions(inputActions: Array<Rule["action"]>) {
const mutations = inputActions.map((action) => {
const actionType = action.type;
// TODO: fix this cast
const actionDef =
actionDefinitionsByType[actionType as ActionDefinitionType];
return actionDef.mutationFactory(action);
});

return (artifact: Artifact) =>
mutations.reduce(
(artifact, mutation) => {
artifact = mutation(artifact);
return artifact;
},
{ ...artifact }
);
function createMutation(action: ActionInstance) {
const actionType = action.type;
// TODO: fix this cast
const actionDef = actionDefinitionsByType[actionType as ActionDefinitionType];
return actionDef.mutationFactory(action);
}

export function validateRuleset(ruleset: unknown): ValidationResult<Ruleset> {
Expand Down Expand Up @@ -132,28 +120,21 @@ export function applyRuleset(ruleset: Ruleset, good: GOOD): GOOD {
console.time("applyRuleset");

const { rules } = ruleset;
const editedGood = {
...good,
};
const editedGood = clone(good);

const artifacts = editedGood.artifacts!;

const composedFilter = createPredicateFromFilters(
rules.map((rule) => rule.filter)
);

const composedMutation = createMutationFromActions(
rules.map((rule) => rule.action)
);

// artifacts.filter((art) => art.location === "Yelan").forEach(console.log);

for (let i = 0; i < artifacts.length; i++) {
const ogArtifact = artifacts[i];
// TODO: very WIP code...
if (composedFilter(ogArtifact)) {
const newArtifact = composedMutation(ogArtifact);
artifacts[i] = newArtifact;
// TODO: naive code...
for (const rule of rules) {
const predicate = createPredicate(rule.filter);
const mutation = createMutation(rule.action);
if (predicate(ogArtifact)) {
artifacts[i] = mutation(ogArtifact);
}
}
}

Expand All @@ -167,3 +148,38 @@ export function applyRuleset(ruleset: Ruleset, good: GOOD): GOOD {
console.log("stats", stats);
return editedGood;
}

/**
* From a local bench, recursive seems to be faster than the iterative solution...
* @param value Object to clone
* @returns Cloned value
*/
function clone<T>(value: T): T {
// Handle primitive types and functions directly
if (value === null || typeof value !== "object") {
return value;
}

// Handle Date
if (value instanceof Date) {
return new Date(value.getTime()) as T;
}

// Handle Array
if (Array.isArray(value)) {
return value.map((item) => clone(item)) as T;
}

// Handle Object
if (value instanceof Object) {
const copy: { [key: string]: any } = {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
copy[key] = clone(value[key]);
}
}
return copy as T;
}

throw new Error(`Unsupported type: ${typeof value}`);
}

0 comments on commit 8983d50

Please sign in to comment.