interactives/src/components/guessing-game/QuestionGuessingMode.tsx
gpt-engineer-app[bot] 62c6acc388 Refine guessing game logic
Update the guessing game to handle user-provided questions and optimize computer-generated questions. The game will now grey out numbers divisible by 10 based on user input. Additionally, the computer will prioritize questions that maximize the remaining search space, ensuring a longer guessing period for the user. The "too high/too low" feedback is removed, relying solely on user questions for deduction.
2025-08-10 14:11:36 +00:00

383 lines
No EOL
15 KiB
TypeScript

import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { HelpCircle, CheckCircle, XCircle } from 'lucide-react';
interface QuestionGuessingModeProps {
maxRange: number;
guesses: number[];
feedback: string;
validNumbers: Set<number>;
userGuess: string;
setUserGuess: (value: string) => void;
onGuess: (guessValue?: number) => void;
onUpdateValidNumbers: (newValidNumbers: Set<number>) => void;
onWin: () => void;
}
const QuestionGuessingMode: React.FC<QuestionGuessingModeProps> = ({
maxRange,
guesses,
feedback,
validNumbers,
userGuess,
setUserGuess,
onGuess,
onUpdateValidNumbers,
onWin
}) => {
const [question, setQuestion] = useState('');
const [questionHistory, setQuestionHistory] = useState<{question: string, answer: string, understood: boolean}[]>([]);
const [questionFeedback, setQuestionFeedback] = useState('');
// Function to determine the optimal answer to keep search space as large as possible
const getOptimalAnswer = (question: string): { understood: boolean, answer: boolean, questionType?: string, value?: number } => {
const q = question.toLowerCase().trim();
// Remove question marks and normalize
const normalizedQ = q.replace(/\?/g, '').trim();
// Check for various question patterns
if (normalizedQ.includes('is') && normalizedQ.includes('number')) {
// Is the number...
if (normalizedQ.includes('even')) {
// Count how many valid numbers are even vs odd
const evenCount = Array.from(validNumbers).filter(n => n % 2 === 0).length;
const oddCount = validNumbers.size - evenCount;
// Choose answer that keeps more numbers
const answer = evenCount > oddCount;
return { understood: true, answer, questionType: 'even' };
}
if (normalizedQ.includes('odd')) {
const oddCount = Array.from(validNumbers).filter(n => n % 2 !== 0).length;
const evenCount = validNumbers.size - oddCount;
const answer = oddCount > evenCount;
return { understood: true, answer, questionType: 'odd' };
}
if (normalizedQ.includes('prime')) {
const isPrime = (num: number): boolean => {
if (num < 2) return false;
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) return false;
}
return true;
};
const primeCount = Array.from(validNumbers).filter(n => isPrime(n)).length;
const nonPrimeCount = validNumbers.size - primeCount;
const answer = primeCount > nonPrimeCount;
return { understood: true, answer, questionType: 'prime' };
}
if (normalizedQ.includes('perfect square')) {
const perfectSquareCount = Array.from(validNumbers).filter(n => Math.sqrt(n) % 1 === 0).length;
const nonPerfectSquareCount = validNumbers.size - perfectSquareCount;
const answer = perfectSquareCount > nonPerfectSquareCount;
return { understood: true, answer, questionType: 'perfectSquare' };
}
}
// Check for divisibility questions
if (normalizedQ.includes('divisible by') || normalizedQ.includes('multiple of')) {
const matches = normalizedQ.match(/(?:divisible by|multiple of)\s+(\d+)/);
if (matches) {
const divisor = parseInt(matches[1]);
const divisibleCount = Array.from(validNumbers).filter(n => n % divisor === 0).length;
const nonDivisibleCount = validNumbers.size - divisibleCount;
const answer = divisibleCount > nonDivisibleCount;
return { understood: true, answer, questionType: 'divisible', value: divisor };
}
}
// Check for range questions
if (normalizedQ.includes('greater than') || normalizedQ.includes('bigger than') || normalizedQ.includes('larger than')) {
const matches = normalizedQ.match(/(?:greater than|bigger than|larger than)\s+(\d+)/);
if (matches) {
const threshold = parseInt(matches[1]);
const greaterCount = Array.from(validNumbers).filter(n => n > threshold).length;
const lessEqualCount = validNumbers.size - greaterCount;
const answer = greaterCount > lessEqualCount;
return { understood: true, answer, questionType: 'greaterThan', value: threshold };
}
}
if (normalizedQ.includes('less than') || normalizedQ.includes('smaller than')) {
const matches = normalizedQ.match(/(?:less than|smaller than)\s+(\d+)/);
if (matches) {
const threshold = parseInt(matches[1]);
const lessCount = Array.from(validNumbers).filter(n => n < threshold).length;
const greaterEqualCount = validNumbers.size - lessCount;
const answer = lessCount > greaterEqualCount;
return { understood: true, answer, questionType: 'lessThan', value: threshold };
}
}
// Check for digit contains questions
if (normalizedQ.includes('contain') && normalizedQ.includes('digit')) {
const matches = normalizedQ.match(/contain(?:s)?\s+(?:the\s+)?digit\s+(\d)/);
if (matches) {
const digit = matches[1];
const containsCount = Array.from(validNumbers).filter(n => n.toString().includes(digit)).length;
const doesntContainCount = validNumbers.size - containsCount;
const answer = containsCount > doesntContainCount;
return { understood: true, answer, questionType: 'containsDigit', value: parseInt(digit) };
}
}
// Check for specific number questions
if (normalizedQ.includes('is it') || normalizedQ.includes('is the number')) {
const matches = normalizedQ.match(/(?:is it|is the number)\s+(\d+)/);
if (matches) {
const num = parseInt(matches[1]);
// If there's only one number left and they're asking about it, we must answer truthfully
if (validNumbers.size === 1) {
const answer = validNumbers.has(num);
return { understood: true, answer, questionType: 'equals', value: num };
}
// Otherwise, say no to keep the game going unless this number isn't even valid
const answer = false;
return { understood: true, answer, questionType: 'equals', value: num };
}
}
return { understood: false, answer: false };
};
// Function to update valid numbers based on question and answer
const updateValidNumbersFromQuestion = (questionType: string, answer: boolean, value?: number) => {
const newValidNumbers = new Set<number>();
for (let num = 1; num <= maxRange; num++) {
if (!validNumbers.has(num)) continue; // Skip already invalid numbers
let shouldKeep = false;
switch (questionType) {
case 'even':
shouldKeep = answer ? (num % 2 === 0) : (num % 2 !== 0);
break;
case 'odd':
shouldKeep = answer ? (num % 2 !== 0) : (num % 2 === 0);
break;
case 'prime':
const isPrimeNum = (n: number): boolean => {
if (n < 2) return false;
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) return false;
}
return true;
};
shouldKeep = answer ? isPrimeNum(num) : !isPrimeNum(num);
break;
case 'perfectSquare':
const isPerfectSquareNum = Math.sqrt(num) % 1 === 0;
shouldKeep = answer ? isPerfectSquareNum : !isPerfectSquareNum;
break;
case 'divisible':
if (value !== undefined) {
shouldKeep = answer ? (num % value === 0) : (num % value !== 0);
}
break;
case 'greaterThan':
if (value !== undefined) {
shouldKeep = answer ? (num > value) : (num <= value);
}
break;
case 'lessThan':
if (value !== undefined) {
shouldKeep = answer ? (num < value) : (num >= value);
}
break;
case 'containsDigit':
if (value !== undefined) {
shouldKeep = answer ? num.toString().includes(value.toString()) : !num.toString().includes(value.toString());
}
break;
case 'equals':
if (value !== undefined) {
shouldKeep = answer ? (num === value) : (num !== value);
}
break;
default:
shouldKeep = true; // Keep all numbers if we don't understand the question type
}
if (shouldKeep) {
newValidNumbers.add(num);
}
}
onUpdateValidNumbers(newValidNumbers);
};
const handleQuestionSubmit = () => {
if (!question.trim()) return;
const result = getOptimalAnswer(question);
if (result.understood && result.questionType) {
const answer = result.answer ? 'Yes' : 'No';
setQuestionHistory([...questionHistory, {
question: question,
answer: answer,
understood: true
}]);
setQuestionFeedback(answer);
// Update valid numbers based on the question and answer
updateValidNumbersFromQuestion(result.questionType, result.answer, result.value);
} else {
setQuestionHistory([...questionHistory, {
question: question,
answer: "I don't understand this question",
understood: false
}]);
setQuestionFeedback("I don't understand that question. Please try rephrasing it as a clear yes/no question about mathematical properties, ranges, or specific values.");
}
setQuestion('');
};
// Handle final number guess - just check if it's the only remaining valid number
const handleFinalGuess = (guessValue?: number) => {
const guess = guessValue || parseInt(userGuess);
if (isNaN(guess) || guess < 1 || guess > maxRange) {
return;
}
if (validNumbers.size === 1 && validNumbers.has(guess)) {
onWin();
} else if (validNumbers.has(guess)) {
// Remove this number from valid numbers since it's not the answer
const newValidNumbers = new Set(validNumbers);
newValidNumbers.delete(guess);
onUpdateValidNumbers(newValidNumbers);
// Check if we're down to one number
if (newValidNumbers.size === 1) {
onWin();
}
}
setUserGuess('');
};
return (
<div className="space-y-6">
<div className="text-center space-y-2">
<h3 className="text-xl font-semibold">Ask yes/no questions to find the number between 1 and {maxRange}</h3>
<p className="text-muted-foreground">Questions asked: {questionHistory.length}</p>
{feedback && (
<div className={`p-3 rounded-lg ${feedback.includes('Congratulations') ? 'bg-success/10 text-success' : 'bg-muted'}`}>
{feedback}
</div>
)}
</div>
{/* Question Input */}
<div className="space-y-4">
<div className="space-y-2">
<label className="font-semibold">Ask a yes/no question:</label>
<div className="flex gap-2">
<Textarea
placeholder="e.g., 'Is the number even?', 'Is it greater than 25?', 'Is it divisible by 3?'"
value={question}
onChange={(e) => setQuestion(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleQuestionSubmit();
}
}}
rows={2}
/>
<Button onClick={handleQuestionSubmit} disabled={!question.trim()}>
Ask
</Button>
</div>
</div>
{questionFeedback && (
<div className="p-3 rounded-lg bg-muted border">
<div className="flex items-center gap-2">
<HelpCircle className="w-4 h-4" />
<span className="font-medium">Answer:</span>
<span>{questionFeedback}</span>
</div>
</div>
)}
</div>
{/* Question History */}
{questionHistory.length > 0 && (
<div className="space-y-3">
<h4 className="font-semibold">Question History:</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{questionHistory.map((item, index) => (
<div key={index} className="p-3 rounded-lg border bg-card">
<div className="flex items-start gap-2">
{item.understood ? (
<CheckCircle className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
) : (
<XCircle className="w-4 h-4 text-red-500 mt-0.5 flex-shrink-0" />
)}
<div className="flex-1 space-y-1">
<p className="text-sm"><strong>Q:</strong> {item.question}</p>
<p className="text-sm text-muted-foreground"><strong>A:</strong> {item.answer}</p>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Number Grid Display */}
<div className="space-y-4">
<h4 className="font-semibold text-center">Click a number to make your final guess ({validNumbers.size} possibilities remaining):</h4>
<div className="grid grid-cols-8 sm:grid-cols-10 md:grid-cols-12 lg:grid-cols-16 gap-1 max-h-64 overflow-y-auto p-2 border rounded-lg">
{Array.from({ length: maxRange }, (_, i) => i + 1).map(num => {
const isValid = validNumbers.has(num);
const isGuessed = guesses.includes(num);
return (
<Button
key={num}
size="sm"
variant={isGuessed ? "default" : isValid ? "outline" : "ghost"}
className={`
h-8 p-1 text-xs
${!isValid && !isGuessed ? 'opacity-30 cursor-not-allowed text-muted-foreground' : ''}
${isGuessed ? 'bg-accent text-accent-foreground' : ''}
${isValid && !isGuessed ? 'hover:bg-accent/10' : ''}
`}
onClick={() => isValid && !isGuessed && handleFinalGuess(num)}
disabled={!isValid || isGuessed}
>
{num}
</Button>
);
})}
</div>
</div>
<div className="flex gap-2">
<Input
type="number"
placeholder="Or type your final guess"
value={userGuess}
onChange={(e) => setUserGuess(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleFinalGuess()}
min={1}
max={maxRange}
/>
<Button onClick={() => handleFinalGuess()} disabled={!userGuess}>
Guess
</Button>
</div>
</div>
);
};
export default QuestionGuessingMode;