soroban-abacus-flashcards/demo_heaven_bead_fix.html

4739 lines
178 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;
}
.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>
<button
id="check-sorting"
class="sort-check-btn"
style="display: none"
>
Check My Solution
</button>
<button
id="reveal-numbers"
class="sort-reveal-btn"
style="display: none"
>
Reveal Numbers
</button>
<button
id="new-sorting"
class="sort-new-btn"
style="display: none"
>
New Challenge
</button>
<button
id="end-sorting"
class="end-game-btn"
style="display: none"
>
End Sorting
</button>
</div>
</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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 2)"
>
<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 41)"
>
<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 57)"
>
<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 73)"
>
<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 89)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 2)"
>
<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 24)"
>
<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 57)"
>
<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 73)"
>
<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 89)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 2)"
>
<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 24)"
>
<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 40)"
>
<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 73)"
>
<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 89)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 2)"
>
<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 24)"
>
<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 40)"
>
<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 56)"
>
<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 89)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 2)"
>
<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 24)"
>
<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 40)"
>
<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 56)"
>
<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 72)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 7)"
>
<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 41)"
>
<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 57)"
>
<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 73)"
>
<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 89)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 7)"
>
<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 24)"
>
<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 57)"
>
<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 73)"
>
<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 89)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 7)"
>
<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 24)"
>
<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 40)"
>
<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 73)"
>
<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 89)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 7)"
>
<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 24)"
>
<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 40)"
>
<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 56)"
>
<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 89)"
>
<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 20)">
<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" style="color: #333">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 20.600000000000005)"
>
<g class="typst-group">
<g transform="matrix(0.8 0 0 0.8 2.5 11)">
<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 110 L 3 110 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 7)"
>
<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 24)"
>
<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 40)"
>
<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 56)"
>
<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 72)"
>
<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 20)">
<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" style="color: #333">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.getElementById("check-sorting").style.display =
"inline-block";
document.getElementById("reveal-numbers").style.display =
"inline-block";
document.getElementById("new-sorting").style.display = "inline-block";
document.getElementById("end-sorting").style.display = "inline-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.getElementById("check-sorting").style.display = "none";
document.getElementById("reveal-numbers").style.display = "none";
document.getElementById("new-sorting").style.display = "none";
document.getElementById("end-sorting").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>