feat: redesign memory game with invisible input and penalty scoring

## Input Interface Improvements
- Remove field borders and cursor for natural typing experience
- Hide input field completely off-screen, display numbers in clean text area
- Add click-to-focus on display area to maintain keyboard interaction
- Use monospace font with letter spacing for clear number display

## Memory Card Sizing Improvements
- Make cards viewport-aware using min(85vw, 700px) and min(50vh, 400px)
- Add responsive breakpoints for small screens to prevent scrolling
- Ensure entire game fits in viewport without requiring scroll
- Scale SVG content to fill available card space

## Penalty Scoring System
- Track incorrect guesses and apply 5-point penalty per wrong guess
- Display score breakdown showing base score, penalties, and final score
- Update results display to clearly show wrong guesses with point deduction
- Encourage careful thinking rather than random guessing

## User Experience
- Numbers appear naturally as user types without form-like interface
- Large but viewport-constrained cards for optimal memorization
- Fair scoring that rewards accuracy and penalizes careless guessing
- Responsive design works across all screen sizes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-09-10 14:21:36 -05:00
parent c7ae4d30eb
commit b92a867677
6 changed files with 29723 additions and 42 deletions

View File

@ -550,12 +550,55 @@ def generate_web_flashcards(numbers, config, output_path):
.quiz-flashcard {{
background: white;
border-radius: 12px;
padding: 30px;
padding: 20px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
max-width: 400px;
width: min(85vw, 700px);
height: min(50vh, 400px);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
transition: transform 0.3s ease;
}}
/* Ensure quiz game section fits in viewport */
#quiz-game {{
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
box-sizing: border-box;
}}
/* Responsive adjustments for smaller screens */
@media (max-height: 600px) {{
.quiz-flashcard {{
height: min(40vh, 300px);
padding: 15px;
}}
}}
@media (max-height: 500px) {{
.quiz-flashcard {{
height: min(35vh, 250px);
padding: 10px;
}}
#quiz-game {{
min-height: auto;
padding: 10px;
}}
}}
.quiz-flashcard svg {{
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}}
.quiz-flashcard.pulse {{
transform: scale(1.05);
}}
@ -647,36 +690,44 @@ def generate_web_flashcards(numbers, config, output_path):
.smart-input-container {{
position: relative;
margin: 30px 0;
}}
#smart-input {{
width: 100%;
max-width: 400px;
padding: 15px 20px;
font-size: 20px;
border: 3px solid #ddd;
border-radius: 12px;
margin: 40px 0;
text-align: center;
}}
.smart-input-prompt {{
font-size: 16px;
color: #7a8695;
margin-bottom: 15px;
font-weight: normal;
}}
.number-display {{
min-height: 60px;
padding: 20px;
font-size: 32px;
font-family: 'Courier New', 'Monaco', monospace;
text-align: center;
transition: all 0.3s ease;
font-weight: bold;
color: #2c3e50;
letter-spacing: 3px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}}
#smart-input:focus {{
outline: none;
border-color: #2c5f76;
box-shadow: 0 0 0 3px rgba(44, 95, 118, 0.1);
.current-typing {{
display: inline-block;
transition: all 0.3s ease;
}}
#smart-input.correct {{
border-color: #28a745;
background: rgba(40, 167, 69, 0.1);
.number-display.correct .current-typing {{
color: #28a745;
animation: successPulse 0.5s ease;
}}
#smart-input.incorrect {{
border-color: #dc3545;
background: rgba(220, 53, 69, 0.1);
.number-display.incorrect .current-typing {{
color: #dc3545;
animation: errorShake 0.5s ease;
}}
@ -2071,8 +2122,11 @@ def generate_web_flashcards(numbers, config, output_path):
</div>
<div class="smart-input-container">
<input type="text" id="smart-input" placeholder="Type any number you remember..." autocomplete="off">
<div class="input-feedback" id="input-feedback"></div>
<div class="smart-input-prompt">Type the numbers you remember:</div>
<div class="number-display" id="number-display">
<span class="current-typing" id="current-typing"></span>
</div>
<input type="text" id="smart-input" style="position: absolute; left: -9999px; opacity: 0; pointer-events: none;" autocomplete="off">
</div>
<div class="found-numbers" id="found-numbers">
@ -2372,6 +2426,7 @@ def generate_web_flashcards(numbers, config, output_path):
this.correctAnswers = [];
this.guessesRemaining = 0;
this.currentInput = '';
this.incorrectGuesses = 0;
this.finishButtonsBound = false;
this.initializeCards();
@ -2573,7 +2628,11 @@ def generate_web_flashcards(numbers, config, output_path):
// Setup smart input
const smartInput = document.getElementById('smart-input');
const display = document.getElementById('number-display');
smartInput.value = '';
document.getElementById('current-typing').textContent = '';
// Focus the hidden input and make sure it captures keyboard events
smartInput.focus();
// Remove any existing event listeners to prevent duplicates
@ -2582,6 +2641,13 @@ def generate_web_flashcards(numbers, config, output_path):
// Add input event listener for real-time validation
newSmartInput.addEventListener('input', (e) => this.handleSmartInput(e));
// Make the display area clickable to maintain focus
display.addEventListener('click', () => {{
newSmartInput.focus();
}});
// Keep focus on the hidden input
newSmartInput.focus();
// Bind finish buttons (they exist now that quiz-input is shown)
@ -2625,9 +2691,14 @@ def generate_web_flashcards(numbers, config, output_path):
handleSmartInput(event) {{
const input = event.target;
const value = input.value.trim();
const display = document.getElementById('number-display');
const typingSpan = document.getElementById('current-typing');
// Reset visual feedback
input.classList.remove('correct', 'incorrect');
display.classList.remove('correct', 'incorrect');
// Update the visual display
typingSpan.textContent = value;
// Check if input is empty
if (!value) {{
@ -2646,19 +2717,19 @@ def generate_web_flashcards(numbers, config, output_path):
// Check if this number is in our correct answers and not already found
if (this.correctAnswers.includes(number) && !this.foundNumbers.includes(number)) {{
// Correct number found!
this.acceptCorrectNumber(number, input);
this.acceptCorrectNumber(number, input, display);
}} else if (value.length >= 2 && !this.correctAnswers.includes(number)) {{
// Wrong number (only trigger after at least 2 digits to avoid false positives)
this.handleIncorrectGuess(input);
this.handleIncorrectGuess(input, display);
}}
}}
acceptCorrectNumber(number, input) {{
acceptCorrectNumber(number, input, display) {{
// Add to found numbers
this.foundNumbers.push(number);
// Visual success feedback
input.classList.add('correct');
display.classList.add('correct');
// Update stats
document.getElementById('numbers-found').textContent = this.foundNumbers.length;
@ -2669,6 +2740,7 @@ def generate_web_flashcards(numbers, config, output_path):
// Clear input immediately for fast entry
setTimeout(() => {{
input.value = '';
document.getElementById('current-typing').textContent = '';
this.currentInput = '';
// Check if we're done
@ -2682,22 +2754,24 @@ def generate_web_flashcards(numbers, config, output_path):
// Remove success visual feedback after animation completes
setTimeout(() => {{
input.classList.remove('correct');
display.classList.remove('correct');
}}, 500);
}}
handleIncorrectGuess(input) {{
handleIncorrectGuess(input, display) {{
// Only penalize if we have guesses remaining
if (this.guessesRemaining > 0) {{
this.guessesRemaining--;
this.incorrectGuesses++; // Track incorrect guesses for scoring
document.getElementById('guesses-remaining').textContent = this.guessesRemaining;
// Visual error feedback
input.classList.add('incorrect');
display.classList.add('incorrect');
// Clear input quickly for rapid entry
setTimeout(() => {{
input.value = '';
document.getElementById('current-typing').textContent = '';
this.currentInput = '';
// Check if we're out of guesses
@ -2709,7 +2783,7 @@ def generate_web_flashcards(numbers, config, output_path):
// Remove error visual feedback after animation completes
setTimeout(() => {{
input.classList.remove('incorrect');
display.classList.remove('incorrect');
}}, 500);
}}
}}
@ -2772,8 +2846,10 @@ def generate_web_flashcards(numbers, config, output_path):
}}
showResults() {{
const correct = this.calculateScore();
const percentage = Math.round((correct.length / this.correctAnswers.length) * 100);
const scoreData = this.calculateScore();
const correct = scoreData.correct;
const finalScore = scoreData.finalScore;
const percentage = Math.round(finalScore);
// Update score display
document.getElementById('score-percentage').textContent = percentage + '%';
@ -2781,8 +2857,8 @@ def generate_web_flashcards(numbers, config, output_path):
document.getElementById('score-total').textContent = this.correctAnswers.length;
document.getElementById('result-timing').textContent = this.displayTime.toFixed(1) + 's';
// Show detailed results
this.showDetailedResults(correct);
// Show detailed results with penalty info
this.showDetailedResults(correct, scoreData);
// Hide input, show results
this.hideQuizSections();
@ -2801,16 +2877,41 @@ def generate_web_flashcards(numbers, config, output_path):
}}
}});
return correct;
// Calculate base score as percentage of correct answers
const baseScore = (correct.length / this.correctAnswers.length) * 100;
// Calculate penalty: lose 5 points per incorrect guess, minimum 0%
const penalty = this.incorrectGuesses * 5;
const finalScore = Math.max(0, baseScore - penalty);
return {{
correct: correct,
baseScore: baseScore,
penalty: penalty,
incorrectGuesses: this.incorrectGuesses,
finalScore: finalScore
}};
}}
showDetailedResults(correct) {{
showDetailedResults(correct, scoreData) {{
const resultsEl = document.getElementById('results-list');
const correctSet = new Set(correct);
const answerSet = new Set(this.answers);
let html = '';
// Show scoring breakdown if there were penalties
if (scoreData && scoreData.incorrectGuesses > 0) {{
html += `<div class="result-item score-breakdown">
<div style="margin-bottom: 10px; font-weight: bold; color: #2c5f76;">Score Breakdown:</div>
<div style="font-size: 0.9em; color: #666;">
Base Score: ${{Math.round(scoreData.baseScore)}}% (${{correct.length}} of ${{this.correctAnswers.length}} correct)<br>
Penalty: -${{scoreData.penalty}}% (${{scoreData.incorrectGuesses}} wrong guess${{scoreData.incorrectGuesses > 1 ? 'es' : ''}} × 5 points each)<br>
<strong style="color: #2c5f76;">Final Score: ${{Math.round(scoreData.finalScore)}}%</strong>
</div>
</div>`;
}}
// Show all correct answers and whether user got them
this.correctAnswers.forEach(num => {{
const wasCorrect = correctSet.has(num);
@ -2826,8 +2927,8 @@ def generate_web_flashcards(numbers, config, output_path):
const extraAnswers = this.answers.filter(a => !correctSet.has(a) && this.correctAnswers.includes(a) === false);
extraAnswers.forEach(num => {{
html += `<div class="result-item">
<span>Extra answer: ${{num}}</span>
<span class="result-incorrect"> Not in quiz</span>
<span>Wrong guess: ${{num}}</span>
<span class="result-incorrect"> Not in quiz (-5 points)</span>
</div>`;
}});
@ -2848,6 +2949,7 @@ def generate_web_flashcards(numbers, config, output_path):
this.foundNumbers = [];
this.guessesRemaining = 0;
this.currentInput = '';
this.incorrectGuesses = 0;
this.finishButtonsBound = false;
// Clear smart input

5878
test_borderless_input.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5959
test_penalty_scoring.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff