Skip to content

Commit

Permalink
feat: support multiple correct answers
Browse files Browse the repository at this point in the history
  • Loading branch information
gmickel committed Jul 25, 2024
1 parent a8501ee commit f8cb843
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 159 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Welcome to CodeQuest, the quiz game that's more addictive than trying to fix a b

- 🧠 **Dynamic Question Loading**: Questions appear like magic, no rabbit (or developer) required!
- ⏱️ **Time-based Challenges**: Race against the clock! (It's totally cool to pretend you're defusing a bomb)
- 🔀 Flexible Answer Options: Support for both single and multiple correct answers in your questions
- 🏋️ **Difficulty Levels**: From "I just learned to code" to "I dream in binary"
- 🎨 **Snazzy Code Highlighting**: Making your code snippets look prettier than a sunset
- 🔊 **Sound Effects**: Auditory dopamine hits for your correct answers!
Expand All @@ -22,13 +23,14 @@ Welcome to CodeQuest, the quiz game that's more addictive than trying to fix a b
- 📝 **Markdown Support**: Write questions and answers in Markdown for easy formatting
- 🔥 **Code Snippets**: Include code snippets to test your knowledge
- 🖼️ **Images**: Include images to spice up your questions
- 🧩 **Multiple Choice**: Create questions with multiple answer options
- 🧩 Multiple Choice: Create questions with single or multiple correct answers
- 📚 Explanations: Give detailed explanations for answers, including all correct options for multiple-answer questions
-**Time Limits**: Set time limits for each question to keep players on their toes
- 🔢 **Difficulty Levels**: Assign difficulty levels to questions
- 💡 **Hints**: Provide hints for when players get stuck
- 📚 **Explanations**: Give detailed explanations for answers to enhance learning

Check out `example-question.md` for a comprehensive example of all these features in action!
Check out `example-question.md` for a comprehensive example of all these features in action! `example-question-two.md` shows how to create a question with multiple correct answers.

## 🧙‍♂️ Question Generation and Management (Let AI Do the Heavy Lifting)

Expand Down Expand Up @@ -179,6 +181,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file

- A big, cosmic thank you to [OpenEHR Quest](https://github.com/gmickel/openehr-quest) for being the primordial soup from which CodeQuest evolved!
- Shoutout to caffeine, the true MVP of this project
- High-five to our awesome community for suggesting and helping implement the multiple correct answers feature!
- And finally, thanks to you, for reading this far. You're the real hero! 🦸‍♂️

Now go forth and create quizzes that'll make people forget about doomscrolling! 🚀🧠🎉
13 changes: 7 additions & 6 deletions src/components/Quiz/Answers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,31 @@ import React from 'react';
import { Button } from '@/components/ui/button';
import RenderContent from '@/components/RenderContent';
import { cn } from '@/lib/utils';
import type { ParsedQuestion } from '@/lib/markdownParser';

interface AnswersProps {
answers: string[];
question: ParsedQuestion;
onAnswer: (index: number) => void;
disabled: boolean;
selectedAnswer: number | null;
correctAnswer: number;
answered: boolean;
}

const Answers: React.FC<AnswersProps> = ({
answers,
question,
onAnswer,
disabled,
selectedAnswer,
correctAnswer,
answered,
}) => {
const correctAnswers = question.correctAnswers || [question.correctAnswer];

return (
<div className="space-y-4 mt-4">
<h2 className="text-lg font-semibold">Answers</h2>
{answers.map((answer, index) => {
{question.answers.map((answer, index) => {
const isSelected = selectedAnswer === index;
const isCorrect = correctAnswer - 1 === index;
const isCorrect = correctAnswers.includes(index + 1);

let buttonClass = '';
if (answered) {
Expand Down
49 changes: 34 additions & 15 deletions src/components/Quiz/Result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,53 @@ import React from 'react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import RenderContent from '@/components/RenderContent';
import type { ParsedQuestion } from '@/lib/markdownParser';
import { isCorrectAnswer } from '@/lib/quiz-utils';

interface ResultProps {
isCorrect: boolean;
explanation: string;
question: ParsedQuestion;
selectedAnswer: number | null;
isLastQuestion: boolean;
onNext: () => void;
}

const Result: React.FC<ResultProps> = ({ isCorrect, explanation, isLastQuestion, onNext }) => {
const Result: React.FC<ResultProps> = ({ question, selectedAnswer, isLastQuestion, onNext }) => {
const isCorrect = selectedAnswer !== null && isCorrectAnswer(question, selectedAnswer);
const correctAnswers = question.correctAnswers || [question.correctAnswer];

return (
<>
<Alert className={`mt-4 ${isCorrect ? 'bg-green-100' : 'bg-red-100'}`}>
<AlertTitle>{isCorrect ? 'Correct!' : 'Incorrect'}</AlertTitle>
<AlertDescription>
<RenderContent content={explanation} />
{correctAnswers.length === 1 && (
<p className="mb-4">
The correct answer is:
{' '}
{correctAnswers.map(a => `Answer ${a}`).join(', ')}
</p>
)}

{correctAnswers.length > 1 && (
<p className="mb-4">
The correct answers are:
{' '}
{correctAnswers.map(a => `Answer ${a}`).join(', ')}
</p>
)}
{selectedAnswer !== null
? (
''
)
: (
<p>You didn't select an answer</p>
)}
<RenderContent content={question.explanation} />
</AlertDescription>
</Alert>
{isLastQuestion
? (
<Button onClick={onNext} className="w-full mt-4">
Show Results
</Button>
)
: (
<Button onClick={onNext} className="w-full mt-4">
Next Level
</Button>
)}
<Button onClick={onNext} className="w-full mt-4">
{isLastQuestion ? 'Show Results' : 'Next Level'}
</Button>
</>
);
};
Expand Down
24 changes: 18 additions & 6 deletions src/components/Quiz/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import HealthBar from '@/components/Quiz/HealthBar';
import useSoundEffects from '@/hooks/useSoundEffects';
import type { ParsedQuestion } from '@/lib/markdownParser.ts';
import { loadAllQuestions } from '@/lib/markdownParser.ts';
import { isCorrectAnswer } from '@/lib/quiz-utils';
import { renderContent } from '@/components/RenderContent';
import { config } from '@/config';
import './styles.css';
Expand Down Expand Up @@ -67,7 +68,7 @@ const Quiz: React.FC = () => {
const handleAnswer = useCallback((selectedIndex: number) => {
setSelectedAnswer(selectedIndex);
const currentQuestion = questions[gameState.currentLevel];
const isCorrect = selectedIndex === currentQuestion.correctAnswer - 1;
const isCorrect = isCorrectAnswer(currentQuestion, selectedIndex);
const isTimedOut = selectedIndex === -1;

setGameState((prevState) => {
Expand All @@ -92,7 +93,19 @@ const Quiz: React.FC = () => {
}

if (isTimedOut) {
setSelectedAnswer(currentQuestion.correctAnswer - 1);
// Handle timed out scenario for both single and multiple correct answers
if (currentQuestion.correctAnswer !== undefined) {
setSelectedAnswer(currentQuestion.correctAnswer - 1);
}
else if (currentQuestion.correctAnswers !== undefined && currentQuestion.correctAnswers.length > 0) {
// If multiple correct answers, select the first one
setSelectedAnswer(currentQuestion.correctAnswers[0] - 1);
}
else {
// Fallback if no correct answer is defined (shouldn't happen, but just in case)
console.error('No correct answer defined for the current question');
setSelectedAnswer(null);
}
}
}, [gameState.currentLevel, questions, soundEnabled, playCorrectSound, playWrongSound]);

Expand Down Expand Up @@ -317,17 +330,16 @@ const Quiz: React.FC = () => {
<Card className="w-full max-w-4xl mt-4">
<CardContent>
<Answers
answers={currentQuestion.answers}
question={currentQuestion}
onAnswer={handleAnswer}
disabled={gameState.answerSelected}
selectedAnswer={selectedAnswer}
correctAnswer={currentQuestion.correctAnswer}
answered={gameState.answerSelected}
/>
{gameState.answerSelected && (
<Result
isCorrect={gameState.isCorrect}
explanation={currentQuestion.explanation}
question={currentQuestion}
selectedAnswer={selectedAnswer}
isLastQuestion={isLastQuestion}
onNext={nextLevel}
/>
Expand Down
4 changes: 3 additions & 1 deletion src/lib/markdownParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export interface ParsedQuestion {
outro: string;
};
answers: string[];
correctAnswer: number;
correctAnswer?: number;
correctAnswers?: number[];
timeLimit?: number;
difficulty?: string;
explanation: string;
Expand Down Expand Up @@ -112,6 +113,7 @@ export function parseQuestionContent(content: string): ParsedQuestion {
description: frontMatter.description,
level: frontMatter.level,
correctAnswer: frontMatter.correctAnswer,
correctAnswers: frontMatter.correctAnswers,
timeLimit: frontMatter.timeLimit,
difficulty: frontMatter.difficulty,
...parsedContent,
Expand Down
11 changes: 11 additions & 0 deletions src/lib/quiz-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { ParsedQuestion } from './markdownParser';

export function isCorrectAnswer(question: ParsedQuestion, selectedIndex: number): boolean {
if (question.correctAnswer !== undefined) {
return selectedIndex + 1 === question.correctAnswer;
}
if (question.correctAnswers !== undefined) {
return question.correctAnswers.includes(selectedIndex + 1);
}
throw new Error('Question has no correct answer defined');
}
137 changes: 12 additions & 125 deletions src/questions/example-question-two.md
Original file line number Diff line number Diff line change
@@ -1,141 +1,28 @@
---
title: The Ultimate Coding Challenge
description: Test your programming skills across various languages and concepts!
level: 2 # Questions will be sorted by level
correctAnswer: 2
difficulty: "Expert" # Beginner, Intermediate, or Expert (optional)
title: The Integration Inquisitor
description: The Integration Inquisitor will test your knowledge of posting compositions to an OpenEHR system.
level: 2
correctAnswers: [2, 4]
timeLimit: 5
---

## Context

### Introduction

Welcome to the Ultimate Coding Challenge! This quest will test your knowledge of **programming concepts, algorithms, and problem-solving skills**. Are you ready to prove your coding prowess?

Test of code highlighting:

inline code: `console.log('Hello, Alice!');`

```python
def decorator(func):
def wrapper(*args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
return wrapper
```

### Question

You're tasked with implementing a function to find the most frequent element in an array. The function should work efficiently for large arrays. Consider the following requirements:

1. The function should handle arrays of integers.
2. If there are multiple elements with the same highest frequency, return any one of them.
3. The function should have a time complexity better than O(n^2).

```go
func mostFrequent(arr []int) int {
maxCount := 0
result := arr[0]
for i := range arr {
count := 0
for j := range arr {
if arr[i] == arr[j] {
count++
}
}
if count > maxCount {
maxCount = count
result = arr[i]
}
}
return result
}
```

How would you implement this function?

### Outro

__Efficient__ array manipulation and understanding of data structures are crucial skills for any programmer. This problem tests your ability to balance time complexity with code readability.
Which HTTP method and content type should you use when posting a composition to an OpenEHR system?

## Answers

- Use nested loops to count occurrences of each element:

```python
def most_frequent(arr):
max_count = 0
result = arr[0]
for i in range(len(arr)):
count = 0
for j in range(len(arr)):
if arr[i] == arr[j]:
count += 1
if count > max_count:
max_count = count
result = arr[i]
return result
```

- Use a hash map to count occurrences in a single pass:

```python
from collections import defaultdict

def most_frequent(arr):
count = defaultdict(int)
for num in arr:
count[num] += 1
return max(count, key=count.get)
```

- Sort the array and count consecutive elements:

```python
def most_frequent(arr):
arr.sort()
max_count = 1
res = arr[0]
curr_count = 1
for i in range(1, len(arr)):
if arr[i] == arr[i-1]:
curr_count += 1
else:
if curr_count > max_count:
max_count = curr_count
res = arr[i-1]
curr_count = 1
return res
```

- Use a set to eliminate duplicates, then count occurrences:

```python
def most_frequent(arr):
return max(set(arr), key=arr.count)
```
- GET request with application/json content type
- POST request with application/xml content type
- PUT request with text/plain content type
- POST request with application/json content type

## Explanation

The correct answer is option 2: Using a hash map to count occurrences in a single pass.

This solution is optimal because:

1. It has a time complexity of O(n), where n is the length of the array.
2. It only requires a single pass through the array.
3. The space complexity is O(k), where k is the number of unique elements in the array.

Here's a breakdown of how the function works:

1. `defaultdict(int)` creates a dictionary where any new key is automatically initialized with a value of 0.
2. The loop `for num in arr:` iterates through each element in the array.
3. `count[num] += 1` increments the count for each number.
4. `max(count, key=count.get)` returns the key (number) with the highest count.

This solution efficiently handles large arrays and meets all the specified requirements.
When posting a new composition to an OpenEHR system, you typically use a POST request with application/json or application/xml content type. This allows you to send structured data in a format that's widely supported and easy to work with.

## Hint

Think about data structures that allow for fast lookups and updates. How can you keep track of counts while only passing through the array once?
Think about which HTTP method is typically used for creating new resources.
4 changes: 0 additions & 4 deletions src/questions/example-question.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,12 @@ def most_frequent(arr):

## Explanation

The correct answer is option 2: Using a hash map to count occurrences in a single pass.

This solution is optimal because:

1. It has a time complexity of O(n), where n is the length of the array.
2. It only requires a single pass through the array.
3. The space complexity is O(k), where k is the number of unique elements in the array.

Here's a breakdown of how the function works:

1. `defaultdict(int)` creates a dictionary where any new key is automatically initialized with a value of 0.
2. The loop `for num in arr:` iterates through each element in the array.
3. `count[num] += 1` increments the count for each number.
Expand Down

0 comments on commit f8cb843

Please sign in to comment.