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.
383 lines
No EOL
15 KiB
TypeScript
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; |