Shuffles fixed
This commit is contained in:
155
src/exams/Level/Shuffle.ts
Normal file
155
src/exams/Level/Shuffle.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Exercise, FillBlanksExercise, FillBlanksMCOption, MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap, Shuffles, UserSolution } from "@/interfaces/exam";
|
||||
|
||||
export default function shuffleExamExercise(
|
||||
shuffle: boolean | undefined,
|
||||
exercise: Exercise,
|
||||
showSolutions: boolean,
|
||||
userSolutions: UserSolution[],
|
||||
shuffles: Shuffles[],
|
||||
setShuffles: (maps: Shuffles[]) => void
|
||||
): Exercise {
|
||||
if (!shuffle) {
|
||||
return exercise;
|
||||
}
|
||||
const userSolution = userSolutions.find((x) => x.exercise === exercise.id)!;
|
||||
|
||||
if (exercise.type === "multipleChoice") {
|
||||
return shuffleMultipleChoice(exercise, userSolution, shuffles, setShuffles, showSolutions);
|
||||
} else if (exercise.type === "fillBlanks") {
|
||||
return shuffleFillBlanks(exercise, userSolution, shuffles, setShuffles, showSolutions);
|
||||
}
|
||||
|
||||
return exercise;
|
||||
}
|
||||
|
||||
function shuffleMultipleChoice(
|
||||
exercise: MultipleChoiceExercise,
|
||||
userSolution: UserSolution,
|
||||
shuffles: Shuffles[],
|
||||
setShuffles: (maps: Shuffles[]) => void,
|
||||
showSolutions: boolean,
|
||||
): MultipleChoiceExercise {
|
||||
|
||||
if (typeof userSolution.shuffleMaps === "undefined" || (userSolution.shuffleMaps && userSolution.shuffleMaps.length === 0) && !showSolutions) {
|
||||
const newShuffleMaps: ShuffleMap[] = [];
|
||||
exercise.questions = exercise.questions.map(shuffleQuestion(newShuffleMaps));
|
||||
userSolution!.shuffleMaps = newShuffleMaps;
|
||||
setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), {exerciseID: exercise.id, shuffles: newShuffleMaps}]);
|
||||
} else {
|
||||
exercise.questions = exercise.questions.map(retrieveShuffledQuestion(userSolution.shuffleMaps));
|
||||
}
|
||||
|
||||
return exercise;
|
||||
}
|
||||
|
||||
function shuffleQuestion(newShuffleMaps: ShuffleMap[]) {
|
||||
return (question: MultipleChoiceQuestion): MultipleChoiceQuestion => {
|
||||
const options = [...question.options];
|
||||
const shuffledOptions = fisherYatesShuffle(options);
|
||||
|
||||
const optionMapping: Record<string, string> = {};
|
||||
const newOptions = shuffledOptions.map((option, index) => {
|
||||
const newId = String.fromCharCode(65 + index);
|
||||
optionMapping[option.id] = newId;
|
||||
return { ...option, id: newId };
|
||||
});
|
||||
|
||||
newShuffleMaps.push({ questionID: question.id, map: optionMapping });
|
||||
|
||||
return { ...question, options: newOptions, shuffleMap: optionMapping };
|
||||
};
|
||||
}
|
||||
|
||||
function retrieveShuffledQuestion(shuffleMaps: ShuffleMap[]) {
|
||||
return (question: MultipleChoiceQuestion): MultipleChoiceQuestion => {
|
||||
const questionShuffleMap = shuffleMaps.find(map => map.questionID === question.id);
|
||||
if (questionShuffleMap) {
|
||||
const shuffledOptions = Object.entries(questionShuffleMap.map)
|
||||
.sort(([, a], [, b]) => a.localeCompare(b))
|
||||
.map(([originalId, newId]) => {
|
||||
const originalOption = question.options.find(opt => opt.id === originalId);
|
||||
return { ...originalOption, id: newId };
|
||||
});
|
||||
|
||||
return { ...question, options: shuffledOptions, shuffleMap: questionShuffleMap.map };
|
||||
}
|
||||
return question;
|
||||
};
|
||||
}
|
||||
function shuffleFillBlanks(
|
||||
exercise: FillBlanksExercise,
|
||||
userSolution: UserSolution,
|
||||
shuffles: Shuffles[],
|
||||
setShuffles: (maps: Shuffles[]) => void,
|
||||
showSolutions: boolean
|
||||
): FillBlanksExercise {
|
||||
if (typeof userSolution.shuffleMaps === "undefined" || (userSolution.shuffleMaps && userSolution.shuffleMaps.length === 0) && !showSolutions) {
|
||||
const newShuffleMaps: ShuffleMap[] = [];
|
||||
exercise.words = exercise.words.map(shuffleWord(newShuffleMaps));
|
||||
userSolution.shuffleMaps = newShuffleMaps;
|
||||
setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), {exerciseID: exercise.id, shuffles: newShuffleMaps}]);
|
||||
} else {
|
||||
exercise.words = exercise.words.map(retrieveShuffledWord(userSolution.shuffleMaps!));
|
||||
}
|
||||
|
||||
return exercise;
|
||||
}
|
||||
|
||||
function shuffleWord(newShuffleMaps: ShuffleMap[]) {
|
||||
return (word: string | { letter: string; word: string } | FillBlanksMCOption): typeof word => {
|
||||
if (typeof word === 'object' && 'options' in word) {
|
||||
const options = word.options;
|
||||
const originalKeys = Object.keys(options);
|
||||
const shuffledKeys = fisherYatesShuffle(originalKeys);
|
||||
|
||||
const newOptions = shuffledKeys.reduce<typeof options>((acc, key, index) => {
|
||||
acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options];
|
||||
return acc;
|
||||
}, {} as typeof options);
|
||||
|
||||
const optionMapping = originalKeys.reduce<Record<string, string>>((acc, key, index) => {
|
||||
acc[key] = shuffledKeys[index];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
newShuffleMaps.push({ questionID: word.id, map: optionMapping });
|
||||
|
||||
return { ...word, options: newOptions };
|
||||
}
|
||||
return word;
|
||||
};
|
||||
}
|
||||
|
||||
function retrieveShuffledWord(shuffleMaps: ShuffleMap[]) {
|
||||
return (word: string | { letter: string; word: string } | FillBlanksMCOption): typeof word => {
|
||||
if (typeof word === 'object' && 'options' in word) {
|
||||
const shuffleMap = shuffleMaps.find(map => map.questionID === word.id);
|
||||
if (shuffleMap) {
|
||||
const options = word.options;
|
||||
const shuffledOptions = Object.keys(options).reduce<typeof options>((acc, key) => {
|
||||
const shuffledKey = shuffleMap.map[key as keyof typeof options];
|
||||
acc[shuffledKey as keyof typeof options] = options[key as keyof typeof options];
|
||||
return acc;
|
||||
}, {} as typeof options);
|
||||
|
||||
return { ...word, options: shuffledOptions };
|
||||
}
|
||||
}
|
||||
return word;
|
||||
};
|
||||
}
|
||||
|
||||
function fisherYatesShuffle<T>(array: T[]): T[] {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||
return Array.isArray(words) && words.every(
|
||||
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { use, useEffect, useState } from "react";
|
||||
import TextComponent from "./TextComponent";
|
||||
import PartDivider from "./PartDivider";
|
||||
import Timer from "@/components/Medium/Timer";
|
||||
import { Stat } from "@/interfaces/user";
|
||||
import shuffleExamExercise from "./Shuffle";
|
||||
|
||||
interface Props {
|
||||
exam: LevelExam;
|
||||
@@ -40,7 +40,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
const { partIndex, setPartIndex } = useExamStore((state) => state);
|
||||
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
|
||||
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
|
||||
const [shuffleMaps, setShuffleMaps] = useExamStore((state) => [state.shuffleMaps, state.setShuffleMaps])
|
||||
const [shuffles, setShuffles] = useExamStore((state) => [state.shuffles, state.setShuffles])
|
||||
const [currentExercise, setCurrentExercise] = useState<Exercise>();
|
||||
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
|
||||
|
||||
@@ -49,11 +49,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
||||
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (showSolutions && exerciseIndex && exam.shuffle && userSolutions[exerciseIndex].shuffleMaps) {
|
||||
setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[])
|
||||
}
|
||||
}, [showSolutions, exerciseIndex, setShuffleMaps, userSolutions, exam.shuffle])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExamEnded && exerciseIndex === -1) {
|
||||
@@ -69,109 +64,21 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
...exercise,
|
||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||
};
|
||||
|
||||
if (exam.shuffle && exercise.type === "multipleChoice" && !showSolutions) {
|
||||
console.log("Shuffling MC ");
|
||||
const exerciseShuffles = userSolutions[exerciseIndex].shuffleMaps;
|
||||
if (exerciseShuffles && exerciseShuffles.length == 0) {
|
||||
const newShuffleMaps: ShuffleMap[] = [];
|
||||
|
||||
exercise.questions = exercise.questions.map(question => {
|
||||
const options = [...question.options];
|
||||
let shuffledOptions = [...options].sort(() => Math.random() - 0.5);
|
||||
|
||||
const newOptions = options.map((option, index) => ({
|
||||
id: option.id,
|
||||
text: shuffledOptions[index].text
|
||||
}));
|
||||
|
||||
const optionMapping = options.reduce<{ [key: string]: string }>((acc, originalOption) => {
|
||||
const shuffledPosition = newOptions.find(newOpt => newOpt.text === originalOption.text)?.id;
|
||||
if (shuffledPosition) {
|
||||
acc[shuffledPosition] = originalOption.id;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
newShuffleMaps.push({ id: question.id, map: optionMapping });
|
||||
|
||||
return { ...question, options: newOptions };
|
||||
});
|
||||
|
||||
setShuffleMaps(newShuffleMaps);
|
||||
} else {
|
||||
console.log("retrieving MC shuffles");
|
||||
exercise.questions = exercise.questions.map(question => {
|
||||
const questionShuffleMap = shuffleMaps.find(map => map.id === question.id);
|
||||
if (questionShuffleMap) {
|
||||
const newOptions = question.options.map(option => ({
|
||||
id: option.id,
|
||||
text: question.options.find(o => questionShuffleMap.map[o.id] === option.id)?.text || option.text
|
||||
}));
|
||||
return { ...question, options: newOptions };
|
||||
}
|
||||
return question;
|
||||
});
|
||||
}
|
||||
} else if (exam.shuffle && exercise.type === "fillBlanks" && typeCheckWordsMC(exercise.words) && !showSolutions) {
|
||||
if (shuffleMaps.length === 0 && !showSolutions) {
|
||||
const newShuffleMaps: ShuffleMap[] = [];
|
||||
console.log("Shuffling Words");
|
||||
exercise.words = exercise.words.map(word => {
|
||||
if ('options' in word) {
|
||||
const options = { ...word.options };
|
||||
const originalKeys = Object.keys(options);
|
||||
const shuffledKeys = [...originalKeys].sort(() => Math.random() - 0.5);
|
||||
|
||||
const newOptions = shuffledKeys.reduce((acc, key, index) => {
|
||||
acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options];
|
||||
return acc;
|
||||
}, {} as { [key in keyof typeof options]: string });
|
||||
|
||||
const optionMapping = originalKeys.reduce((acc, key, index) => {
|
||||
acc[key as keyof typeof options] = shuffledKeys[index];
|
||||
return acc;
|
||||
}, {} as { [key in keyof typeof options]: string });
|
||||
|
||||
newShuffleMaps.push({ id: word.id, map: optionMapping });
|
||||
|
||||
return { ...word, options: newOptions };
|
||||
}
|
||||
return word;
|
||||
});
|
||||
|
||||
setShuffleMaps(newShuffleMaps);
|
||||
} else {
|
||||
console.log("Retrieving Words shuffle");
|
||||
exercise.words = exercise.words.map(word => {
|
||||
if ('options' in word) {
|
||||
const shuffleMap = shuffleMaps.find(map => map.id === word.id);
|
||||
if (shuffleMap) {
|
||||
const options = { ...word.options };
|
||||
const shuffledOptions = Object.keys(options).reduce((acc, key) => {
|
||||
const shuffledKey = shuffleMap.map[key as keyof typeof options];
|
||||
acc[shuffledKey as keyof typeof options] = options[key as keyof typeof options];
|
||||
return acc;
|
||||
}, {} as { [key in keyof typeof options]: string });
|
||||
|
||||
return { ...word, options: shuffledOptions };
|
||||
}
|
||||
}
|
||||
return word;
|
||||
});
|
||||
|
||||
}
|
||||
if (showSolutions) {
|
||||
setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), { exerciseID: exercise.id, shuffles: userSolutions.find((x) => x.exercise === exercise.id)!.shuffleMaps!}]);
|
||||
} else {
|
||||
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
|
||||
}
|
||||
console.log(exercise);
|
||||
return exercise;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (exerciseIndex !== -1) {
|
||||
setCurrentExercise(getExercise());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [partIndex, exerciseIndex, shuffleMaps, exam.parts[partIndex].context]);
|
||||
}, [partIndex, exerciseIndex, exam.parts[partIndex].context]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -202,7 +109,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
const nextExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
if (solution) {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
||||
}
|
||||
|
||||
/*if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
|
||||
@@ -247,11 +154,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
setHasExamEnded(false);
|
||||
|
||||
if (solution) {
|
||||
let stat = { ...solution, module: "level" as Module, exam: exam.id }
|
||||
if (exam.shuffle) {
|
||||
stat.shuffleMaps = shuffleMaps
|
||||
}
|
||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...stat }]);
|
||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
||||
} else {
|
||||
onFinish(userSolutions);
|
||||
}
|
||||
@@ -260,7 +163,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
const previousExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
if (solution) {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
||||
}
|
||||
|
||||
setExerciseIndex(exerciseIndex - 1);
|
||||
@@ -383,7 +286,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
module="level"
|
||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||
disableTimer={showSolutions || editing}
|
||||
showTimer={typeof exam.parts[0].intro === "undefined"}
|
||||
showTimer={false}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
Reference in New Issue
Block a user