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 = {}; 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((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>((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((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(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 ); }