add randomizer module
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s

This commit is contained in:
Shinuwa 2026-08-31 09:10:38 +02:00
parent 07de342fc2
commit 35fe877d58
32 changed files with 3822 additions and 200 deletions

View file

@ -11,8 +11,9 @@ import { createDefaultDraft, getDateSpanDays, getDraftSelection, normalizeDraftA
import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, layoutConcurrentOccurrences } from "../website/src/features/toolboxes/modules/calendarLayout.js";
import { appendChecklistItems, moveChecklistItem, moveChecklistItemToCategory, renameChecklistSection } from "../website/src/features/toolboxes/modules/checklistUtils.js";
import { formatUpdatedAt, isHtmlEmpty, queryBlockFormat, queryCommandState } from "../website/src/features/toolboxes/modules/notepadUtils.js";
import { createBalancedTeams, drawWeightedEntries, filterExclusionsForPlayers, getDiceShape, rollDice } from "../website/src/features/toolboxes/modules/randomizerUtils.js";
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { compactModuleDataForStorage, createToolboxExportPayload, moduleStorageKey, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeRandomizerData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
function createTextImportContext() {
@ -32,6 +33,7 @@ function createTextImportContext() {
normalizeGoalTreeData,
normalizeEquipmentPlannerData,
normalizeImageAnnotationData,
normalizeRandomizerData,
normalizeUrl
};
}
@ -132,6 +134,156 @@ test("text exchange round trips simple toolbox modules and resets checklist prog
assert.deepEqual(memosImport.data.items.map((memo) => [memo.text, memo.color]), [["Route", "#f6c453"], ["Potion", "#22d3ee"]]);
});
test("randomizer helpers roll dice, draw weighted values and balance teams", () => {
assert.equal(getDiceShape(2), "circle");
assert.equal(getDiceShape(3), "triangle");
assert.equal(getDiceShape(6), "square");
assert.equal(getDiceShape(12), "hexagon");
assert.deepEqual(rollDice(6, 3, () => 0), [1, 1, 1]);
assert.deepEqual(rollDice(6, 2, () => 0.99), [6, 6]);
const wheelDraws = drawWeightedEntries([
{ id: "a", label: "A", weight: 1 },
{ id: "b", label: "B", weight: 3 }
], 2, () => 0.9);
assert.deepEqual(wheelDraws.map((entry) => entry.label), ["B", "B"]);
const result = createBalancedTeams([
{ id: "p1", name: "Alice", score: 10 },
{ id: "p2", name: "Bob", score: 8 },
{ id: "p3", name: "Chloé", score: 7 },
{ id: "p4", name: "Dorian", score: 5 }
], 2, [{ id: "e1", playerAId: "p1", playerBId: "p2" }], () => 0.4);
assert.equal(result.ok, true);
assert.equal(result.teams.length, 2);
assert.ok(result.teams.every((team) => team.players.length === 2));
assert.ok(!result.teams.some((team) => team.players.some((player) => player.id === "p1") && team.players.some((player) => player.id === "p2")));
const singleTeam = createBalancedTeams([
{ id: "p1", name: "Bulbizarre", score: 0 },
{ id: "p2", name: "Salamèche", score: 0 },
{ id: "p3", name: "Carapuce", score: 0 }
], 1, [], () => 0.4);
assert.equal(singleTeam.ok, true);
assert.equal(singleTeam.teams.length, 1);
assert.equal(singleTeam.teams[0].players.length, 3);
});
test("randomizer storage compacts values and detects impossible teams", () => {
const normalized = normalizeRandomizerData({
activeTab: "teams",
dice: { faces: 2000, rolls: 200, history: [{ id: "old", at: "2026-08-30T12:00:00.000Z", results: [1, 2, 6] }] },
wheel: { entries: [{ id: "w1", label: "Rare", weight: 1000 }, { label: "", weight: 1 }] },
teams: {
teamCount: 2,
playerSelectionMode: "count",
playerSelectionCount: 3,
players: [
{ id: "p1", name: "Alice", score: 10 },
{ id: "p2", name: "Bob", score: 8 },
{ id: "p3", name: "Chloé", score: 7 }
],
exclusions: [
{ id: "e1", playerAId: "p1", playerBId: "p2", mode: "group" },
{ id: "e2", playerAId: "p2", playerBId: "p1" },
{ id: "e3", playerAId: "p1", playerBId: "missing" }
]
}
});
assert.equal(normalized.dice.faces, 999);
assert.equal(normalized.dice.rolls, 100);
assert.deepEqual(normalized.dice.history[0].results, [1, 2, 6]);
assert.equal(normalized.wheel.entries[0].weight, 999);
assert.equal(normalized.teams.exclusions.length, 1);
assert.deepEqual(normalized.teams.exclusions[0].playerBIds, ["p2"]);
assert.equal(normalized.teams.exclusions[0].mode, "group");
assert.equal(normalized.teams.playerSelectionMode, "count");
assert.equal(normalized.teams.playerSelectionCount, 3);
assert.equal(compactModuleDataForStorage("randomizer", normalizeRandomizerData({})), null);
assert.equal(compactModuleDataForStorage("randomizer", normalized).activeTab, "teams");
const impossible = createBalancedTeams([
{ id: "p1", name: "Alice", score: 10 },
{ id: "p2", name: "Bob", score: 8 },
{ id: "p3", name: "Chloé", score: 7 }
], 2, [
{ id: "e1", playerAId: "p1", playerBId: "p2" },
{ id: "e2", playerAId: "p1", playerBId: "p3" },
{ id: "e3", playerAId: "p2", playerBId: "p3" }
], () => 0.2);
assert.equal(impossible.ok, false);
assert.equal(impossible.error, "constraints_impossible");
const impossibleGroup = createBalancedTeams([
{ id: "p1", name: "Dracolosse", score: 10 },
{ id: "p2", name: "Drattak", score: 9 },
{ id: "p3", name: "Carchacrok", score: 8 },
{ id: "p4", name: "Lanssorien", score: 7 }
], 3, [
{ id: "e1", playerAId: "p1", playerBIds: ["p2", "p3", "p4"], mode: "group" }
], () => 0.2);
assert.equal(impossibleGroup.ok, false);
assert.equal(impossibleGroup.error, "constraints_impossible");
const scopedGroupExclusions = filterExclusionsForPlayers([
{ id: "e1", playerAId: "p1", playerBIds: ["p2", "p3", "p4"], mode: "group" }
], [
{ id: "p3", name: "Carchacrok", score: 8 },
{ id: "p4", name: "Lanssorien", score: 7 },
{ id: "p5", name: "Corvaillus", score: 6 }
]);
assert.equal(scopedGroupExclusions.length, 1);
assert.equal(scopedGroupExclusions[0].playerAId, "p3");
assert.deepEqual(scopedGroupExclusions[0].playerBIds, ["p4"]);
const scopedGroupResult = createBalancedTeams([
{ id: "p3", name: "Carchacrok", score: 8 },
{ id: "p4", name: "Lanssorien", score: 7 },
{ id: "p5", name: "Corvaillus", score: 6 }
], 2, scopedGroupExclusions, () => 0.2);
assert.equal(scopedGroupResult.ok, true);
assert.ok(!scopedGroupResult.teams.some((team) => team.players.some((player) => player.id === "p3") && team.players.some((player) => player.id === "p4")));
});
test("randomizer text exchange round trips full configuration", () => {
const context = createTextImportContext();
const randomizer = normalizeRandomizerData({
activeTab: "wheel",
dice: { faces: 6, rolls: 3 },
wheel: {
draws: 2,
entries: [
{ id: "w1", label: "Route farm", weight: 3 },
{ id: "w2", label: "Boss rare", weight: 1 }
]
},
teams: {
teamCount: 2,
playerSelectionMode: "count",
playerSelectionCount: 3,
players: [
{ id: "p1", name: "Alice", score: 10 },
{ id: "p2", name: "Bob", score: 8 },
{ id: "p3", name: "Chloé", score: 7 }
],
exclusions: [{ id: "e1", playerAId: "p1", playerBIds: ["p2", "p3"], mode: "group" }]
}
});
const imported = importModuleText("randomizer", exportModuleText("randomizer", randomizer), context);
assert.equal(imported.ok, true);
assert.equal(imported.data.activeTab, "wheel");
assert.deepEqual(imported.data.wheel.entries.map((entry) => [entry.label, entry.weight]), [["Route farm", 3], ["Boss rare", 1]]);
assert.equal(imported.data.teams.playerSelectionMode, "count");
assert.equal(imported.data.teams.playerSelectionCount, 3);
assert.deepEqual(imported.data.teams.players.map((player) => [player.name, player.score]), [["Alice", 10], ["Bob", 8], ["Chloé", 7]]);
assert.equal(imported.data.teams.exclusions.length, 1);
assert.equal(imported.data.teams.exclusions[0].mode, "group");
assert.deepEqual(
imported.data.teams.exclusions[0].playerBIds.map((playerId) => imported.data.teams.players.find((player) => player.id === playerId)?.name),
["Bob", "Chloé"]
);
});
test("notepad helpers keep empty html and invalid command states stable", () => {
assert.equal(isHtmlEmpty("<p><br></p>", ""), true);
assert.equal(isHtmlEmpty("<p>&nbsp;</p>", ""), true);
@ -673,6 +825,40 @@ test("toolbox export remaps module order ids", () => {
assert.deepEqual(normalizeToolbox(payload.toolbox).moduleOrder, payload.toolbox.moduleOrder);
});
test("toolbox export remaps randomizer multi-target exclusions", () => {
const payload = createToolboxExportPayload(
{
id: "toolbox1",
name: "Randomizer",
modules: [{ id: "module1", type: "randomizer" }],
moduleOrder: {
one: ["module1"],
two: [["module1"], []]
}
},
{
[moduleStorageKey("toolbox1", "module1")]: normalizeRandomizerData({
teams: {
players: [
{ id: "p1", name: "Dracolosse" },
{ id: "p2", name: "Drattak" },
{ id: "p3", name: "Carchacrok" },
{ id: "p4", name: "Lanssorien" }
],
exclusions: [{ id: "e1", playerAId: "p1", playerBIds: ["p2", "p3", "p4"], mode: "group" }]
}
})
}
);
const moduleData = Object.values(payload.modules)[0];
assert.equal(moduleData.teams.exclusions.length, 1);
assert.equal(moduleData.teams.exclusions[0].mode, "group");
assert.equal(moduleData.teams.exclusions[0].playerBIds.length, 3);
assert.ok(!moduleData.teams.exclusions[0].playerBIds.includes(undefined));
assert.ok(moduleData.teams.exclusions[0].playerBIds.every((playerId) => playerId !== "p2" && playerId !== "p3" && playerId !== "p4"));
});
test("grouped reorder helpers group flat items and complete group order", () => {
const items = [
{ id: "a", category: "" },