soroban-abacus-flashcards/test_all_digits_physical.html

3601 lines
160 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soroban Flashcards</title>
<style>
body {
font-family: DejaVu Sans, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 1.2em;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin: 20px 0;
}
.flashcard {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
position: relative;
min-height: 220px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.flashcard:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.abacus-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
margin: 10px 0;
max-width: 100%;
overflow: hidden;
}
.abacus-container svg {
max-width: 100%;
height: auto;
}
.numeral {
font-size: 48pt;
font-weight: bold;
color: #333;
opacity: 0;
transition: opacity 0.3s ease;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255,255,255,0.95);
padding: 15px 25px;
border-radius: 8px;
border: 2px solid #ddd;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 10;
}
.flashcard:hover .numeral {
opacity: 1;
}
.card-number {
position: absolute;
top: 10px;
left: 10px;
font-size: 0.8em;
color: #999;
background: rgba(255,255,255,0.8);
padding: 2px 6px;
border-radius: 4px;
}
.instructions {
text-align: center;
margin: 30px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.instructions h3 {
color: #333;
margin-bottom: 10px;
}
.instructions p {
color: #666;
line-height: 1.5;
}
.stats {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.stats div {
background: #f8f9fa;
padding: 10px 15px;
border-radius: 6px;
font-size: 0.9em;
}
@media (max-width: 768px) {
.cards-grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
}
.flashcard {
min-height: 200px;
padding: 15px;
}
.numeral {
font-size: calc(48pt * 0.8);
padding: 10px 20px;
}
}
/* Quiz Styling */
.quiz-section {
background: #f8f9fa;
border-radius: 12px;
padding: 30px;
margin: 30px 0;
text-align: center;
}
.quiz-section h2 {
color: #333;
margin-bottom: 10px;
}
.quiz-controls {
max-width: 600px;
margin: 0 auto;
}
.control-group {
margin: 20px 0;
}
.control-group label {
display: block;
font-weight: bold;
margin-bottom: 10px;
color: #555;
}
.count-buttons {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.count-btn {
background: white;
border: 2px solid #ddd;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 16px;
}
.count-btn:hover {
border-color: #4a90e2;
background: #f0f7ff;
}
.count-btn.active {
background: #4a90e2;
color: white;
border-color: #4a90e2;
}
.slider-container {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.slider-container input[type="range"] {
width: 200px;
height: 6px;
border-radius: 3px;
background: #ddd;
outline: none;
-webkit-appearance: none;
}
.slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #4a90e2;
cursor: pointer;
}
.slider-value {
font-weight: bold;
color: #4a90e2;
min-width: 50px;
}
.quiz-start-btn {
background: #28a745;
color: white;
border: none;
border-radius: 8px;
padding: 15px 30px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s ease;
margin-top: 20px;
}
.quiz-start-btn:hover {
background: #218838;
}
/* Quiz Game Area */
.quiz-game {
text-align: center;
padding: 40px 20px;
}
.quiz-progress {
margin-bottom: 30px;
}
.progress-bar {
width: 100%;
max-width: 400px;
height: 8px;
background: #ddd;
border-radius: 4px;
margin: 0 auto 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #4a90e2;
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
color: #666;
font-size: 16px;
}
.quiz-display {
position: relative;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.quiz-flashcard {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
max-width: 400px;
transition: transform 0.3s ease;
}
.quiz-flashcard.pulse {
transform: scale(1.05);
}
.countdown {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 48px;
font-weight: bold;
color: #4a90e2;
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
z-index: 10;
}
.countdown.ready {
color: #28a745;
}
.countdown.go {
color: #ffc107;
}
/* Quiz Input */
.quiz-input {
text-align: center;
padding: 40px 20px;
max-width: 600px;
margin: 0 auto;
}
.input-container {
margin-top: 20px;
}
.quiz-input textarea {
width: 100%;
max-width: 400px;
height: 120px;
border: 2px solid #ddd;
border-radius: 8px;
padding: 15px;
font-size: 16px;
resize: vertical;
font-family: inherit;
}
.quiz-input textarea:focus {
outline: none;
border-color: #4a90e2;
}
.quiz-input button {
background: #4a90e2;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
margin-top: 15px;
}
.quiz-input button:hover {
background: #357abd;
}
/* Quiz Results */
.quiz-results {
text-align: center;
padding: 40px 20px;
max-width: 700px;
margin: 0 auto;
}
.score-display {
display: flex;
justify-content: center;
align-items: center;
gap: 40px;
margin: 30px 0;
flex-wrap: wrap;
}
.score-circle {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #4a90e2, #357abd);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: bold;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.score-details {
text-align: left;
}
.score-details p {
margin: 8px 0;
font-size: 16px;
}
.results-breakdown {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 30px 0;
text-align: left;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.result-item:last-child {
border-bottom: none;
}
.result-correct {
color: #28a745;
font-weight: bold;
}
.result-incorrect {
color: #dc3545;
font-weight: bold;
}
.quiz-actions {
margin-top: 30px;
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.quiz-actions button {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s ease;
}
#retry-quiz {
background: #ffc107;
color: #333;
}
#retry-quiz:hover {
background: #e0a800;
}
#back-to-cards {
background: #6c757d;
color: white;
}
#back-to-cards:hover {
background: #545b62;
}
/* End Game Button */
.end-game-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s ease;
}
.end-game-btn:hover {
background: #c82333;
}
/* Quiz Header for Game Mode */
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px 20px;
background: rgba(74, 144, 226, 0.1);
border-radius: 8px;
}
.quiz-header .quiz-progress {
flex: 1;
}
@media (max-width: 768px) {
.quiz-section {
padding: 20px;
}
.count-buttons {
gap: 8px;
}
.count-btn {
padding: 8px 16px;
font-size: 14px;
}
.score-display {
flex-direction: column;
gap: 20px;
}
.score-details {
text-align: center;
}
.countdown {
font-size: 36px;
}
.sorting-section {
padding: 20px;
}
.sorting-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
padding: 15px;
}
.sort-card {
padding: 10px;
}
.sort-count-btn {
padding: 8px 16px;
font-size: 14px;
margin: 2px;
}
.sort-start-btn, .sort-check-btn, .sort-reveal-btn, .sort-new-btn {
padding: 10px 16px;
font-size: 14px;
margin: 5px;
}
.sorting-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.challenge-buttons {
grid-template-columns: 1fr;
gap: 15px;
margin: 20px 0;
}
.modal-content {
width: 95%;
max-height: 95vh;
}
.modal-header {
padding: 15px 20px;
}
.modal-body {
padding: 20px;
}
.modal-header h2 {
font-size: 18px;
}
}
/* Card Sorting Styling */
.sorting-section {
background: #e8f4f8;
border-radius: 12px;
padding: 30px;
margin: 30px 0;
text-align: center;
}
.sorting-section h2 {
color: #2c5f76;
margin-bottom: 10px;
}
.sorting-controls {
max-width: 600px;
margin: 0 auto;
}
.sort-count-btn {
background: #ffffff;
color: #2c5f76;
border: 2px solid #2c5f76;
border-radius: 8px;
padding: 10px 20px;
margin: 0 5px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.sort-count-btn:hover {
background: #2c5f76;
color: white;
transform: translateY(-2px);
}
.sort-count-btn.active {
background: #2c5f76;
color: white;
}
.sorting-actions {
margin-top: 20px;
}
.sorting-game-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0;
}
.sort-start-btn, .sort-check-btn, .sort-reveal-btn, .sort-new-btn {
background: #2c5f76;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
margin: 0 10px;
transition: all 0.3s ease;
}
.sort-start-btn:hover, .sort-check-btn:hover, .sort-reveal-btn:hover, .sort-new-btn:hover {
background: #1e4a61;
transform: translateY(-2px);
}
.sort-check-btn {
background: #28a745;
}
.sort-check-btn:hover {
background: #218838;
}
.sort-reveal-btn {
background: #ffc107;
color: #333;
}
.sort-reveal-btn:hover {
background: #e0a800;
}
.sort-new-btn {
background: #6f42c1;
}
.sort-new-btn:hover {
background: #5a32a3;
}
.sorting-instructions {
margin: 20px 0;
color: #2c5f76;
}
.sorting-progress {
background: #fff;
border-radius: 8px;
padding: 10px 20px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.sorting-area {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: rgba(255,255,255,0.5);
border-radius: 8px;
min-height: 150px;
}
.position-slots {
display: flex;
flex-wrap: wrap;
gap: 5px;
justify-content: center;
align-items: center;
margin: 20px 0;
padding: 20px;
background: rgba(255,255,255,0.7);
border-radius: 8px;
border: 2px dashed #2c5f76;
}
.insert-button {
width: 40px;
height: 60px;
background: #2c5f76;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 24px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
opacity: 0.3;
}
.insert-button:hover {
opacity: 1;
background: #1976d2;
transform: scale(1.1);
}
.insert-button.active {
opacity: 1;
background: #1976d2;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.insert-button.disabled {
opacity: 0.1;
cursor: not-allowed;
}
.position-slot {
width: 120px;
height: 140px;
border: 2px solid #2c5f76;
border-radius: 8px;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.position-slot:hover {
background: #f0f8ff;
border-color: #1a4a5c;
transform: scale(1.05);
}
.position-slot.active {
background: #e3f2fd;
border-color: #1976d2;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.position-slot .slot-number {
font-size: 18px;
font-weight: bold;
color: #2c5f76;
margin-bottom: 5px;
}
.position-slot .slot-label {
font-size: 12px;
color: #666;
text-align: center;
}
.position-slot.filled {
background: #e8f5e8;
border-color: #28a745;
}
.position-slot.filled .slot-card {
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 25px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
}
.position-slot.correct {
border-color: #28a745;
background: #f8fff9;
}
.position-slot.incorrect {
border-color: #dc3545;
background: #fff8f8;
animation: shake 0.5s ease-in-out;
}
.sorting-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin: 30px 0;
padding: 20px;
background: rgba(255,255,255,0.7);
border-radius: 12px;
min-height: 300px;
}
.sort-card {
background: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 3px solid transparent;
position: relative;
user-select: none;
width: 120px;
height: 120px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.sort-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
border-color: #2c5f76;
}
.sort-card.selected {
border-color: #1976d2;
background: #e3f2fd;
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.3);
}
.sort-card.placed {
opacity: 0.7;
transform: scale(0.9);
cursor: default;
}
.sort-card.placed:hover {
transform: scale(0.95);
border-color: #28a745;
}
.sort-card.correct {
border-color: #28a745;
background: #f8fff9;
}
.sort-card.incorrect {
border-color: #dc3545;
background: #fff8f8;
animation: shake 0.5s ease-in-out;
}
.sort-card .card-position {
position: absolute;
top: 5px;
left: 5px;
background: rgba(44, 95, 118, 0.8);
color: white;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
.sort-card .revealed-number {
position: absolute;
top: 5px;
right: 5px;
background: #ffc107;
color: #333;
border-radius: 4px;
padding: 2px 8px;
font-size: 14px;
font-weight: bold;
display: none;
}
.sort-card.revealed .revealed-number {
display: block;
}
.sorting-feedback {
background: white;
border-radius: 12px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
.feedback-perfect {
background: linear-gradient(135deg, #d4edda, #c3e6cb);
color: #155724;
border: 2px solid #28a745;
}
.feedback-good {
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
color: #856404;
border: 2px solid #ffc107;
}
.feedback-needs-work {
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
color: #721c24;
border: 2px solid #dc3545;
}
.feedback-score {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.feedback-message {
font-size: 18px;
margin: 10px 0;
}
.feedback-details {
margin-top: 15px;
font-size: 14px;
opacity: 0.8;
}
.feedback-breakdown {
margin: 20px 0;
padding: 15px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.score-component {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0;
padding: 5px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.score-component:last-child {
border-bottom: none;
}
.component-label {
font-weight: bold;
flex: 1;
}
.component-score {
font-weight: bold;
margin: 0 10px;
min-width: 40px;
text-align: right;
}
.component-weight {
font-size: 12px;
opacity: 0.7;
min-width: 80px;
text-align: right;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
@keyframes success-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.sort-card.success {
animation: success-pulse 0.6s ease-in-out;
}
/* Challenge Buttons */
.challenge-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 30px 0;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.challenge-btn {
background: linear-gradient(135deg, #4a90e2, #357abd);
color: white;
border: none;
border-radius: 16px;
padding: 30px;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.3);
}
.challenge-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(74, 144, 226, 0.4);
}
.challenge-btn h3 {
margin: 0 0 10px 0;
font-size: 20px;
font-weight: bold;
}
.challenge-btn p {
margin: 0;
opacity: 0.9;
font-size: 14px;
line-height: 1.4;
}
.sorting-btn {
background: linear-gradient(135deg, #2c5f76, #1e4a61);
box-shadow: 0 6px 20px rgba(44, 95, 118, 0.3);
}
.sorting-btn:hover {
box-shadow: 0 8px 25px rgba(44, 95, 118, 0.4);
}
/* Modal Styling */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.3s ease-out;
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 900px;
max-height: 90vh;
display: flex;
flex-direction: column;
animation: slideIn 0.3s ease-out;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
border-bottom: 1px solid #eee;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 12px 12px 0 0;
}
.modal-header h2 {
margin: 0;
color: #333;
}
.modal-controls {
display: flex;
gap: 10px;
}
.fullscreen-btn, .close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 5px 10px;
border-radius: 6px;
transition: background 0.2s ease;
}
.fullscreen-btn:hover, .close-btn:hover {
background: rgba(0, 0, 0, 0.1);
}
.close-btn {
color: #666;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 30px;
flex: 1;
overflow-y: auto;
}
/* Fullscreen modal styling */
.modal.fullscreen {
background: rgba(0, 0, 0, 0.95);
}
.modal.fullscreen .modal-content {
width: 100%;
height: 100%;
max-width: none;
max-height: none;
border-radius: 0;
}
.modal.fullscreen .modal-header {
border-radius: 0;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from { transform: scale(0.9) translateY(-20px); opacity: 0; }
to { transform: scale(1) translateY(0); opacity: 1; }
}
@media print {
body {
background-color: white;
}
.flashcard {
box-shadow: none;
border: 1px solid #ddd;
break-inside: avoid;
}
.numeral {
opacity: 0.5;
background: transparent;
border: none;
box-shadow: none;
}
.quiz-section, .quiz-game, .quiz-input, .quiz-results, .sorting-section {
display: none !important;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Soroban Flashcards</h1>
<p>Hover over the cards to reveal the numbers</p>
</div>
<div class="instructions">
<h3>How to use these flashcards:</h3>
<p>Look at each abacus representation and try to determine the number before hovering to reveal the answer.
The abacus shows numbers using beads: each column represents a place value (ones, tens, hundreds, etc.).
In each column, the top bead represents 5 and the bottom beads each represent 1.</p>
<div class="stats">
<div><strong>Cards:</strong> 10</div>
<div><strong>Range:</strong> 0 - 9</div>
<div><strong>Color Scheme:</strong> All beads are the same color</div>
<div><strong>Bead Shape:</strong> Diamond</div>
</div>
</div>
<!-- Challenge Buttons -->
<div class="challenge-buttons">
<button id="open-quiz-modal" class="challenge-btn quiz-btn">
<h3>Speed Memory Quiz</h3>
<p>Test your soroban reading skills with timed card displays</p>
</button>
<button id="open-sorting-modal" class="challenge-btn sorting-btn">
<h3>Card Sorting Challenge</h3>
<p>Arrange cards in order using only the abacus representations</p>
</button>
</div>
<!-- Quiz Modal -->
<div id="quiz-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Speed Memory Quiz</h2>
<div class="modal-controls">
<button id="quiz-fullscreen-btn" class="fullscreen-btn" title="Toggle Fullscreen"></button>
<button id="close-quiz-modal" class="close-btn">&times;</button>
</div>
</div>
<div class="modal-body">
<p>Test your soroban reading skills! Cards will be shown briefly, then you'll enter the numbers you remember.</p>
<div class="quiz-controls">
<div class="control-group">
<label for="quiz-count">Cards to Quiz:</label>
<div class="count-buttons">
<button type="button" class="count-btn" data-count="5">5</button>
<button type="button" class="count-btn" data-count="10">10</button>
<button type="button" class="count-btn active" data-count="15">15</button>
<button type="button" class="count-btn" data-count="25">25</button>
<button type="button" class="count-btn" data-count="all">All (10)</button>
</div>
</div>
<div class="control-group">
<label for="display-time">Display Time per Card:</label>
<div class="slider-container">
<input type="range" id="display-time" min="0.5" max="10" step="0.5" value="2" />
<span class="slider-value">2.0s</span>
</div>
</div>
<button id="start-quiz" class="quiz-start-btn">Start Quiz</button>
</div>
<!-- Quiz Game Area (hidden initially) -->
<div id="quiz-game" class="quiz-game" style="display: none;">
<div class="quiz-header">
<div class="quiz-progress">
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<span class="progress-text">Card <span id="current-card">1</span> of <span id="total-cards">10</span></span>
</div>
<button id="end-quiz" class="end-game-btn">End Quiz</button>
</div>
<div class="quiz-display">
<div id="quiz-card" class="quiz-flashcard">
<!-- Card content will be inserted here -->
</div>
<div id="quiz-countdown" class="countdown">Get Ready...</div>
</div>
</div>
<!-- Quiz Input Phase (hidden initially) -->
<div id="quiz-input" class="quiz-input" style="display: none;">
<h3>Enter the Numbers You Remember</h3>
<p>Type the numbers you saw, separated by commas or spaces:</p>
<div class="input-container">
<textarea id="answer-input" placeholder="e.g., 23, 45, 67 or 23 45 67"></textarea>
<button id="submit-answers">Submit Answers</button>
</div>
</div>
<!-- Quiz Results (hidden initially) -->
<div id="quiz-results" class="quiz-results" style="display: none;">
<h3>Quiz Results</h3>
<div class="score-display">
<div class="score-circle">
<span id="score-percentage">0%</span>
</div>
<div class="score-details">
<p><strong>Score:</strong> <span id="score-correct">0</span> / <span id="score-total">0</span> correct</p>
<p><strong>Time per card:</strong> <span id="result-timing">2.0s</span></p>
</div>
</div>
<div class="results-breakdown">
<h4>Detailed Results:</h4>
<div id="results-list">
<!-- Results will be inserted here -->
</div>
</div>
<div class="quiz-actions">
<button id="retry-quiz">Try Again</button>
<button id="back-to-cards">Back to Cards</button>
</div>
</div>
</div>
</div>
</div>
<!-- Sorting Modal -->
<div id="sorting-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Card Sorting Challenge</h2>
<div class="modal-controls">
<button id="sorting-fullscreen-btn" class="fullscreen-btn" title="Toggle Fullscreen"></button>
<button id="close-sorting-modal" class="close-btn">&times;</button>
</div>
</div>
<div class="modal-body">
<p>Click cards and positions to arrange them in ascending order (smallest to largest). No numerals shown - rely on reading the abacus!</p>
<div class="sorting-controls">
<div class="control-group">
<label for="sort-count">Cards to Sort:</label>
<div class="count-buttons">
<button type="button" class="sort-count-btn active" data-count="5">5</button>
<button type="button" class="sort-count-btn" data-count="8">8</button>
<button type="button" class="sort-count-btn" data-count="12">12</button>
<button type="button" class="sort-count-btn" data-count="15">15</button>
</div>
</div>
<div class="sorting-actions">
<button id="start-sorting" class="sort-start-btn">Start Sorting Challenge</button>
</div>
</div>
<!-- Game Action Buttons (outside controls so they remain visible during gameplay) -->
<div class="sorting-game-actions" style="display: none;">
<button id="check-sorting" class="sort-check-btn">Check My Solution</button>
<button id="reveal-numbers" class="sort-reveal-btn">Reveal Numbers</button>
<button id="new-sorting" class="sort-new-btn">New Challenge</button>
<button id="end-sorting" class="end-game-btn">End Sorting</button>
</div>
<!-- Sorting Game Area -->
<div id="sorting-game" style="display: none;">
<div class="sorting-instructions">
<p><strong>Place cards:</strong> Click a card, then click a position or + button to insert</p>
<p><strong>Move cards:</strong> Click any placed card to move it back to available cards</p>
<div class="sorting-progress">
<span id="sorting-status">Ready to start</span>
</div>
</div>
<div id="position-slots" class="position-slots">
<!-- Position slots will be created here -->
</div>
<div id="sorting-area" class="sorting-area">
<!-- Available cards will be shown here -->
</div>
<div class="sorting-feedback" id="sorting-feedback" style="display: none;">
<!-- Feedback will be shown here -->
</div>
</div>
</div>
</div>
</div>
<div class="cards-grid" id="cards-grid">
<div class="flashcard" data-number="0">
<div class="card-number">#1</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 13)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 37)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 53)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 69)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 85)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">0</div>
</div>
<div class="flashcard" data-number="1">
<div class="card-number">#2</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 13)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 33)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 54)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 70)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 86)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">1</div>
</div>
<div class="flashcard" data-number="2">
<div class="card-number">#3</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 13)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 33)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 49)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 70)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 86)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">2</div>
</div>
<div class="flashcard" data-number="3">
<div class="card-number">#4</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 13)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 33)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 49)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 65)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 86)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">3</div>
</div>
<div class="flashcard" data-number="4">
<div class="card-number">#5</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 13)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 33)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 49)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 65)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 81)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">4</div>
</div>
<div class="flashcard" data-number="5">
<div class="card-number">#6</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 17)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 37)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 53)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 69)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 85)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">5</div>
</div>
<div class="flashcard" data-number="6">
<div class="card-number">#7</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 17)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 33)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 54)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 70)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 86)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">6</div>
</div>
<div class="flashcard" data-number="7">
<div class="card-number">#8</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 17)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 33)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 49)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 70)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 86)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">7</div>
</div>
<div class="flashcard" data-number="8">
<div class="card-number">#9</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 17)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 33)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 49)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 65)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 86)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">8</div>
</div>
<div class="flashcard" data-number="9">
<div class="card-number">#10</div>
<div class="abacus-container">
<svg class="typst-doc" viewBox="0 0 288 180" width="288pt" height="180pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g>
<g transform="translate(14.399999999999995 14.399999999999995)">
<g class="typst-group">
<g>
<g transform="translate(117.10000000000001 15.600000000000005)">
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 12)">
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 0)">
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 120 L 3 120 L 3 0 Z "/>
</g>
<g transform="translate(4.1000000000000005 17)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 33)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 49)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 65)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(4.1000000000000005 81)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">9</div>
</div>
</div>
<div class="instructions">
<p><em>Tip: You can print these cards for offline practice. Numbers will be faintly visible in print mode.</em></p>
</div>
</div>
<script>
// Modal Manager - Handles modal dialogs and fullscreen functionality
class ModalManager {
constructor() {
this.isFullscreen = false;
this.bindEvents();
}
bindEvents() {
// Challenge button events
document.getElementById('open-quiz-modal').addEventListener('click', () => {
this.openModal('quiz-modal');
});
document.getElementById('open-sorting-modal').addEventListener('click', () => {
this.openModal('sorting-modal');
});
// Close button events
document.getElementById('close-quiz-modal').addEventListener('click', () => {
this.closeModal('quiz-modal');
});
document.getElementById('close-sorting-modal').addEventListener('click', () => {
this.closeModal('sorting-modal');
});
// Fullscreen button events
document.getElementById('quiz-fullscreen-btn').addEventListener('click', () => {
this.toggleFullscreen('quiz-modal');
});
document.getElementById('sorting-fullscreen-btn').addEventListener('click', () => {
this.toggleFullscreen('sorting-modal');
});
// Close modal when clicking outside
document.addEventListener('click', (e) => {
if (e.target.classList.contains('modal') && e.target.classList.contains('show')) {
this.closeModal(e.target.id);
}
});
// ESC key to close modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const openModal = document.querySelector('.modal.show');
if (openModal) {
this.closeModal(openModal.id);
}
}
});
// Fullscreen change events
document.addEventListener('fullscreenchange', () => {
this.handleFullscreenChange();
});
document.addEventListener('webkitfullscreenchange', () => {
this.handleFullscreenChange();
});
document.addEventListener('mozfullscreenchange', () => {
this.handleFullscreenChange();
});
document.addEventListener('MSFullscreenChange', () => {
this.handleFullscreenChange();
});
}
openModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.add('show');
document.body.style.overflow = 'hidden';
}
closeModal(modalId) {
const modal = document.getElementById(modalId);
if (this.isFullscreen) {
this.exitFullscreen();
}
modal.classList.remove('show', 'fullscreen');
document.body.style.overflow = '';
}
async toggleFullscreen(modalId) {
const modal = document.getElementById(modalId);
if (!this.isFullscreen) {
try {
if (modal.requestFullscreen) {
await modal.requestFullscreen();
} else if (modal.webkitRequestFullscreen) {
await modal.webkitRequestFullscreen();
} else if (modal.mozRequestFullScreen) {
await modal.mozRequestFullScreen();
} else if (modal.msRequestFullscreen) {
await modal.msRequestFullscreen();
}
modal.classList.add('fullscreen');
} catch (error) {
console.warn('Fullscreen not supported or failed:', error);
// Fallback to CSS fullscreen
modal.classList.add('fullscreen');
}
} else {
this.exitFullscreen();
}
}
exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
handleFullscreenChange() {
const isFullscreen = !!(document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement);
this.isFullscreen = isFullscreen;
if (!isFullscreen) {
// Remove fullscreen class from all modals when exiting fullscreen
document.querySelectorAll('.modal').forEach(modal => {
modal.classList.remove('fullscreen');
});
}
}
}
// Quiz functionality - No dependencies, pure JavaScript
class SorobanQuiz {
constructor() {
this.cards = [];
this.quizCards = [];
this.currentCardIndex = 0;
this.displayTime = 2.0;
this.selectedCount = 15;
this.answers = [];
this.correctAnswers = [];
this.initializeCards();
this.bindEvents();
}
initializeCards() {
// Extract card data from the DOM
const cardElements = document.querySelectorAll('.flashcard');
this.cards = Array.from(cardElements).map(card => ({
number: parseInt(card.dataset.number),
svg: card.querySelector('.abacus-container').innerHTML,
element: card
}));
}
bindEvents() {
// Count buttons
document.querySelectorAll('.count-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.count-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.selectedCount = btn.dataset.count === 'all' ? this.cards.length : parseInt(btn.dataset.count);
});
});
// Display time slider
const slider = document.getElementById('display-time');
const valueDisplay = document.querySelector('.slider-value');
slider.addEventListener('input', (e) => {
this.displayTime = parseFloat(e.target.value);
valueDisplay.textContent = this.displayTime.toFixed(1) + 's';
});
// Start quiz button
document.getElementById('start-quiz').addEventListener('click', () => {
this.startQuiz();
});
// Submit answers button
document.getElementById('submit-answers').addEventListener('click', () => {
this.submitAnswers();
});
// End quiz button
document.getElementById('end-quiz').addEventListener('click', () => {
this.endQuiz();
});
// Retry and back buttons
document.getElementById('retry-quiz').addEventListener('click', () => {
this.resetQuiz();
this.startQuiz();
});
document.getElementById('back-to-cards').addEventListener('click', () => {
this.resetQuiz();
});
}
async startQuiz() {
// Select random cards
this.quizCards = this.getRandomCards(this.selectedCount);
this.correctAnswers = this.quizCards.map(card => card.number);
this.currentCardIndex = 0;
// Hide configuration controls, show only game
document.querySelector('.quiz-controls').style.display = 'none';
// Show quiz game section within modal
this.hideQuizSections();
document.getElementById('quiz-game').style.display = 'block';
document.getElementById('total-cards').textContent = this.quizCards.length;
// Start with the first card
this.showNextCard();
}
getRandomCards(count) {
const shuffled = [...this.cards].sort(() => 0.5 - Math.random());
return shuffled.slice(0, Math.min(count, this.cards.length));
}
async showNextCard() {
if (this.currentCardIndex >= this.quizCards.length) {
this.showInputPhase();
return;
}
const card = this.quizCards[this.currentCardIndex];
const progress = ((this.currentCardIndex) / this.quizCards.length) * 100;
// Update progress
document.querySelector('.progress-fill').style.width = progress + '%';
document.getElementById('current-card').textContent = this.currentCardIndex + 1;
// Show countdown
await this.showCountdown();
// Show card
await this.displayCard(card);
this.currentCardIndex++;
// Small delay before next card
setTimeout(() => {
this.showNextCard();
}, 200);
}
async showCountdown() {
const countdownEl = document.getElementById('quiz-countdown');
const cardEl = document.getElementById('quiz-card');
cardEl.style.visibility = 'hidden';
countdownEl.style.display = 'block';
// 3, 2, 1 countdown
const counts = ['3', '2', '1', 'GO!'];
for (let i = 0; i < counts.length; i++) {
countdownEl.textContent = counts[i];
countdownEl.className = 'countdown';
if (i === counts.length - 1) countdownEl.classList.add('go');
await this.delay(400);
}
countdownEl.style.display = 'none';
}
async displayCard(card) {
const cardEl = document.getElementById('quiz-card');
const countdownEl = document.getElementById('quiz-countdown');
// Show card content
cardEl.innerHTML = card.svg;
cardEl.style.visibility = 'visible';
cardEl.classList.add('pulse');
// Display for the specified time
await this.delay(this.displayTime * 1000);
// Hide card
cardEl.classList.remove('pulse');
cardEl.style.visibility = 'hidden';
}
showInputPhase() {
// Complete progress bar
document.querySelector('.progress-fill').style.width = '100%';
// Hide quiz game, show input
this.hideQuizSections();
document.getElementById('quiz-input').style.display = 'block';
document.getElementById('answer-input').focus();
}
submitAnswers() {
const input = document.getElementById('answer-input').value;
this.answers = this.parseAnswers(input);
this.showResults();
}
parseAnswers(input) {
// Parse comma or space separated numbers
return input.split(/[,\s]+/)
.map(s => s.trim())
.filter(s => s.length > 0)
.map(s => parseInt(s))
.filter(n => !isNaN(n));
}
showResults() {
const correct = this.calculateScore();
const percentage = Math.round((correct.length / this.correctAnswers.length) * 100);
// Update score display
document.getElementById('score-percentage').textContent = percentage + '%';
document.getElementById('score-correct').textContent = correct.length;
document.getElementById('score-total').textContent = this.correctAnswers.length;
document.getElementById('result-timing').textContent = this.displayTime.toFixed(1) + 's';
// Show detailed results
this.showDetailedResults(correct);
// Hide input, show results
this.hideQuizSections();
document.getElementById('quiz-results').style.display = 'block';
}
calculateScore() {
const correct = [];
const correctSet = new Set(this.correctAnswers);
const answerSet = new Set(this.answers);
// Find correct answers
this.answers.forEach(answer => {
if (correctSet.has(answer)) {
correct.push(answer);
}
});
return correct;
}
showDetailedResults(correct) {
const resultsEl = document.getElementById('results-list');
const correctSet = new Set(correct);
const answerSet = new Set(this.answers);
let html = '';
// Show all correct answers and whether user got them
this.correctAnswers.forEach(num => {
const wasCorrect = correctSet.has(num);
const className = wasCorrect ? 'result-correct' : 'result-incorrect';
const status = wasCorrect ? '✓ Correct' : '✗ Missed';
html += `<div class="result-item">
<span>Card: ${num}</span>
<span class="${className}">${status}</span>
</div>`;
});
// Show any extra incorrect answers
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>
</div>`;
});
resultsEl.innerHTML = html;
}
endQuiz() {
// Stop the current quiz and return to configuration
this.resetQuiz();
}
resetQuiz() {
// Reset state
this.currentCardIndex = 0;
this.answers = [];
this.correctAnswers = [];
this.quizCards = [];
// Clear input
document.getElementById('answer-input').value = '';
// Reset to initial quiz state (hide all sections, show controls)
this.hideQuizSections();
document.querySelector('.quiz-controls').style.display = 'block';
}
hideQuizSections() {
document.getElementById('quiz-game').style.display = 'none';
document.getElementById('quiz-input').style.display = 'none';
document.getElementById('quiz-results').style.display = 'none';
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Card Sorting Challenge - Drag and drop functionality
class SortingChallenge {
constructor() {
this.cards = [];
this.sortingCards = [];
this.selectedCount = 5;
this.currentOrder = [];
this.correctOrder = [];
this.isDragging = false;
this.draggedElement = null;
this.draggedIndex = -1;
this.previewOrder = [];
this.lastInsertIndex = -1;
this.touchStartElement = null;
this.touchStartIndex = -1;
this.touchStartX = 0;
this.touchStartY = 0;
this.initializeSorting();
this.bindSortingEvents();
}
initializeSorting() {
// Get available cards (same as quiz cards)
const cardElements = document.querySelectorAll('.flashcard');
this.cards = Array.from(cardElements).map(card => ({
number: parseInt(card.dataset.number),
svg: card.querySelector('.abacus-container').outerHTML
}));
}
bindSortingEvents() {
// Card count selection
document.querySelectorAll('.sort-count-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.sort-count-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
this.selectedCount = parseInt(e.target.dataset.count);
});
});
// Action buttons
document.getElementById('start-sorting').addEventListener('click', () => this.startSorting());
document.getElementById('check-sorting').addEventListener('click', () => this.checkSolution());
document.getElementById('reveal-numbers').addEventListener('click', () => this.revealNumbers());
document.getElementById('new-sorting').addEventListener('click', () => this.newChallenge());
document.getElementById('end-sorting').addEventListener('click', () => this.endSorting());
}
startSorting() {
// Select random cards for sorting
const shuffledCards = [...this.cards].sort(() => Math.random() - 0.5);
this.sortingCards = shuffledCards.slice(0, this.selectedCount);
this.correctOrder = [...this.sortingCards].sort((a, b) => a.number - b.number);
// Shuffle the display order
this.currentOrder = [...this.sortingCards].sort(() => Math.random() - 0.5);
// Hide configuration controls, show only game
document.querySelector('.sorting-controls').style.display = 'none';
// Show sorting game
document.getElementById('sorting-game').style.display = 'block';
this.renderSortingCards();
this.updateSortingStatus(`Arrange the ${this.selectedCount} cards in ascending order (smallest to largest)`);
// Update buttons - show game controls
document.getElementById('start-sorting').style.display = 'none';
document.querySelector('.sorting-game-actions').style.display = 'block';
}
renderSortingCards() {
this.createPositionSlots();
this.renderAvailableCards();
}
createPositionSlots() {
const slotsContainer = document.getElementById('position-slots');
if (!slotsContainer) return;
slotsContainer.innerHTML = '';
this.placedCards = new Array(this.selectedCount).fill(null);
// Add insert button before the very first position
const firstInsertBtn = document.createElement('button');
firstInsertBtn.className = 'insert-button';
firstInsertBtn.innerHTML = '+';
firstInsertBtn.dataset.insertAt = 0;
firstInsertBtn.addEventListener('click', (e) => this.handleInsertClick(0));
slotsContainer.appendChild(firstInsertBtn);
for (let i = 0; i < this.selectedCount; i++) {
// Add the position slot
const slot = document.createElement('div');
slot.className = 'position-slot';
slot.dataset.position = i;
slot.innerHTML = `
<div class="slot-number">${i + 1}</div>
<div class="slot-label">${i === 0 ? 'Smallest' : i === this.selectedCount - 1 ? 'Largest' : ''}</div>
`;
slot.addEventListener('click', (e) => this.handleSlotClick(i));
slotsContainer.appendChild(slot);
// Add insert button after each position
const insertBtn = document.createElement('button');
insertBtn.className = 'insert-button';
insertBtn.innerHTML = '+';
insertBtn.dataset.insertAt = i + 1;
insertBtn.addEventListener('click', (e) => this.handleInsertClick(i + 1));
slotsContainer.appendChild(insertBtn);
}
}
renderAvailableCards() {
const sortingArea = document.getElementById('sorting-area');
if (!sortingArea) return;
sortingArea.innerHTML = '';
// Remove duplicates and filter out placed cards
const uniqueAvailable = this.currentOrder.filter((card, index, arr) => {
// Only keep first occurrence of each card number
const firstIndex = arr.findIndex(c => c.number === card.number);
if (firstIndex !== index) {
console.warn(`Duplicate card found: ${card.number}, removing duplicate`);
return false;
}
// Skip if already placed
if (this.placedCards.some(placed => placed && placed.number === card.number)) {
console.warn(`Card ${card.number} is both available and placed, removing from available`);
return false;
}
return true;
});
// Update currentOrder to clean version
this.currentOrder = uniqueAvailable;
this.currentOrder.forEach((card, index) => {
const cardEl = document.createElement('div');
cardEl.className = 'sort-card';
cardEl.dataset.number = card.number;
cardEl.innerHTML = `
<div class="revealed-number">${card.number}</div>
<div class="card-svg">${card.svg}</div>
`;
cardEl.addEventListener('click', (e) => this.handleCardClick(card, cardEl));
sortingArea.appendChild(cardEl);
});
}
handleCardClick(card, cardElement) {
// Clear any previously selected cards
document.querySelectorAll('.sort-card.selected').forEach(el => {
el.classList.remove('selected');
});
// Select this card
cardElement.classList.add('selected');
this.selectedCard = card;
this.selectedCardElement = cardElement;
// Highlight available positions and insert buttons
document.querySelectorAll('.position-slot').forEach(slot => {
if (!slot.classList.contains('filled')) {
slot.classList.add('active');
}
});
document.querySelectorAll('.insert-button').forEach(btn => {
btn.classList.add('active');
});
this.updateSortingStatus(`Selected card with value ${card.number}. Click a position or + button to place it.`);
}
handleInsertClick(insertPosition) {
if (!this.selectedCard) {
this.updateSortingStatus('Please select a card first, then click where to insert it.');
return;
}
// Handle insertion at the rightmost position (beyond current array bounds)
if (insertPosition >= this.selectedCount) {
// Find the rightmost empty position
let rightmostEmptyPos = -1;
for (let i = this.selectedCount - 1; i >= 0; i--) {
if (this.placedCards[i] === null) {
rightmostEmptyPos = i;
break;
}
}
if (rightmostEmptyPos === -1) {
this.updateSortingStatus('All positions are filled. Move a card first to make room.');
return;
}
// Place card in the rightmost empty position
this.placedCards[rightmostEmptyPos] = this.selectedCard;
} else {
// Create a new array for placed cards
const newPlacedCards = new Array(this.selectedCount).fill(null);
// Copy existing cards, shifting them as needed
for (let i = 0; i < this.placedCards.length; i++) {
if (this.placedCards[i] !== null) {
if (i < insertPosition) {
// Cards before insert position stay in same place
newPlacedCards[i] = this.placedCards[i];
} else {
// Cards at or after insert position shift right by 1
if (i + 1 < this.selectedCount) {
newPlacedCards[i + 1] = this.placedCards[i];
} else {
// Card would fall off - store temporarily, will handle below
newPlacedCards[i + 1] = this.placedCards[i];
}
}
}
}
// Place the selected card at the insert position
newPlacedCards[insertPosition] = this.selectedCard;
// Now apply gap-filling logic: shift cards left to compress and eliminate gaps
const compactedCards = [];
for (let i = 0; i < newPlacedCards.length; i++) {
if (newPlacedCards[i] !== null) {
compactedCards.push(newPlacedCards[i]);
}
}
// If we have more cards than positions, put excess back in available
if (compactedCards.length > this.selectedCount) {
const excessCards = compactedCards.slice(this.selectedCount);
this.currentOrder.push(...excessCards);
compactedCards.splice(this.selectedCount);
}
// Fill the final array with compacted cards (no gaps)
const finalPlacedCards = new Array(this.selectedCount).fill(null);
for (let i = 0; i < compactedCards.length; i++) {
finalPlacedCards[i] = compactedCards[i];
}
this.placedCards = finalPlacedCards;
}
// Remove card from available cards
this.currentOrder = this.currentOrder.filter(c => c.number !== this.selectedCard.number);
// Debug: Check total card count
this.debugCardCount('after insert');
// Clear selection and re-render
this.clearSelection();
this.updatePositionSlots();
this.renderAvailableCards();
// Update status
const placedCount = this.placedCards.filter(c => c !== null).length;
if (placedCount === this.selectedCount) {
this.updateSortingStatus('All cards placed! Click "Check My Solution" to see how you did.');
} else {
this.updateSortingStatus(`${placedCount}/${this.selectedCount} cards placed. Select another card to continue.`);
}
}
debugCardCount(context) {
const placedCount = this.placedCards.filter(c => c !== null).length;
const availableCount = this.currentOrder.length;
const totalCount = placedCount + availableCount;
console.log(`DEBUG ${context}: Placed=${placedCount}, Available=${availableCount}, Total=${totalCount}, Expected=${this.selectedCount}`);
if (totalCount !== this.selectedCount) {
console.error('Card count mismatch! Some cards are missing or duplicated.');
console.log('Placed cards:', this.placedCards.map(c => c ? c.number : 'empty'));
console.log('Available cards:', this.currentOrder.map(c => c.number));
}
}
updatePositionSlots() {
this.placedCards.forEach((card, position) => {
const slot = document.querySelector(`[data-position="${position}"]`);
if (!slot) return;
if (card) {
slot.classList.add('filled');
slot.innerHTML = `
<div class="slot-number">${position + 1}</div>
<div class="slot-card">
<div class="card-svg">${card.svg}</div>
</div>
<div class="slot-label">Click to move back</div>
`;
} else {
slot.classList.remove('filled');
slot.innerHTML = `
<div class="slot-number">${position + 1}</div>
<div class="slot-label">${position === 0 ? 'Smallest' : position === this.selectedCount - 1 ? 'Largest' : ''}</div>
`;
}
});
}
clearSelection() {
this.selectedCard = null;
this.selectedCardElement = null;
// Remove active states
document.querySelectorAll('.sort-card.selected').forEach(el => {
el.classList.remove('selected');
});
document.querySelectorAll('.position-slot.active').forEach(el => {
el.classList.remove('active');
});
document.querySelectorAll('.insert-button.active').forEach(el => {
el.classList.remove('active');
});
}
handleSlotClick(position) {
const slot = document.querySelector(`[data-position="${position}"]`);
// If no card is selected but slot has a card, move it back to available
if (!this.selectedCard && this.placedCards[position]) {
const cardToMove = this.placedCards[position];
// Remove card from this position
this.placedCards[position] = null;
// Add card back to available cards
this.currentOrder.push(cardToMove);
// Debug: Check total card count
this.debugCardCount('after moving card back');
// Update slot appearance
this.updatePositionSlots();
this.renderAvailableCards();
const placedCount = this.placedCards.filter(c => c !== null).length;
this.updateSortingStatus(`Moved card ${cardToMove.number} back to available cards. ${placedCount}/${this.selectedCount} cards placed.`);
return;
}
if (!this.selectedCard) {
this.updateSortingStatus('Select a card first, or click a placed card to move it back.');
return;
}
// If slot is already filled, replace the card
if (this.placedCards[position]) {
// Move the previous card back to available area
this.currentOrder.push(this.placedCards[position]);
}
// Place the selected card in this position
this.placedCards[position] = this.selectedCard;
// Remove card from current order (available cards)
this.currentOrder = this.currentOrder.filter(c => c.number !== this.selectedCard.number);
// Debug: Check total card count
this.debugCardCount('after regular placement');
// Update slot appearance
slot.classList.add('filled');
slot.innerHTML = `
<div class="slot-number">${position + 1}</div>
<div class="slot-card">
<div class="card-svg">${this.selectedCard.svg}</div>
</div>
<div class="slot-label">Click to move back</div>
`;
// Clear selection and re-render
this.clearSelection();
this.renderAvailableCards();
// Update status
const placedCount = this.placedCards.filter(c => c !== null).length;
if (placedCount === this.selectedCount) {
this.updateSortingStatus('All cards placed! Click "Check My Solution" to see how you did.');
} else {
this.updateSortingStatus(`${placedCount}/${this.selectedCount} cards placed. Select another card to continue.`);
}
}
handleDragEnd(e) {
if (this.previewOrder.length > 0) {
// Commit the preview order to the actual order
this.currentOrder = [...this.previewOrder];
}
// Clean up
this.resetDragState();
// Re-render with final positions
this.renderSortingCards();
// Add success animation
setTimeout(() => {
document.querySelectorAll('.sort-card').forEach(card => {
card.classList.add('success');
setTimeout(() => card.classList.remove('success'), 600);
});
}, 100);
}
handleDragOver(e) {
if (e.preventDefault) e.preventDefault();
e.dataTransfer.dropEffect = 'move';
// Calculate where to insert based on mouse position
const insertIndex = this.calculateInsertIndex(e.clientX, e.clientY);
console.log('DragOver - insertIndex:', insertIndex, 'lastInsertIndex:', this.lastInsertIndex);
if (insertIndex !== -1 && insertIndex !== this.lastInsertIndex) {
console.log('Calling updatePreviewOrder with insertIndex:', insertIndex);
this.updatePreviewOrder(insertIndex);
}
return false;
}
handleDragEnter(e) {
// Handle drag enter for the sorting area
}
handleDragLeave(e) {
// Handle drag leave
}
handleDrop(e) {
if (e.stopPropagation) e.stopPropagation();
// The preview order is already set, just let dragEnd handle the commit
return false;
}
calculateInsertIndex(clientX, clientY) {
const sortingArea = document.getElementById('sorting-area');
const cards = Array.from(sortingArea.querySelectorAll('.sort-card:not(.dragging)'));
if (cards.length === 0) return 0;
// Simple approach: find which card the mouse is over
for (let i = 0; i < cards.length; i++) {
const card = cards[i];
const rect = card.getBoundingClientRect();
// Check if mouse is over this card
if (clientX >= rect.left && clientX <= rect.right &&
clientY >= rect.top && clientY <= rect.bottom) {
const cardIndex = parseInt(card.dataset.index);
const cardCenterX = rect.left + rect.width / 2;
const insertIndex = clientX < cardCenterX ? cardIndex : cardIndex + 1;
return insertIndex;
}
}
return this.currentOrder.length;
}
updatePreviewOrder(insertIndex) {
if (this.lastInsertIndex === insertIndex) return; // No change needed
this.lastInsertIndex = insertIndex;
console.log('updatePreviewOrder called with insertIndex:', insertIndex);
console.log('currentOrder:', this.currentOrder.map(c => c.number));
console.log('draggedIndex:', this.draggedIndex);
// Create new preview order
const newOrder = [...this.currentOrder];
const draggedCard = newOrder.splice(this.draggedIndex, 1)[0];
// Adjust insert index if we removed an item before it
let adjustedInsertIndex = insertIndex;
if (this.draggedIndex < insertIndex) {
adjustedInsertIndex--;
}
// Insert at new position
newOrder.splice(adjustedInsertIndex, 0, draggedCard);
this.previewOrder = newOrder;
console.log('New preview order:', this.previewOrder.map(c => c.number));
// Update the visual layout immediately
this.renderPreview();
}
renderPreview() {
console.log('renderPreview called');
const sortingArea = document.getElementById('sorting-area');
// Get all cards except the dragged one
const cards = Array.from(sortingArea.querySelectorAll('.sort-card:not(.dragging)'));
console.log('Found cards (excluding dragged):', cards.length);
const draggedNumber = parseInt(this.draggedElement.dataset.number);
// Simply compare the full orders - if they're different, reorder
const previewNumbers = this.previewOrder.map(c => c.number);
const currentNumbers = this.currentOrder.map(c => c.number);
console.log('Preview order:', previewNumbers);
console.log('Current order:', currentNumbers);
// Check if the orders are different
const needsReorder = !previewNumbers.every((num, index) => currentNumbers[index] === num);
console.log('Needs reorder:', needsReorder);
if (needsReorder) {
console.log('Performing reorder...');
// Update currentOrder to match preview (this is key!)
this.currentOrder = [...this.previewOrder];
// Create a simple reordering without removing cards from DOM
// Just change their visual order using CSS flexbox order or reinsert in correct order
const previewWithoutDragged = this.previewOrder.filter(card => card.number !== draggedNumber);
console.log('Cards to reorder:', previewWithoutDragged.map(c => c.number));
console.log('Available DOM cards:', cards.map(c => parseInt(c.dataset.number)));
// Store references to all current cards
const cardElements = new Map();
cards.forEach(card => {
cardElements.set(parseInt(card.dataset.number), card);
});
// Reorder by moving each card to the correct position
previewWithoutDragged.forEach((card, targetIndex) => {
const cardEl = cardElements.get(card.number);
console.log('Finding card', card.number, 'found element:', !!cardEl);
if (cardEl) {
cardEl.dataset.index = targetIndex;
cardEl.querySelector('.card-position').textContent = targetIndex + 1;
// Move to correct position in DOM
const currentIndex = Array.from(sortingArea.children).indexOf(cardEl);
const targetPosition = targetIndex;
if (currentIndex !== targetPosition) {
if (targetPosition >= sortingArea.children.length - 1) {
sortingArea.appendChild(cardEl);
} else {
const nextSibling = sortingArea.children[targetPosition];
sortingArea.insertBefore(cardEl, nextSibling);
}
}
}
});
// Ensure dragged element stays at the end for z-index
if (this.draggedElement && this.draggedElement.parentNode === sortingArea) {
sortingArea.appendChild(this.draggedElement);
}
}
}
resetDragState() {
if (this.draggedElement) {
this.draggedElement.classList.remove('dragging');
this.draggedElement.style.transform = '';
this.draggedElement.style.position = '';
this.draggedElement.style.pointerEvents = '';
}
this.isDragging = false;
this.draggedElement = null;
this.draggedIndex = -1;
this.previewOrder = [];
this.lastInsertIndex = -1;
}
// Enhanced touch support for mobile with real-time reordering
handleTouchStart(e) {
const touch = e.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
this.touchStartElement = e.target.closest('.sort-card');
this.touchStartIndex = this.touchStartElement ? parseInt(this.touchStartElement.dataset.index) : -1;
if (this.touchStartElement) {
this.isDragging = true;
this.draggedElement = this.touchStartElement;
this.draggedIndex = this.touchStartIndex;
this.previewOrder = [...this.currentOrder];
this.lastInsertIndex = -1;
this.touchStartElement.classList.add('dragging');
this.createPlaceholder();
}
}
handleTouchMove(e) {
e.preventDefault();
if (this.touchStartElement && this.isDragging) {
const touch = e.touches[0];
// Update dragged element position
const deltaX = touch.clientX - this.touchStartX;
const deltaY = touch.clientY - this.touchStartY;
this.touchStartElement.style.transform = `translate(${deltaX}px, ${deltaY}px) rotate(5deg) scale(1.05)`;
// Calculate insert position and update preview
const insertIndex = this.calculateInsertIndex(touch.clientX, touch.clientY);
if (insertIndex !== -1) {
this.updatePreviewOrder(insertIndex);
}
}
}
handleTouchEnd(e) {
if (!this.touchStartElement || !this.isDragging) return;
// Commit the preview order
this.currentOrder = [...this.previewOrder];
// Clean up
this.touchStartElement.classList.remove('dragging');
this.touchStartElement.style.transform = '';
this.removePlaceholder();
this.isDragging = false;
this.draggedElement = null;
this.draggedIndex = -1;
this.previewOrder = [];
this.lastInsertIndex = -1;
// Re-render final positions
this.renderSortingCards();
// Success animation
setTimeout(() => {
document.querySelectorAll('.sort-card').forEach(card => {
card.classList.add('success');
setTimeout(() => card.classList.remove('success'), 600);
});
}, 100);
this.touchStartElement = null;
this.touchStartIndex = -1;
}
checkSolution() {
if (this.placedCards.some(card => card === null)) {
this.updateSortingStatus('Please place all cards before checking your solution.');
return;
}
// Get the sequences for comparison
const userSequence = this.placedCards.map(card => card.number);
const correctSequence = this.correctOrder.map(card => card.number);
// Calculate fair score using sequence alignment
const scoreResult = this.calculateSequenceScore(userSequence, correctSequence);
// Update visual feedback for each position
this.placedCards.forEach((card, index) => {
const slot = document.querySelector(`[data-position="${index}"]`);
const isExactMatch = card.number === this.correctOrder[index].number;
if (isExactMatch) {
slot.classList.add('correct');
slot.classList.remove('incorrect');
} else {
slot.classList.add('incorrect');
slot.classList.remove('correct');
}
});
this.showAdvancedFeedback(userSequence, correctSequence);
}
calculateSequenceScore(userSeq, correctSeq) {
// Calculate Longest Common Subsequence (LCS) score
const lcsLength = this.longestCommonSubsequence(userSeq, correctSeq);
// Calculate how many cards are in correct relative order
const relativeOrderScore = (lcsLength / correctSeq.length) * 100;
// Calculate exact position matches
let exactMatches = 0;
for (let i = 0; i < userSeq.length; i++) {
if (userSeq[i] === correctSeq[i]) {
exactMatches++;
}
}
const exactScore = (exactMatches / correctSeq.length) * 100;
// Calculate inversion count (how "scrambled" the sequence is)
const inversions = this.countInversions(userSeq, correctSeq);
const maxInversions = (correctSeq.length * (correctSeq.length - 1)) / 2;
const inversionScore = Math.max(0, ((maxInversions - inversions) / maxInversions) * 100);
// Weighted final score:
// - 50% for having cards in correct relative order (LCS)
// - 30% for exact position matches
// - 20% for overall sequence organization (inversions)
const finalScore = Math.round(
(relativeOrderScore * 0.5) +
(exactScore * 0.3) +
(inversionScore * 0.2)
);
return {
percentage: finalScore,
exactMatches: exactMatches,
correctRelativeOrder: lcsLength,
totalCards: correctSeq.length,
details: {
relativeOrderScore: Math.round(relativeOrderScore),
exactScore: Math.round(exactScore),
inversionScore: Math.round(inversionScore),
inversions: inversions
}
};
}
longestCommonSubsequence(seq1, seq2) {
const m = seq1.length;
const n = seq2.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (seq1[i - 1] === seq2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
countInversions(userSeq, correctSeq) {
// Create a mapping from value to correct position
const correctPositions = {};
correctSeq.forEach((val, idx) => {
correctPositions[val] = idx;
});
// Convert user sequence to "correct position" sequence
const userCorrectPositions = userSeq.map(val => correctPositions[val]);
// Count inversions in this position sequence
let inversions = 0;
for (let i = 0; i < userCorrectPositions.length; i++) {
for (let j = i + 1; j < userCorrectPositions.length; j++) {
if (userCorrectPositions[i] > userCorrectPositions[j]) {
inversions++;
}
}
}
return inversions;
}
showAdvancedFeedback(userSeq, correctSeq) {
const feedbackEl = document.getElementById('sorting-feedback');
// Calculate advanced scoring metrics
const lcsLength = this.longestCommonSubsequence(userSeq, correctSeq);
const relativeOrderScore = (lcsLength / correctSeq.length) * 100;
// Calculate exact position matches
let exactMatches = 0;
for (let i = 0; i < Math.min(userSeq.length, correctSeq.length); i++) {
if (userSeq[i] === correctSeq[i]) {
exactMatches++;
}
}
const exactScore = (exactMatches / correctSeq.length) * 100;
// Calculate inversion score
const inversions = this.countInversions(userSeq, correctSeq);
const maxInversions = (correctSeq.length * (correctSeq.length - 1)) / 2;
const inversionScore = Math.max(0, ((maxInversions - inversions) / maxInversions) * 100);
// Weighted final score
const finalScore = Math.round(
(relativeOrderScore * 0.5) +
(exactScore * 0.3) +
(inversionScore * 0.2)
);
const isPerfect = finalScore === 100;
let feedbackClass, message;
if (isPerfect) {
feedbackClass = 'feedback-perfect';
message = '🎉 Perfect! All cards in correct order!';
} else if (finalScore >= 80) {
feedbackClass = 'feedback-good';
message = '👍 Excellent! Very close to perfect!';
} else if (finalScore >= 60) {
feedbackClass = 'feedback-good';
message = '👍 Good job! You understand the pattern!';
} else {
feedbackClass = 'feedback-needs-work';
message = '💪 Keep practicing! Focus on reading each abacus carefully.';
}
feedbackEl.innerHTML = `
<div class="feedback-score">${finalScore}%</div>
<div class="feedback-message">${message}</div>
<div class="feedback-breakdown">
<div class="score-component">
<span class="component-label">Sequence Order:</span>
<span class="component-score">${Math.round(relativeOrderScore)}%</span>
<span class="component-weight">(50% weight)</span>
</div>
<div class="score-component">
<span class="component-label">Exact Positions:</span>
<span class="component-score">${Math.round(exactScore)}%</span>
<span class="component-weight">(30% weight)</span>
</div>
<div class="score-component">
<span class="component-label">Organization:</span>
<span class="component-score">${Math.round(inversionScore)}%</span>
<span class="component-weight">(20% weight)</span>
</div>
</div>
<div class="feedback-details">
Cards in correct order: ${lcsLength}/${correctSeq.length}
Exact position matches: ${exactMatches}/${correctSeq.length}
</div>
`;
feedbackEl.className = `sorting-feedback ${feedbackClass}`;
feedbackEl.style.display = 'block';
this.updateSortingStatus(isPerfect ? 'Perfect solution!' : `${finalScore}% score`);
}
revealNumbers() {
document.querySelectorAll('.sort-card').forEach(card => {
card.classList.add('revealed');
});
document.getElementById('reveal-numbers').style.display = 'none';
this.updateSortingStatus('Numbers revealed - now you can see the correct order!');
}
endSorting() {
// End the current sorting and return to configuration
this.resetSorting();
}
resetSorting() {
// Reset state
this.currentOrder = [];
this.correctOrder = [];
this.placedCards = [];
this.selectedCard = null;
this.selectedCardElement = null;
// Clear feedback
document.getElementById('sorting-feedback').style.display = 'none';
// Hide game, show configuration controls
document.getElementById('sorting-game').style.display = 'none';
document.querySelector('.sorting-controls').style.display = 'block';
// Reset buttons
document.getElementById('start-sorting').style.display = 'inline-block';
document.querySelector('.sorting-game-actions').style.display = 'none';
}
newChallenge() {
// Reset state but stay in game mode
this.currentOrder = [];
this.correctOrder = [];
this.placedCards = [];
this.selectedCard = null;
this.selectedCardElement = null;
// Clear feedback
document.getElementById('sorting-feedback').style.display = 'none';
// Clear card states
document.querySelectorAll('.sort-card').forEach(card => {
card.classList.remove('correct', 'incorrect', 'revealed');
});
// Start a new challenge (keeping game mode active)
this.startSorting();
}
updateSortingStatus(message) {
document.getElementById('sorting-status').textContent = message;
}
}
// Initialize quiz and sorting when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new ModalManager();
new SorobanQuiz();
new SortingChallenge();
});
</script>
</body>
</html>