Skip to content

Commit

Permalink
move chooseTechniques algorithm to core
Browse files Browse the repository at this point in the history
  • Loading branch information
nknapp committed Apr 4, 2024
1 parent 3d91761 commit e0e72f2
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Component } from "solid-js";
import type { TechniqueFilters } from "$core/techniqueFilter/technique-filters.ts";
import type { TechniqueFilters } from "$core/chooseTechniques";
import { t } from "@/i18n";
import { CheckButton } from "@/components/solid/CheckButton.tsx";

Expand All @@ -12,7 +12,7 @@ export const ChooserControlFilters: Component<ChooserControlFiltersProps> = (pro
return (
<CheckButton
text={t("examChooser.filters.kneeFriendly")}
value={props.value.kneeFriendly}
value={props.value.kneeFriendly ?? false}
onChange={(value) => props.onChange({ kneeFriendly: value })}
/>
);
Expand Down
8 changes: 4 additions & 4 deletions src/components/solid/TechniqueChooser/ChooserControlOrder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import { CheckButton } from "@/components/solid/CheckButton.tsx";
import { nanoid } from "nanoid";
import { IconRefresh } from "@/icons";

export interface OrderOptions {
export interface ChoosableOrderOptions {
randomize: boolean;
includePercent: number;
}

export interface ChooserControlOrderProps {
value: OrderOptions;
onChange: (value: OrderOptions) => void;
value: ChoosableOrderOptions;
onChange: (value: ChoosableOrderOptions) => void;
onForceRefresh: () => void;
}

export const ChooserControlOrder: Component<ChooserControlOrderProps> = (props) => {
function update<T extends keyof OrderOptions>(prop: T, value: OrderOptions[T]): void {
function update<T extends keyof ChoosableOrderOptions>(prop: T, value: ChoosableOrderOptions[T]): void {
props.onChange({
...props.value,
[prop]: value,
Expand Down
40 changes: 21 additions & 19 deletions src/components/solid/TechniqueChooser/TechniqueChooser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { type Component, createDeferred, createSignal } from "solid-js";
import type { ResolvedDojo } from "$core/model/Dojo.ts";
import { t } from "@/i18n";
import { ExamSheet } from "@/components/solid/TechniqueChooser/ExamSheet.tsx";
import { resolveExamTables } from "$core/resolveExamTables";
import { groupTechniques } from "$core/groupTechniques/groupTechniques.ts";
import { ChooserControlButtons, type Option } from "@/components/solid/TechniqueChooser/ChooserControlButtons.tsx";
import type { Exam } from "$core/model";
import { syncToStorage } from "@/components/solid/syncToStorage.ts";
import { isServer } from "solid-js/web";
import { type TechniqueFilters, techniquePredicate } from "$core/techniqueFilter/technique-filters.ts";
import { ChooserControlContainer } from "@/components/solid/TechniqueChooser/ChooserControlContainer.tsx";
import { ChooserControlFilters } from "@/components/solid/TechniqueChooser/ChooserControlFilters.tsx";
import { ChooserControlOrder, type OrderOptions } from "@/components/solid/TechniqueChooser/ChooserControlOrder.tsx";
import { shuffleTechniques } from "$core/shuffleTechniques";
import {
ChooserControlOrder,
type ChoosableOrderOptions,
} from "@/components/solid/TechniqueChooser/ChooserControlOrder.tsx";
import { chooseTechniques, type TechniqueFilters } from "$core/chooseTechniques";

export const TechniqueChooser: Component<{ dojo: ResolvedDojo }> = (props) => {
const [examSelection, setExamSelection] = syncToStorage(createSignal(new Set<string>()), {
Expand All @@ -27,27 +27,29 @@ export const TechniqueChooser: Component<{ dojo: ResolvedDojo }> = (props) => {
storage: isServer ? global.localStorage : sessionStorage,
});

const [order, setOrder] = syncToStorage(createSignal<OrderOptions>({ randomize: true, includePercent: 80 }), {
name: "orderOptions::" + props.dojo.info.id,
storage: isServer ? global.localStorage : sessionStorage,
});
const [order, setOrder] = syncToStorage(
createSignal<ChoosableOrderOptions>({ randomize: true, includePercent: 80 }),
{
name: "orderOptions:" + props.dojo.info.id,
storage: isServer ? global.localStorage : sessionStorage,
},
);

const [refreshForced, setRefreshForced] = createSignal(false);
function forceRefresh() {
setRefreshForced((oldValue) => !oldValue);
}
const selectedTechniques = createDeferred(() => {
// Make force refresh a dependency
// Make force refresh a dependency, so that he "shuffle" button works
refreshForced();
const exams = props.dojo.details.exams.filter((exam) => examSelection().has(exam.id));
const allTechniques = resolveExamTables(exams);

const filteredTechniques = allTechniques.filter(techniquePredicate(selectedFilters()));
const orderedTechniques = order().randomize
? shuffleTechniques(filteredTechniques, { coverage: order().includePercent / 100 })
: filteredTechniques;

return groupTechniques(orderedTechniques, { orderExecutions: true });
return chooseTechniques(props.dojo.details.exams, {
order: {
...order(),
orderExecutions: true,
},
selectedExams: examSelection(),
filters: selectedFilters(),
});
});

return (
Expand Down
141 changes: 141 additions & 0 deletions src/core/chooseTechniques/chooseTechniques.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { chooseTechniques } from "$core/chooseTechniques/chooseTechniques.ts";
import { createExam } from "$core/model/Exam.test-helper.ts";
import { buildExamTable } from "$core/buildExamTable";
import { createTechnique, tqs } from "$core/model/Technique.test-helper.ts";
import { assertMock } from "$core/test-utils/assertMock.ts";
import { shuffleList } from "$core/utils/shuffleList.ts";
import type { Technique } from "$core/model";

const SUWARI_IKKYO_OMOTE = createTechnique("suwari waza", "ai hanmi katate dori", "ikkyo", "omote");
const SUWARI_IKKYO_URA = createTechnique("suwari waza", "ai hanmi katate dori", "ikkyo", "ura");
const HANMI_IKKYO_OMOTE = createTechnique("hanmi handachi waza", "ai hanmi katate dori", "ikkyo", "omote");
const HANMI_IKKYO_URA = createTechnique("hanmi handachi waza", "ai hanmi katate dori", "ikkyo", "ura");
const TACHI_IKKYO_OMOTE = createTechnique("tachi waza", "ai hanmi katate dori", "ikkyo", "omote");
const TACHI_IKKYO_URA = createTechnique("tachi waza", "ai hanmi katate dori", "ikkyo", "ura");
const exams = [
createExam({
id: "kyu5",
techniques: buildExamTable([SUWARI_IKKYO_OMOTE, SUWARI_IKKYO_URA]),
}),
createExam({
id: "kyu4",
techniques: buildExamTable([HANMI_IKKYO_OMOTE, HANMI_IKKYO_URA]),
}),
createExam({
id: "kyu3",
techniques: buildExamTable([TACHI_IKKYO_OMOTE, TACHI_IKKYO_URA]),
}),
];

describe("chooseTechniques", () => {
beforeEach(() => {
assertMock(shuffleList);
shuffleList.mockImplementation((list: Technique[]) => {
return list.toReversed();
});
});

it("returns an empty array if called without any option", () => {
expect(chooseTechniques(exams, {})).toHaveLength(0);
});

it("returns all techniques from the selected exams", () => {
expect(tqs(chooseTechniques(exams, { selectedExams: new Set(["kyu5", "kyu4"]) }))).toEqual(
tqs([SUWARI_IKKYO_OMOTE, SUWARI_IKKYO_URA, HANMI_IKKYO_OMOTE, HANMI_IKKYO_URA]),
);
});

it("applies filters", () => {
expect(
tqs(
chooseTechniques(exams, {
selectedExams: new Set(["kyu5", "kyu4", "kyu3"]),
filters: {
kneeFriendly: true,
},
}),
),
).toEqual(tqs([TACHI_IKKYO_OMOTE, TACHI_IKKYO_URA]));
});

it("applies randomization", () => {
expect(
tqs(
chooseTechniques(exams, {
selectedExams: new Set(["kyu5", "kyu4", "kyu3"]),
order: {
randomize: true,
},
}),
),
).toEqual(
tqs([
SUWARI_IKKYO_OMOTE,
SUWARI_IKKYO_URA,
HANMI_IKKYO_OMOTE,
HANMI_IKKYO_URA,
TACHI_IKKYO_OMOTE,
TACHI_IKKYO_URA,
]).toReversed(),
);
});

it("applies cutoff after randomization", () => {
expect(
tqs(
chooseTechniques(exams, {
selectedExams: new Set(["kyu5", "kyu4", "kyu3"]),
order: {
randomize: true,
includePercent: 50,
},
}),
),
).toEqual(tqs([TACHI_IKKYO_URA, TACHI_IKKYO_OMOTE, HANMI_IKKYO_URA]));
});

it("groups techniques by execution", () => {
const exams = [
createExam({
id: "kyu5",
techniques: buildExamTable([SUWARI_IKKYO_OMOTE, HANMI_IKKYO_OMOTE]),
}),
createExam({
id: "kyu4",
techniques: buildExamTable([SUWARI_IKKYO_URA, HANMI_IKKYO_URA]),
}),
];
expect(
tqs(
chooseTechniques(exams, {
selectedExams: new Set(["kyu5", "kyu4"]),
}),
),
).toEqual(tqs([SUWARI_IKKYO_OMOTE, SUWARI_IKKYO_URA, HANMI_IKKYO_OMOTE, HANMI_IKKYO_URA]));
});

it("groups techniques by execution when randomized", () => {
const exams = [
createExam({
id: "kyu5",
techniques: buildExamTable([SUWARI_IKKYO_OMOTE, HANMI_IKKYO_OMOTE]),
}),
createExam({
id: "kyu4",
techniques: buildExamTable([SUWARI_IKKYO_URA, HANMI_IKKYO_URA]),
}),
];
expect(
tqs(
chooseTechniques(exams, {
selectedExams: new Set(["kyu5", "kyu4"]),
order: {
randomize: true,
includePercent: 100,
orderExecutions: true,
},
}),
),
).toEqual(tqs([SUWARI_IKKYO_URA, SUWARI_IKKYO_OMOTE, HANMI_IKKYO_URA, HANMI_IKKYO_OMOTE]));
});
});
32 changes: 32 additions & 0 deletions src/core/chooseTechniques/chooseTechniques.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Exam, Technique } from "$core/model";
import { resolveExamTables } from "$core/resolveExamTables";
import { type TechniqueFilters, techniquePredicate } from "./techniqueFilter";
import { shuffleTechniques } from "$core/shuffleTechniques";
import { groupTechniques } from "$core/groupTechniques/groupTechniques.ts";

export interface ChooseTechniqueOptions {
selectedExams?: Set<string>;
filters?: TechniqueFilters;
order?: OrderOptions;
}

export interface OrderOptions {
randomize: boolean;
includePercent?: number;
orderExecutions?: boolean;
}

export function chooseTechniques(allExams: readonly Exam[], options: ChooseTechniqueOptions): Technique[] {
const selectedExams = options.selectedExams ?? new Set();
const filters = options.filters ?? {};
const randomize = options.order?.randomize ?? false;
const includePercent = options.order?.includePercent ?? 100;
const orderExecutions = options.order?.orderExecutions ?? false;

let result = resolveExamTables(allExams.filter((exam) => selectedExams.has(exam.id)));
result = result.filter(techniquePredicate(filters));
if (randomize) {
result = shuffleTechniques(result, { includePercent });
}
return groupTechniques(result, { orderExecutions });
}
2 changes: 2 additions & 0 deletions src/core/chooseTechniques/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./chooseTechniques";
export type { TechniqueFilters } from "./techniqueFilter";
1 change: 1 addition & 0 deletions src/core/chooseTechniques/techniqueFilter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./technique-filters";
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ const techniqueFilterSpec = {

export type TechniqueFilterName = keyof typeof techniqueFilterSpec;

export type TechniqueFilters = Record<TechniqueFilterName, boolean>;
export type TechniqueFilters = Partial<Record<TechniqueFilterName, boolean>>;

export function techniquePredicate(filters: TechniqueFilters): (technique: Technique) => boolean {
// This can be refactored to be generic based on the techniqueFilterSpec, but that does not make sense
// with just one filter
return (technique) => {
return !filters["kneeFriendly"] || techniqueFilterSpec.kneeFriendly.filter(technique);
};
Expand Down
6 changes: 6 additions & 0 deletions src/core/model/Technique.test-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ export function techniqueComponentsAsString(
export function assertEqualTechniques(actual: Technique[], expected: Technique[]) {
expect(actual.map(techniqueAsString)).toEqual(expected.map(techniqueAsString));
}

export function techniquesAsString(techniques: Technique[]): string[] {
return techniques.map(techniqueAsString);
}

export const tqs = techniquesAsString;
4 changes: 2 additions & 2 deletions src/core/shuffleTechniques/shuffleTechniques.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("shuffleTechniques", () => {
];

it("only returns the requested coverage of techniques", () => {
assertEqualTechniques(shuffleTechniques(techniques, { coverage: 0.5 }), [
assertEqualTechniques(shuffleTechniques(techniques, { includePercent: 50 }), [
createTechnique("tachi waza", "kata dori", "shiho nage", "ura"),
createTechnique("suwari waza", "ai hanmi katate dori", "ikkyo", "omote"),
createTechnique("tachi waza", "kata dori", "irimi nage", "ura"),
Expand All @@ -25,7 +25,7 @@ describe("shuffleTechniques", () => {
it("applies randomization", () => {
assertMock(shuffleList);
shuffleList.mockImplementation((list) => reverse(list));
assertEqualTechniques(shuffleTechniques(techniques, { coverage: 0.5 }), [
assertEqualTechniques(shuffleTechniques(techniques, { includePercent: 50 }), [
createTechnique("tachi waza", "kata dori", "shiho nage", "omote"),
createTechnique("suwari waza", "ai hanmi katate dori", "ikkyo", "ura"),
createTechnique("suwari waza", "katate ryote dori", "shiho nage", "ura"),
Expand Down
4 changes: 2 additions & 2 deletions src/core/shuffleTechniques/shuffleTechniques.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { shuffleList } from "$core/utils/shuffleList.ts";
import { type Technique } from "$core/model";

export function shuffleTechniques(techniques: Technique[], { coverage = 0.8 } = {}): Technique[] {
export function shuffleTechniques(techniques: Technique[], { includePercent = 100 } = {}): Technique[] {
const shuffledTechniques: Technique[] = shuffleList(techniques);
const sliceEnd = Math.ceil(coverage * techniques.length);
const sliceEnd = Math.ceil((includePercent * techniques.length) / 100);
return shuffledTechniques.slice(0, sliceEnd);
}
1 change: 0 additions & 1 deletion src/core/techniqueFilter/index.ts

This file was deleted.

0 comments on commit e0e72f2

Please sign in to comment.