Skip to main content
Glama
answer-randomizer.js17.2 kB
/** * Answer Randomization System * @module content-generation/answer-randomizer * @description Secure randomization to prevent pattern gaming in assessments * @version 5.0.0-alpha */ export class AnswerRandomizer { constructor(config = {}) { this.config = { enableRandomization: true, balancePositions: true, trackDistribution: true, cryptographicSeed: true, minShuffleRounds: 3, maxShuffleRounds: 7, ...config }; // Position distribution tracking this.positionHistory = { total: 0, positions: {} }; // Randomization patterns to avoid this.avoidancePatterns = [ 'consecutive_same_position', 'alphabetical_order', 'reverse_alphabetical', 'predictable_sequence' ]; } /** * Randomize entire assessment * @param {Object} assessment - Assessment package * @returns {Promise<Object>} Assessment with randomized answers */ async randomizeAssessment(assessment) { console.log('[ANSWER-RANDOMIZER] Randomizing assessment answers'); if (!this.config.enableRandomization) { console.log('[ANSWER-RANDOMIZER] Randomization disabled, returning unchanged'); return assessment; } try { const randomized = { ...assessment }; // Randomize quiz questions if (randomized.quiz && randomized.quiz.questions) { randomized.quiz.questions = await this.randomizeQuestions(randomized.quiz.questions); } // Randomize flashcards (if they have options) if (randomized.flashcards) { randomized.flashcards = await this.randomizeFlashcards(randomized.flashcards); } // Update analytics if (randomized.analytics) { randomized.analytics.randomizationApplied = true; randomized.analytics.randomizationTimestamp = new Date().toISOString(); randomized.analytics.positionDistribution = this.getPositionDistribution(); } console.log('[ANSWER-RANDOMIZER] Assessment randomization complete'); return randomized; } catch (error) { console.error('[ANSWER-RANDOMIZER] Randomization failed:', error); throw new Error(`Answer randomization failed: ${error.message}`); } } /** * Randomize quiz questions * @param {Array} questions - Quiz questions * @returns {Promise<Array>} Randomized questions */ async randomizeQuestions(questions) { const randomized = []; for (const question of questions) { const randomizedQuestion = await this.randomizeQuestion(question); randomized.push(randomizedQuestion); } return randomized; } /** * Randomize individual question * @param {Object} question - Question object * @returns {Promise<Object>} Randomized question */ async randomizeQuestion(question) { const randomized = { ...question }; switch (question.type) { case 'multiple_choice': return this.randomizeMultipleChoice(randomized); case 'matching': return this.randomizeMatching(randomized); case 'true_false': // True/false doesn't need answer randomization return randomized; case 'fill_blank': // Fill blank doesn't need answer randomization return randomized; case 'short_answer': // Short answer doesn't need answer randomization return randomized; default: console.warn(`[ANSWER-RANDOMIZER] Unknown question type: ${question.type}`); return randomized; } } /** * Randomize multiple choice question * @param {Object} question - Multiple choice question * @returns {Object} Randomized question */ randomizeMultipleChoice(question) { if (!question.options || question.options.length < 2) { return question; } const randomized = { ...question }; const correctAnswer = question.options[question.correct]; // Perform secure shuffle const shuffled = this.secureShuffleArray([...question.options]); // Find new position of correct answer const newCorrectIndex = shuffled.indexOf(correctAnswer); // Update question with randomized data randomized.options = shuffled; randomized.correct = newCorrectIndex; // Add randomization metadata randomized.randomization = { originalPositions: question.options.map((option, index) => ({ option: option, originalIndex: index, newIndex: shuffled.indexOf(option) })), correctAnswerMovement: { from: question.correct, to: newCorrectIndex }, shuffleTimestamp: new Date().toISOString(), shuffleMethod: 'fisher_yates_secure' }; // Track position distribution this.trackCorrectAnswerPosition(newCorrectIndex, question.options.length); return randomized; } /** * Randomize matching question * @param {Object} question - Matching question * @returns {Object} Randomized question */ randomizeMatching(question) { if (!question.leftColumn || !question.rightColumn) { return question; } const randomized = { ...question }; // Shuffle right column only (left column stays as reference) const originalRightColumn = [...question.rightColumn]; const shuffledRightColumn = this.secureShuffleArray([...question.rightColumn]); // Update correct pairs mapping const newCorrectPairs = question.correctPairs.map(pair => ({ left: pair.left, // Left stays the same right: shuffledRightColumn.indexOf(originalRightColumn[pair.right]) })); randomized.rightColumn = shuffledRightColumn; randomized.correctPairs = newCorrectPairs; // Add randomization metadata randomized.randomization = { rightColumnShuffle: originalRightColumn.map((item, index) => ({ item: item, originalIndex: index, newIndex: shuffledRightColumn.indexOf(item) })), shuffleTimestamp: new Date().toISOString(), shuffleMethod: 'fisher_yates_secure' }; return randomized; } /** * Randomize flashcards (if they have multiple choice elements) * @param {Array} flashcards - Flashcards array * @returns {Promise<Array>} Randomized flashcards */ async randomizeFlashcards(flashcards) { // Flashcards typically don't need answer randomization // But we shuffle the order of flashcards themselves return this.secureShuffleArray([...flashcards]); } /** * Secure array shuffle using Fisher-Yates algorithm * @param {Array} array - Array to shuffle * @returns {Array} Shuffled array */ secureShuffleArray(array) { const shuffled = [...array]; const rounds = this.getRandomShuffleRounds(); // Perform multiple shuffle rounds for extra security for (let round = 0; round < rounds; round++) { for (let i = shuffled.length - 1; i > 0; i--) { // Use cryptographically secure random if available const j = this.getSecureRandomIndex(i + 1); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } } // Verify shuffle quality if (this.isShuffleQualityGood(array, shuffled)) { return shuffled; } else { // Recursively shuffle again if quality is poor return this.secureShuffleArray(array); } } /** * Get secure random index * @param {number} max - Maximum value (exclusive) * @returns {number} Secure random index */ getSecureRandomIndex(max) { if (this.config.cryptographicSeed && typeof crypto !== 'undefined' && crypto.getRandomValues) { // Use cryptographically secure random const array = new Uint32Array(1); crypto.getRandomValues(array); return array[0] % max; } else { // Fallback to Math.random with enhanced seeding return Math.floor(this.getEnhancedRandom() * max); } } /** * Get enhanced random number * @returns {number} Enhanced random number */ getEnhancedRandom() { // Combine multiple entropy sources const time = Date.now(); const performance = typeof performance !== 'undefined' ? performance.now() : 0; const random = Math.random(); // Simple hash function for additional entropy const seed = (time + performance + random * 1000000) % 1000000; return (seed * 9301 + 49297) % 233280 / 233280; } /** * Get random number of shuffle rounds * @returns {number} Number of shuffle rounds */ getRandomShuffleRounds() { const min = this.config.minShuffleRounds; const max = this.config.maxShuffleRounds; return min + Math.floor(Math.random() * (max - min + 1)); } /** * Check shuffle quality * @param {Array} original - Original array * @param {Array} shuffled - Shuffled array * @returns {boolean} Whether shuffle quality is good */ isShuffleQualityGood(original, shuffled) { if (original.length !== shuffled.length) return false; if (original.length < 2) return true; // Check that at least 50% of items changed position let unchangedCount = 0; for (let i = 0; i < original.length; i++) { if (original[i] === shuffled[i]) { unchangedCount++; } } const unchangedRatio = unchangedCount / original.length; return unchangedRatio < 0.5; // Less than 50% unchanged is good } /** * Track correct answer position for distribution analysis * @param {number} position - Position of correct answer * @param {number} totalOptions - Total number of options */ trackCorrectAnswerPosition(position, totalOptions) { if (!this.config.trackDistribution) return; this.positionHistory.total++; if (!this.positionHistory.positions[totalOptions]) { this.positionHistory.positions[totalOptions] = {}; } if (!this.positionHistory.positions[totalOptions][position]) { this.positionHistory.positions[totalOptions][position] = 0; } this.positionHistory.positions[totalOptions][position]++; } /** * Get position distribution statistics * @returns {Object} Position distribution data */ getPositionDistribution() { const distribution = {}; Object.entries(this.positionHistory.positions).forEach(([optionCount, positions]) => { distribution[optionCount] = {}; const total = Object.values(positions).reduce((sum, count) => sum + count, 0); Object.entries(positions).forEach(([position, count]) => { distribution[optionCount][position] = { count: count, percentage: total > 0 ? (count / total * 100).toFixed(2) : 0 }; }); }); return { totalQuestions: this.positionHistory.total, byOptionCount: distribution, generatedAt: new Date().toISOString() }; } /** * Analyze randomization patterns for gaming detection * @param {Array} questions - Questions to analyze * @returns {Object} Pattern analysis */ analyzeRandomizationPatterns(questions) { const analysis = { consecutivePatterns: this.detectConsecutivePatterns(questions), distributionBalance: this.analyzeDistributionBalance(questions), predictabilityScore: this.calculatePredictabilityScore(questions), qualityScore: 0 }; // Calculate overall quality score analysis.qualityScore = this.calculateRandomizationQuality(analysis); return analysis; } /** * Detect consecutive patterns * @param {Array} questions - Questions array * @returns {Object} Pattern detection results */ detectConsecutivePatterns(questions) { const patterns = { consecutiveSame: 0, consecutiveAscending: 0, consecutiveDescending: 0, maxConsecutive: 0 }; let currentStreak = 1; let currentPattern = null; for (let i = 1; i < questions.length; i++) { const prevCorrect = questions[i - 1].correct; const currCorrect = questions[i].correct; if (prevCorrect === currCorrect) { if (currentPattern === 'same') { currentStreak++; } else { currentPattern = 'same'; currentStreak = 2; } patterns.consecutiveSame++; } else if (currCorrect === prevCorrect + 1) { if (currentPattern === 'ascending') { currentStreak++; } else { currentPattern = 'ascending'; currentStreak = 2; } patterns.consecutiveAscending++; } else if (currCorrect === prevCorrect - 1) { if (currentPattern === 'descending') { currentStreak++; } else { currentPattern = 'descending'; currentStreak = 2; } patterns.consecutiveDescending++; } else { currentStreak = 1; currentPattern = null; } patterns.maxConsecutive = Math.max(patterns.maxConsecutive, currentStreak); } return patterns; } /** * Analyze distribution balance * @param {Array} questions - Questions array * @returns {Object} Distribution analysis */ analyzeDistributionBalance(questions) { const distribution = {}; let maxOptions = 0; questions.forEach(question => { if (question.type === 'multiple_choice' && question.options) { const optionCount = question.options.length; maxOptions = Math.max(maxOptions, optionCount); if (!distribution[optionCount]) { distribution[optionCount] = {}; } if (!distribution[optionCount][question.correct]) { distribution[optionCount][question.correct] = 0; } distribution[optionCount][question.correct]++; } }); // Calculate balance scores const balance = {}; Object.entries(distribution).forEach(([optionCount, positions]) => { const total = Object.values(positions).reduce((sum, count) => sum + count, 0); const expected = total / optionCount; const variance = Object.values(positions).reduce((sum, count) => sum + Math.pow(count - expected, 2), 0) / optionCount; balance[optionCount] = { total: total, expected: expected, variance: variance, balanceScore: Math.max(0, 100 - (variance / expected * 100)) }; }); return balance; } /** * Calculate predictability score * @param {Array} questions - Questions array * @returns {number} Predictability score (0-100, lower is better) */ calculatePredictabilityScore(questions) { if (questions.length < 3) return 0; const correctAnswers = questions .filter(q => q.type === 'multiple_choice') .map(q => q.correct); if (correctAnswers.length < 3) return 0; // Check for patterns let patternScore = 0; // Consecutive same answers for (let i = 1; i < correctAnswers.length; i++) { if (correctAnswers[i] === correctAnswers[i - 1]) { patternScore += 10; } } // Arithmetic sequences for (let i = 2; i < correctAnswers.length; i++) { const diff1 = correctAnswers[i - 1] - correctAnswers[i - 2]; const diff2 = correctAnswers[i] - correctAnswers[i - 1]; if (diff1 === diff2 && diff1 !== 0) { patternScore += 15; } } // Overall uniformity (too even distribution) const uniqueAnswers = new Set(correctAnswers).size; const maxPossible = Math.max(...correctAnswers) + 1; if (uniqueAnswers === maxPossible && correctAnswers.length >= maxPossible * 2) { patternScore += 20; } return Math.min(100, patternScore); } /** * Calculate overall randomization quality * @param {Object} analysis - Pattern analysis * @returns {number} Quality score (0-100, higher is better) */ calculateRandomizationQuality(analysis) { let score = 100; // Deduct for consecutive patterns score -= Math.min(30, analysis.consecutivePatterns.maxConsecutive * 5); // Deduct for high predictability score -= analysis.predictabilityScore * 0.5; // Add for good distribution balance const avgBalance = Object.values(analysis.distributionBalance || {}) .reduce((sum, balance) => sum + (balance.balanceScore || 0), 0) / Object.keys(analysis.distributionBalance || {}).length; score += (avgBalance || 0) * 0.2; return Math.max(0, Math.min(100, score)); } /** * Reset position tracking */ resetPositionTracking() { this.positionHistory = { total: 0, positions: {} }; } /** * Export randomization statistics * @returns {Object} Statistics export */ exportStatistics() { return { positionHistory: this.positionHistory, distribution: this.getPositionDistribution(), config: this.config, exportedAt: new Date().toISOString() }; } } // Factory function for creating AnswerRandomizer instances export function createAnswerRandomizer(config = {}) { return new AnswerRandomizer(config); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rkm097git/euconquisto-composer-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server