elyor-ml commited on
Commit
0b49cb3
·
1 Parent(s): 75f8988

equal step 2 game

Browse files
Files changed (7) hide show
  1. Dockerfile +2 -2
  2. README.md +163 -157
  3. index.html +1 -1
  4. package-lock.json +4 -4
  5. package.json +2 -2
  6. src/App.css +224 -90
  7. src/App.tsx +227 -252
Dockerfile CHANGED
@@ -23,5 +23,5 @@ RUN npm install -g serve
23
  COPY --from=build /app/dist ./dist
24
 
25
  # Expose port and start server
26
- EXPOSE 3000
27
- CMD ["serve", "-s", "dist", "-l", "3000"]
 
23
  COPY --from=build /app/dist ./dist
24
 
25
  # Expose port and start server
26
+ EXPOSE 7860
27
+ CMD ["serve", "-s", "dist", "-l", "7860"]
README.md CHANGED
@@ -1,185 +1,191 @@
1
  ---
2
- title: Equal =
3
- emoji: 📊
4
  colorFrom: blue
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
8
- app_port: 3000
9
  ---
10
- # Colorful Semantics - To Whom? Game
11
 
12
- A learning game that teaches sentence structure through colorful semantic components.
13
 
14
- ## Features
15
 
16
- - Interactive game for learning "to whom" sentence components
17
- - Visual and animated feedback for correct answers
18
- - Support for both images and videos
19
- - Customizable questions and answers
20
 
21
- ## How to Play
22
 
23
- 1. Look at the image/video and read the colored sentence parts
24
- 2. Choose the correct "to whom" option from the choices below
25
- 3. The game will animate the correct answer to the pink box
26
- 4. Continue to the next question automatically
 
27
 
28
- ## Adding New Questions
 
 
 
 
 
29
 
30
- 1. Add your image or video to the `public/media` folder
31
- 2. Edit the `public/questions.json` file to add a new question:
 
32
 
33
- ```json
34
- {
35
- "file": "media/your_image.jpg",
36
- "who": "The boy",
37
- "doing": "is giving",
38
- "what": "a book",
39
- "to_whom": "to the teacher",
40
- "distractors": ["to Mom", "to the dog", "to Grandma"]
41
- }
42
- ```
43
 
44
- 3. Each question needs the following fields:
 
 
 
 
 
45
 
46
- - `file`: Path to the media file (jpg, png, or mp4)
47
- - `who`: The subject of the sentence
48
- - `doing`: The verb phrase
49
- - `what`: The object
50
- - `to_whom`: The correct answer (recipient)
51
- - `distractors`: Array of incorrect options
52
- 4. Restart the application to see your new questions
53
 
54
- ## Development
55
 
56
  ```bash
 
 
 
 
57
  # Install dependencies
58
  npm install
59
 
60
  # Start development server
61
  npm run dev
 
62
 
 
 
 
 
 
63
  # Build for production
64
  npm run build
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  ```
66
 
67
- ## Making sentences
68
-
69
- The words used for subject and to whome:
70
-
71
- ## subjects and to whom
72
-
73
- Blippi
74
- Zaki
75
- Harun
76
- Rashid
77
- Kazwa
78
- Bilal
79
- honeybee
80
- dinasaour
81
- boy
82
- girl
83
- man
84
- woman
85
- lady
86
- children
87
- kids
88
- cat
89
- dog
90
- mouse
91
- tiger
92
- witch
93
- elephent
94
- Mustafo
95
- Muhammad
96
- Hidoyat
97
- Sardor
98
- Ulugbek
99
- Policeman
100
- Nurse
101
- Pilot
102
- Florist
103
- Builder
104
- Teacher
105
- Doctor
106
- Dentist
107
- Binman
108
- Postman
109
- Firefighter
110
- Lollypop lady
111
- Gardner
112
- Mechanic
113
- Painter
114
- Lifeguard
115
- librarian
116
- bus driver
117
- cook / chef
118
- barber
119
- Farmer
120
- Astronaut
121
- Paramedic
122
-
123
- ### verbs
124
-
125
- read
126
- drink
127
- case
128
- sit
129
- buy
130
- jump
131
- hold
132
- cry
133
- ride
134
- wash
135
- fly
136
- eat
137
- dry
138
- play
139
- climb
140
- scratch
141
- cut
142
- brush
143
- bounce
144
- argue
145
- work
146
- fight
147
- practice
148
- throw
149
- take
150
- bring
151
- take
152
- wipe
153
- drink
154
-
155
- ## what
156
-
157
- flower
158
- icecream
159
- pen
160
- pencil
161
- socks
162
- underwear
163
- coat
164
- wellies
165
- shoes
166
- spoon
167
- fork
168
- food
169
- bread
170
- butter
171
- salt
172
- chilly
173
- bathroom
174
- towel
175
- coffee
176
- tea
177
- milk
178
- almond
179
- monkey nuts
180
- upstairs
181
- downstairs
182
-
183
- ## License
184
-
185
- MIT
 
1
  ---
2
+ title: Equal 2-Step Game
3
+ emoji: ⚖️
4
  colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ app_port: 7860
9
  ---
 
10
 
11
+ # Equal 2-Step Game ⚖️
12
 
13
+ An interactive educational game that teaches equality comparison and counting skills through engaging emoji-based questions. Perfect for children learning basic math concepts!
14
 
15
+ ## 🎮 How to Play
 
 
 
16
 
17
+ The game consists of **two sequential steps** that help children develop comparison and counting skills:
18
 
19
+ ### Step 1: Equality Check
20
+ - Look at the two sides with emojis
21
+ - Count the items on each side
22
+ - Answer the question: **"Are they equal?"**
23
+ - Choose between **"Equal"** or **"Not Equal"**
24
 
25
+ ### Step 2: Comparison (Only if Not Equal)
26
+ - If you correctly answered "Not Equal" in Step 1, you proceed to Step 2
27
+ - The game asks either:
28
+ - **"Which side has more?"** or
29
+ - **"Which side has fewer?"**
30
+ - Click on the correct side to answer
31
 
32
+ ### Game Flow
33
+ 1. **Equal sides**: Step 1 Correct answer New question
34
+ 2. **Unequal sides**: Step 1 → Step 2 → Correct answer → New question
35
 
36
+ ## ✨ Features
 
 
 
 
 
 
 
 
 
37
 
38
+ - **Visual Learning**: Uses colorful emojis from different categories (animals, fruits, objects, hearts, stars)
39
+ - **Two-Step Logic**: Teaches both equality and comparison concepts
40
+ - **Immediate Feedback**: Shows "Correct! 🎉" or "Try again! 🤔" messages
41
+ - **Auto-Generated Questions**: Random emoji selection and counts (1-10 items)
42
+ - **Responsive Design**: Works on desktop, tablet, and mobile devices
43
+ - **Clean UI**: Simple, child-friendly interface with large buttons and clear visuals
44
 
45
+ ## 🚀 How to Run
 
 
 
 
 
 
46
 
47
+ ### Option 1: Development Mode
48
 
49
  ```bash
50
+ # Clone the repository
51
+ git clone <repository-url>
52
+ cd equal-2-step-game
53
+
54
  # Install dependencies
55
  npm install
56
 
57
  # Start development server
58
  npm run dev
59
+ ```
60
 
61
+ The game will be available at `http://localhost:5173`
62
+
63
+ ### Option 2: Production Build
64
+
65
+ ```bash
66
  # Build for production
67
  npm run build
68
+
69
+ # Preview production build
70
+ npm run preview
71
+ ```
72
+
73
+ ### Option 3: Docker
74
+
75
+ ```bash
76
+ # Build Docker image
77
+ docker build -t equal-2-step-game .
78
+
79
+ # Run container
80
+ docker run -p 3000:3000 equal-2-step-game
81
+ ```
82
+
83
+ The game will be available at `http://localhost:3000`
84
+
85
+ ## 🎯 Educational Benefits
86
+
87
+ - **Counting Skills**: Children practice counting objects up to 10
88
+ - **Equality Concepts**: Understanding when quantities are the same
89
+ - **Comparison Skills**: Learning "more than" and "fewer than" concepts
90
+ - **Visual Recognition**: Identifying and categorizing different emoji types
91
+ - **Logical Thinking**: Following two-step problem-solving processes
92
+
93
+ ## 🎨 Game Categories
94
+
95
+ The game uses emojis from 5 different categories:
96
+
97
+ - **Animals**: 🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐸 🐷
98
+ - **Fruits**: 🍎 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍒 🥝
99
+ - **Objects**: ⚽ 🏀 🎾 🏈 🎱 🎪 🎨 🎭 🎪 🎯
100
+ - **Hearts**: 💙 💚 💛 💜 🤍 🖤 🤎 💗 💖 💕
101
+ - **Stars**: ⭐ 🌟 💫 ✨ 🌠 ⚡ 🔥 ❄️ ☀️ 🌙
102
+
103
+ ## 🛠️ Technical Details
104
+
105
+ ### Technologies Used
106
+ - **React 19** with TypeScript
107
+ - **Vite** for build tooling
108
+ - **CSS3** with modern features (Grid, Flexbox, Animations)
109
+ - **Docker** for deployment
110
+
111
+ ### Project Structure
112
+ ```
113
+ src/
114
+ ├── App.tsx # Main game component
115
+ ├── App.css # Game styling
116
+ ├── main.tsx # App entry point
117
+ └── index.css # Global styles
118
+
119
+ public/
120
+ ├── favicon.svg # App icon
121
+ └── index.html # HTML template
122
  ```
123
 
124
+ ### Game Logic
125
+ - **Question Generation**: Random emoji selection from categories
126
+ - **Count Generation**: Random numbers 1-10 for each side
127
+ - **Equality Control**: 50% chance for equal vs unequal questions
128
+ - **Step Control**: Step 2 only appears for unequal quantities
129
+ - **Safety Checks**: Prevents step 2 from showing with equal sides
130
+
131
+ ## 🎨 Customization
132
+
133
+ You can easily customize the game by modifying the emoji categories in `src/App.tsx`:
134
+
135
+ ```typescript
136
+ const emojiCategories = {
137
+ animals: ['🐶', '🐱', ...], // Add your animal emojis
138
+ fruits: ['🍎', '🍊', ...], // Add your fruit emojis
139
+ // Add more categories...
140
+ };
141
+ ```
142
+
143
+ ## 📱 Mobile Support
144
+
145
+ The game is fully responsive and optimized for:
146
+ - **Desktop**: Full-featured experience with hover effects
147
+ - **Tablet**: Touch-friendly interface with optimized sizing
148
+ - **Mobile**: Compact layout with large touch targets
149
+
150
+ ## 🚀 Deployment
151
+
152
+ ### Hugging Face Spaces
153
+ This project is configured for easy deployment to Hugging Face Spaces using Docker.
154
+
155
+ ### Other Platforms
156
+ The built project can be deployed to any static hosting service:
157
+ - Vercel
158
+ - Netlify
159
+ - GitHub Pages
160
+ - AWS S3
161
+ - And more...
162
+
163
+ ## 🤝 Contributing
164
+
165
+ 1. Fork the repository
166
+ 2. Create a feature branch: `git checkout -b feature-name`
167
+ 3. Make your changes
168
+ 4. Test thoroughly
169
+ 5. Submit a pull request
170
+
171
+ ## 📄 License
172
+
173
+ MIT License - feel free to use this project for educational purposes!
174
+
175
+ ## 🎓 Age Recommendation
176
+
177
+ This game is designed for children ages **3-8** who are learning:
178
+ - Basic counting (1-10)
179
+ - Equality concepts
180
+ - Comparison skills
181
+ - Visual recognition
182
+
183
+ Perfect for:
184
+ - **Preschool** mathematics preparation
185
+ - **Kindergarten** counting practice
186
+ - **Early elementary** comparison skills
187
+ - **Special education** visual learning support
188
+
189
+ ---
190
+
191
+ Made with ❤️ for young learners everywhere!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Colorful Semantics - To Whom?</title>
8
  </head>
9
  <body>
10
  <div id="root"></div>
 
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Equal 2-Step Game</title>
8
  </head>
9
  <body>
10
  <div id="root"></div>
package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
  {
2
- "name": "verbs-past",
3
- "version": "0.0.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
- "name": "verbs-past",
9
- "version": "0.0.0",
10
  "dependencies": {
11
  "react": "^19.0.0",
12
  "react-dom": "^19.0.0"
 
1
  {
2
+ "name": "equal-2-step-game",
3
+ "version": "1.0.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
+ "name": "equal-2-step-game",
9
+ "version": "1.0.0",
10
  "dependencies": {
11
  "react": "^19.0.0",
12
  "react-dom": "^19.0.0"
package.json CHANGED
@@ -1,9 +1,9 @@
1
  {
2
- "name": "colorful-semantics-game",
3
  "private": true,
4
  "version": "1.0.0",
5
  "type": "module",
6
- "description": "An educational game for teaching sentence structure using colorful semantics",
7
  "scripts": {
8
  "dev": "vite",
9
  "build": "vite build",
 
1
  {
2
+ "name": "equal-2-step-game",
3
  "private": true,
4
  "version": "1.0.0",
5
  "type": "module",
6
+ "description": "A 2-step educational game for teaching equality comparison using emojis",
7
  "scripts": {
8
  "dev": "vite",
9
  "build": "vite build",
src/App.css CHANGED
@@ -1,15 +1,16 @@
1
- .colorful-semantics {
2
- max-width: 1100px;
3
- margin: 0 0 0 auto;
4
- padding: 2rem;
5
- text-align: center;
6
- font-family: 'Arial', sans-serif;
7
  display: flex;
8
  flex-direction: column;
9
  align-items: center;
10
- justify-content: flex-end;
 
 
 
11
  }
12
 
 
13
  .loading {
14
  display: flex;
15
  justify-content: center;
@@ -17,132 +18,265 @@
17
  height: 100vh;
18
  font-size: 1.5rem;
19
  font-weight: bold;
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
- .media-container {
23
- position: relative;
24
- width: 100%;
25
- max-width: 900px;
26
- height: 55vh;
27
- max-height: 500px;
28
- margin: 0 auto 3rem;
29
  display: flex;
 
30
  justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  align-items: center;
32
- overflow: hidden;
33
- border-radius: 12px;
34
- box-shadow: 0 6px 14px rgba(0, 0, 0, 0.25);
35
- background-color: #f0f0f0;
36
  }
37
 
38
- .game-media {
39
- max-width: 100%;
40
- max-height: 100%;
41
- object-fit: contain;
42
  }
43
 
44
- .semantic-boxes {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  display: flex;
46
- gap: 1rem;
47
  justify-content: center;
48
- flex-wrap: nowrap;
49
- margin-bottom: 2rem;
50
  }
51
 
52
- .semantic-box {
53
- /* Let width grow/shrink based on content so long phrases stay on one line */
54
- width: auto;
55
- min-width: 140px;
56
- padding: 1.4rem 1.6rem;
57
- border-radius: 10px;
58
- border: 3px solid #000;
59
- font-size: 1.6rem;
60
  font-weight: bold;
61
- box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
 
 
 
 
62
  display: flex;
63
- justify-content: center;
64
  align-items: center;
65
- min-height: 90px;
66
- text-align: center;
67
- white-space: nowrap; /* prevent text from wrapping to a new line */
68
  }
69
 
70
- .options-container {
 
71
  display: flex;
72
- gap: 1rem;
73
  justify-content: center;
74
  flex-wrap: wrap;
75
- margin-top: 2rem;
76
  }
77
 
78
- .option {
79
- background-color: #d3d3d3;
80
- border: 3px solid #000;
81
- border-radius: 8px;
82
- padding: 1rem 1.5rem;
83
- font-size: 1.3rem;
84
  cursor: pointer;
85
- transition: transform 0.15s, box-shadow 0.15s;
86
- min-width: 140px;
87
- box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
88
  }
89
 
90
- .option:hover {
91
- transform: scale(1.05);
92
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
93
- }
94
-
95
- .flying-option {
96
- padding: 0.8rem 1.2rem;
97
- border: 2px solid #000;
98
- border-radius: 6px;
99
- font-size: 1.1rem;
100
- pointer-events: none;
101
- z-index: 100;
102
- min-width: 120px;
103
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
104
- white-space: nowrap;
105
- display: flex;
106
- justify-content: center;
107
- align-items: center;
108
  }
109
 
110
- .feedback-message {
111
- color: #ff3c3c;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  font-size: 1.2rem;
113
- font-weight: bold;
114
- margin-top: 1.5rem;
115
- min-height: 28px;
 
 
 
 
116
  }
117
 
118
- .big-question {
 
119
  position: fixed;
120
- right: 220px;
121
- top: 30%;
122
- font-size: 6rem;
 
 
 
123
  font-weight: bold;
124
- color: #FF69B4;
125
- text-align: left;
126
- text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
127
- z-index: 10;
 
128
  }
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  @media (max-width: 768px) {
131
- .semantic-boxes {
132
- flex-wrap: wrap;
133
- align-items: center;
134
  }
135
 
136
- .semantic-box {
137
- width: 80%;
 
138
  }
139
 
140
- .options-container {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  flex-direction: column;
142
  align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
144
 
145
- .option {
146
- width: 80%;
147
  }
148
  }
 
1
+ /* Main container */
2
+ .equal-game {
3
+ min-height: 100vh;
 
 
 
4
  display: flex;
5
  flex-direction: column;
6
  align-items: center;
7
+ justify-content: center;
8
+ padding: 2rem;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ font-family: 'Arial', sans-serif;
11
  }
12
 
13
+ /* Loading state */
14
  .loading {
15
  display: flex;
16
  justify-content: center;
 
18
  height: 100vh;
19
  font-size: 1.5rem;
20
  font-weight: bold;
21
+ color: white;
22
+ }
23
+
24
+ /* Question title */
25
+ .question-title {
26
+ font-size: 3rem;
27
+ font-weight: bold;
28
+ color: white;
29
+ text-align: center;
30
+ margin-bottom: 2rem;
31
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
32
  }
33
 
34
+ /* Comparison container */
35
+ .comparison-container {
 
 
 
 
 
36
  display: flex;
37
+ align-items: center;
38
  justify-content: center;
39
+ gap: 3rem;
40
+ margin-bottom: 3rem;
41
+ flex-wrap: wrap;
42
+ }
43
+
44
+ /* Side container */
45
+ .side-container {
46
+ background: white;
47
+ border-radius: 20px;
48
+ padding: 2rem;
49
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
50
+ min-width: 300px;
51
+ max-width: 400px;
52
+ display: flex;
53
+ flex-direction: column;
54
  align-items: center;
55
+ gap: 1rem;
56
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
 
 
57
  }
58
 
59
+ .side-container.clickable {
60
+ cursor: pointer;
 
 
61
  }
62
 
63
+ .side-container.clickable:hover {
64
+ transform: scale(1.05);
65
+ box-shadow: 0 12px 35px rgba(0,0,0,0.2);
66
+ }
67
+
68
+ /* Emoji grid */
69
+ .emoji-grid {
70
+ display: grid;
71
+ grid-template-columns: repeat(auto-fit, minmax(40px, 1fr));
72
+ gap: 8px;
73
+ max-width: 300px;
74
+ justify-items: center;
75
+ }
76
+
77
+ .emoji-item {
78
+ font-size: 2rem;
79
+ display: block;
80
+ animation: popIn 0.3s ease-out;
81
+ }
82
+
83
+ @keyframes popIn {
84
+ 0% {
85
+ transform: scale(0);
86
+ }
87
+ 80% {
88
+ transform: scale(1.1);
89
+ }
90
+ 100% {
91
+ transform: scale(1);
92
+ }
93
+ }
94
+
95
+ /* Count display */
96
+ .count-display {
97
+ font-size: 2.5rem;
98
+ font-weight: bold;
99
+ color: #333;
100
+ background: #f0f0f0;
101
+ border-radius: 50%;
102
+ width: 80px;
103
+ height: 80px;
104
  display: flex;
105
+ align-items: center;
106
  justify-content: center;
107
+ border: 3px solid #ddd;
 
108
  }
109
 
110
+ /* VS divider */
111
+ .vs-divider {
112
+ font-size: 2rem;
 
 
 
 
 
113
  font-weight: bold;
114
+ color: white;
115
+ background: rgba(255,255,255,0.2);
116
+ border-radius: 50%;
117
+ width: 80px;
118
+ height: 80px;
119
  display: flex;
 
120
  align-items: center;
121
+ justify-content: center;
122
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
 
123
  }
124
 
125
+ /* Answer buttons for step 1 */
126
+ .answer-buttons {
127
  display: flex;
128
+ gap: 2rem;
129
  justify-content: center;
130
  flex-wrap: wrap;
 
131
  }
132
 
133
+ .answer-btn {
134
+ padding: 1.5rem 3rem;
135
+ font-size: 1.5rem;
136
+ font-weight: bold;
137
+ border: none;
138
+ border-radius: 15px;
139
  cursor: pointer;
140
+ transition: all 0.3s ease;
141
+ min-width: 150px;
142
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
143
  }
144
 
145
+ .equal-btn {
146
+ background: linear-gradient(45deg, #4CAF50, #45a049);
147
+ color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }
149
 
150
+ .equal-btn:hover {
151
+ background: linear-gradient(45deg, #45a049, #4CAF50);
152
+ transform: translateY(-2px);
153
+ box-shadow: 0 6px 20px rgba(0,0,0,0.3);
154
+ }
155
+
156
+ .not-equal-btn {
157
+ background: linear-gradient(45deg, #f44336, #d32f2f);
158
+ color: white;
159
+ }
160
+
161
+ .not-equal-btn:hover {
162
+ background: linear-gradient(45deg, #d32f2f, #f44336);
163
+ transform: translateY(-2px);
164
+ box-shadow: 0 6px 20px rgba(0,0,0,0.3);
165
+ }
166
+
167
+ /* Step 2 instruction */
168
+ .step2-instruction {
169
  font-size: 1.2rem;
170
+ color: white;
171
+ text-align: center;
172
+ margin-top: 1rem;
173
+ background: rgba(255,255,255,0.1);
174
+ padding: 1rem 2rem;
175
+ border-radius: 10px;
176
+ backdrop-filter: blur(10px);
177
  }
178
 
179
+ /* Feedback message */
180
+ .feedback-message {
181
  position: fixed;
182
+ top: 50%;
183
+ left: 50%;
184
+ transform: translate(-50%, -50%);
185
+ background: white;
186
+ color: #333;
187
+ font-size: 2rem;
188
  font-weight: bold;
189
+ padding: 2rem 3rem;
190
+ border-radius: 20px;
191
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
192
+ z-index: 1000;
193
+ animation: feedbackPop 0.5s ease-out;
194
  }
195
 
196
+ @keyframes feedbackPop {
197
+ 0% {
198
+ transform: translate(-50%, -50%) scale(0);
199
+ }
200
+ 80% {
201
+ transform: translate(-50%, -50%) scale(1.1);
202
+ }
203
+ 100% {
204
+ transform: translate(-50%, -50%) scale(1);
205
+ }
206
+ }
207
+
208
+ /* Responsive design */
209
  @media (max-width: 768px) {
210
+ .question-title {
211
+ font-size: 2rem;
212
+ margin-bottom: 1.5rem;
213
  }
214
 
215
+ .comparison-container {
216
+ gap: 2rem;
217
+ flex-direction: column;
218
  }
219
 
220
+ .side-container {
221
+ min-width: 250px;
222
+ max-width: 300px;
223
+ padding: 1.5rem;
224
+ }
225
+
226
+ .vs-divider {
227
+ width: 60px;
228
+ height: 60px;
229
+ font-size: 1.5rem;
230
+ }
231
+
232
+ .emoji-item {
233
+ font-size: 1.5rem;
234
+ }
235
+
236
+ .count-display {
237
+ width: 60px;
238
+ height: 60px;
239
+ font-size: 2rem;
240
+ }
241
+
242
+ .answer-buttons {
243
  flex-direction: column;
244
  align-items: center;
245
+ gap: 1rem;
246
+ }
247
+
248
+ .answer-btn {
249
+ padding: 1rem 2rem;
250
+ font-size: 1.2rem;
251
+ min-width: 200px;
252
+ }
253
+
254
+ .feedback-message {
255
+ font-size: 1.5rem;
256
+ padding: 1.5rem 2rem;
257
+ }
258
+ }
259
+
260
+ @media (max-width: 480px) {
261
+ .equal-game {
262
+ padding: 1rem;
263
+ }
264
+
265
+ .question-title {
266
+ font-size: 1.5rem;
267
+ }
268
+
269
+ .side-container {
270
+ min-width: 200px;
271
+ max-width: 250px;
272
+ padding: 1rem;
273
+ }
274
+
275
+ .emoji-grid {
276
+ max-width: 200px;
277
  }
278
 
279
+ .emoji-item {
280
+ font-size: 1.2rem;
281
  }
282
  }
src/App.tsx CHANGED
@@ -1,286 +1,261 @@
1
- import { useState, useEffect, useRef } from 'react'
2
  import './App.css'
3
 
4
- // Question interface
5
- interface Question {
6
- file: string;
7
- who: string;
8
- doing: string;
9
- what: string;
10
- to_whom: string;
11
- distractors: string[];
12
  }
13
 
14
- // State for one round
15
- interface GameState {
16
- frames: string[];
17
- frameIndex: number;
18
- who: string;
19
- doing: string;
20
- what: string;
21
- answer: string;
22
- choices: string[];
23
- }
24
 
25
  function App() {
26
- const [questions, setQuestions] = useState<Question[]>([]);
27
- const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
28
- const [gameState, setGameState] = useState<GameState | null>(null);
29
- const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight });
30
- const [feedbackMsg, setFeedbackMsg] = useState('');
31
- const [isAnimating, setIsAnimating] = useState(false);
32
- const [animationElement, setAnimationElement] = useState<{ content: string, position: { x: number, y: number } } | null>(null);
33
- const [isFilled, setIsFilled] = useState(false);
34
-
35
- const wrongMessages = ["Wrong, try again", "Not correct, try again", "Error, come again"];
36
- const animationRef = useRef<HTMLDivElement>(null);
37
- const destinationRef = useRef<HTMLDivElement>(null);
38
-
39
- // Colors
40
- const ORANGE = "#FF8C00";
41
- const YELLOW = "#F0E61E";
42
- const GREEN = "#00B900";
43
- const PINK = "#FF69B4";
44
-
45
- // Load questions on mount
46
- useEffect(() => {
47
- const loadQuestions = async () => {
48
- try {
49
- const response = await fetch('/questions.json');
50
- if (!response.ok) {
51
- throw new Error('Failed to load questions');
52
- }
53
- const data: Question[] = await response.json();
54
- // Shuffle questions
55
- const shuffled = [...data].sort(() => Math.random() - 0.5);
56
- setQuestions(shuffled);
57
- } catch (error) {
58
- console.error('Error loading questions:', error);
59
- }
60
- };
61
 
62
- loadQuestions();
 
63
 
64
- // Set up window resize handler
65
- const handleResize = () => {
66
- setWindowSize({
67
- width: window.innerWidth,
68
- height: window.innerHeight
69
- });
70
- };
71
 
72
- window.addEventListener('resize', handleResize);
73
- return () => window.removeEventListener('resize', handleResize);
74
- }, []);
75
-
76
- // Prepare game state when questions load or change
77
- useEffect(() => {
78
- if (questions.length > 0) {
79
- prepareRound(questions[currentQuestionIndex]);
80
  }
81
- }, [questions, currentQuestionIndex]);
82
-
83
- // Handle video/image frame updates
84
- useEffect(() => {
85
- if (!gameState || !gameState.frames.length) return;
86
 
87
- const frameInterval = setInterval(() => {
88
- if (!isAnimating && !isFilled) {
89
- setGameState(prev => {
90
- if (!prev) return prev;
91
- const nextIndex = (prev.frameIndex + 1) % prev.frames.length;
92
- return { ...prev, frameIndex: nextIndex };
93
- });
94
- }
95
- }, 100); // Adjust frame rate as needed
96
 
97
- return () => clearInterval(frameInterval);
98
- }, [gameState, isAnimating, isFilled]);
99
-
100
- // Process animation completion and next question timing
 
 
 
 
 
 
 
 
101
  useEffect(() => {
102
- if (isFilled) {
103
- const timer = setTimeout(() => {
104
- // Move to next question
105
- setCurrentQuestionIndex(prev => (prev + 1) % questions.length);
106
- setIsFilled(false);
107
- setFeedbackMsg('');
108
- setAnimationElement(null);
109
- }, 1000);
110
-
111
- return () => clearTimeout(timer);
112
- }
113
- }, [isFilled, questions.length]);
114
-
115
- // Animation effect
116
  useEffect(() => {
117
- if (isAnimating && animationElement && animationRef.current && destinationRef.current) {
118
- const destRect = destinationRef.current.getBoundingClientRect();
119
- const targetPosition = {
120
- x: destRect.left + destRect.width / 2,
121
- y: destRect.top + destRect.height / 2
122
- };
123
-
124
- const anim = animationRef.current;
125
- anim.style.transition = 'transform 500ms linear';
126
- anim.style.transform = `translate(${targetPosition.x - animationElement.position.x}px, ${targetPosition.y - animationElement.position.y}px)`;
127
-
128
- const handleAnimationEnd = () => {
129
- setIsAnimating(false);
130
- setIsFilled(true);
131
- };
132
-
133
- anim.addEventListener('transitionend', handleAnimationEnd);
134
- return () => anim.removeEventListener('transitionend', handleAnimationEnd);
135
  }
136
- }, [isAnimating, animationElement]);
137
-
138
- // Prepare a new game round
139
- const prepareRound = (question: Question) => {
140
- const choices = [question.to_whom, ...question.distractors];
141
- // Shuffle choices
142
- const shuffledChoices = [...choices].sort(() => Math.random() - 0.5);
143
-
144
- // Determine if media is image or video
145
- const isVideo = question.file.endsWith('.mp4');
146
- const frames = isVideo
147
- ? [question.file] // For simplicity, we'll just use the path for videos
148
- : [question.file]; // Same for images
149
 
150
- setGameState({
151
- frames,
152
- frameIndex: 0,
153
- who: question.who,
154
- doing: question.doing,
155
- what: question.what,
156
- answer: question.to_whom,
157
- choices: shuffledChoices
158
- });
159
 
160
- setFeedbackMsg('');
161
- setIsAnimating(false);
162
- setAnimationElement(null);
163
- setIsFilled(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  };
165
-
166
- // Handle option selection
167
- const handleOptionClick = (option: string, event: React.MouseEvent<HTMLDivElement>) => {
168
- if (isAnimating || isFilled || !gameState) return;
169
 
170
- const rect = event.currentTarget.getBoundingClientRect();
171
- const position = {
172
- x: rect.left + rect.width / 2,
173
- y: rect.top + rect.height / 2
174
- };
175
 
176
- if (option === gameState.answer) {
177
- // Correct answer
178
- setAnimationElement({
179
- content: option,
180
- position
181
- });
182
- setIsAnimating(true);
183
- setFeedbackMsg('');
184
- } else {
185
- // Wrong answer
186
- setFeedbackMsg(wrongMessages[Math.floor(Math.random() * wrongMessages.length)]);
187
  }
188
- };
189
-
190
- // Render current media (image or video)
191
- const renderMedia = () => {
192
- if (!gameState || !gameState.frames.length) return null;
193
 
194
- const currentFrame = gameState.frames[gameState.frameIndex];
195
- const isVideo = currentFrame.endsWith('.mp4');
196
 
197
- if (isVideo) {
198
- return (
199
- <video
200
- className="game-media"
201
- src={currentFrame}
202
- autoPlay
203
- loop
204
- muted
205
- />
206
- );
 
 
 
 
 
 
 
 
 
 
207
  } else {
208
- return (
209
- <img
210
- className="game-media"
211
- src={currentFrame}
212
- alt="Game visual"
213
- />
214
- );
215
  }
216
  };
217
-
218
- if (!gameState) {
219
- return <div className="loading">Loading questions...</div>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  }
221
-
222
  return (
223
- <div className="colorful-semantics">
224
- <div className="big-question">To whom?</div>
225
- <div className="media-container">
226
- {renderMedia()}
227
- </div>
228
-
229
- <div className="semantic-boxes">
230
- {/* Who - Orange */}
231
- <div className="semantic-box" style={{ backgroundColor: ORANGE }}>
232
- {gameState.who}
233
- </div>
234
-
235
- {/* Doing - Yellow */}
236
- <div className="semantic-box" style={{ backgroundColor: YELLOW }}>
237
- {gameState.doing}
238
- </div>
239
-
240
- {/* What - Green */}
241
- <div className="semantic-box" style={{ backgroundColor: GREEN }}>
242
- {gameState.what}
243
- </div>
244
-
245
- {/* To Whom - Pink */}
246
- <div
247
- className="semantic-box"
248
- style={{ backgroundColor: PINK }}
249
- ref={destinationRef}
250
- >
251
- {isFilled ? gameState.answer : "to whom?"}
252
- </div>
253
- </div>
254
-
255
- <div className="options-container">
256
- {!isFilled && gameState.choices.map((option, index) => (
257
- <div
258
- key={index}
259
- className="option"
260
- onClick={(e) => handleOptionClick(option, e)}
261
- >
262
- {option}
263
  </div>
264
- ))}
265
- </div>
266
-
267
- {feedbackMsg && (
268
- <div className="feedback-message">{feedbackMsg}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  )}
270
 
271
- {isAnimating && animationElement && (
272
- <div
273
- className="flying-option"
274
- ref={animationRef}
275
- style={{
276
- backgroundColor: PINK,
277
- position: 'fixed',
278
- left: animationElement.position.x,
279
- top: animationElement.position.y,
280
- transform: 'translate(-50%, -50%)'
281
- }}
282
- >
283
- {animationElement.content}
284
  </div>
285
  )}
286
  </div>
 
1
+ import { useState, useEffect } from 'react'
2
  import './App.css'
3
 
4
+ // Game question structure
5
+ interface ComparisonQuestion {
6
+ leftSide: string[];
7
+ rightSide: string[];
8
+ isEqual: boolean;
9
+ secondStepQuestion: 'more' | 'fewer';
 
 
10
  }
11
 
12
+ // Game states
13
+ type GameStep = 'step1' | 'step2' | 'correct-step1' | 'correct-step2';
 
 
 
 
 
 
 
 
14
 
15
  function App() {
16
+ const [currentQuestion, setCurrentQuestion] = useState<ComparisonQuestion | null>(null);
17
+ const [gameStep, setGameStep] = useState<GameStep>('step1');
18
+ const [feedback, setFeedback] = useState('');
19
+ const [showFeedback, setShowFeedback] = useState(false);
20
+
21
+ // Emoji pools for different categories
22
+ const emojiCategories = {
23
+ animals: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐸', '🐷'],
24
+ fruits: ['🍎', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🫐', '🍒', '🥝'],
25
+ objects: ['⚽', '🏀', '🎾', '🏈', '🎱', '🎪', '🎨', '🎭', '🎪', '🎯'],
26
+ hearts: ['💙', '💚', '💛', '💜', '🤍', '🖤', '🤎', '💗', '💖', '💕'],
27
+ stars: ['⭐', '🌟', '💫', '✨', '🌠', '⚡', '🔥', '❄️', '☀️', '🌙']
28
+ };
29
+
30
+ // Generate a random question
31
+ const generateQuestion = (): ComparisonQuestion => {
32
+ const categories = Object.keys(emojiCategories) as (keyof typeof emojiCategories)[];
33
+ const selectedCategory = categories[Math.floor(Math.random() * categories.length)];
34
+ const emojis = emojiCategories[selectedCategory];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ // Pick a random emoji for this question
37
+ const emoji = emojis[Math.floor(Math.random() * emojis.length)];
38
 
39
+ // Generate counts (1-10)
40
+ const leftCount = Math.floor(Math.random() * 10) + 1;
41
+ let rightCount = Math.floor(Math.random() * 10) + 1;
 
 
 
 
42
 
43
+ // 50% chance of making them equal
44
+ const shouldBeEqual = Math.random() < 0.5;
45
+ if (shouldBeEqual) {
46
+ rightCount = leftCount;
 
 
 
 
47
  }
 
 
 
 
 
48
 
49
+ // Create arrays with repeated emoji
50
+ const leftSide = Array(leftCount).fill(emoji);
51
+ const rightSide = Array(rightCount).fill(emoji);
 
 
 
 
 
 
52
 
53
+ // Determine second step question type
54
+ const secondStepQuestion: 'more' | 'fewer' = Math.random() < 0.5 ? 'more' : 'fewer';
55
+
56
+ return {
57
+ leftSide,
58
+ rightSide,
59
+ isEqual: leftCount === rightCount,
60
+ secondStepQuestion
61
+ };
62
+ };
63
+
64
+ // Initialize with first question
65
  useEffect(() => {
66
+ setCurrentQuestion(generateQuestion());
67
+ }, []);
68
+
69
+ // Safety effect: if we're in step 2 but sides are equal, generate new question
 
 
 
 
 
 
 
 
 
 
70
  useEffect(() => {
71
+ if (currentQuestion &&
72
+ (gameStep === 'step2' || gameStep === 'correct-step2') &&
73
+ currentQuestion.leftSide.length === currentQuestion.rightSide.length) {
74
+ // Generate new question and go back to step 1
75
+ setTimeout(() => {
76
+ setCurrentQuestion(generateQuestion());
77
+ setGameStep('step1');
78
+ }, 1000);
 
 
 
 
 
 
 
 
 
 
79
  }
80
+ }, [currentQuestion, gameStep]);
81
+
82
+ // Handle step 1 answer (equal/not equal)
83
+ const handleStep1Answer = (answer: 'equal' | 'not-equal') => {
84
+ if (!currentQuestion) return;
 
 
 
 
 
 
 
 
85
 
86
+ const isCorrect = (answer === 'equal') === currentQuestion.isEqual;
 
 
 
 
 
 
 
 
87
 
88
+ if (isCorrect) {
89
+ setFeedback('Correct! 🎉');
90
+ setShowFeedback(true);
91
+ setGameStep('correct-step1');
92
+
93
+ // If they correctly identified as "equal", generate new question
94
+ // If they correctly identified as "not equal", proceed to step 2
95
+ if (answer === 'equal') {
96
+ // Generate new question after delay
97
+ setTimeout(() => {
98
+ setCurrentQuestion(generateQuestion());
99
+ setGameStep('step1');
100
+ setShowFeedback(false);
101
+ setFeedback('');
102
+ }, 2000);
103
+ } else {
104
+ // Proceed to step 2 only if they are NOT equal
105
+ setTimeout(() => {
106
+ setGameStep('step2');
107
+ setShowFeedback(false);
108
+ setFeedback('');
109
+ }, 1500);
110
+ }
111
+ } else {
112
+ setFeedback('Try again! 🤔');
113
+ setShowFeedback(true);
114
+ setTimeout(() => {
115
+ setShowFeedback(false);
116
+ setFeedback('');
117
+ }, 1000);
118
+ }
119
  };
120
+
121
+ // Handle step 2 answer (more/fewer)
122
+ const handleStep2Answer = (answer: 'left' | 'right') => {
123
+ if (!currentQuestion) return;
124
 
125
+ const leftCount = currentQuestion.leftSide.length;
126
+ const rightCount = currentQuestion.rightSide.length;
 
 
 
127
 
128
+ // Safety check: if sides are equal, this shouldn't happen in step 2
129
+ if (leftCount === rightCount) {
130
+ // Generate new question and go back to step 1
131
+ setCurrentQuestion(generateQuestion());
132
+ setGameStep('step1');
133
+ return;
 
 
 
 
 
134
  }
 
 
 
 
 
135
 
136
+ let isCorrect = false;
 
137
 
138
+ if (currentQuestion.secondStepQuestion === 'more') {
139
+ if (leftCount > rightCount && answer === 'left') isCorrect = true;
140
+ if (rightCount > leftCount && answer === 'right') isCorrect = true;
141
+ } else { // fewer
142
+ if (leftCount < rightCount && answer === 'left') isCorrect = true;
143
+ if (rightCount < leftCount && answer === 'right') isCorrect = true;
144
+ }
145
+
146
+ if (isCorrect) {
147
+ setFeedback('Excellent! 🌟');
148
+ setShowFeedback(true);
149
+ setGameStep('correct-step2');
150
+
151
+ // Generate new question after delay
152
+ setTimeout(() => {
153
+ setCurrentQuestion(generateQuestion());
154
+ setGameStep('step1');
155
+ setShowFeedback(false);
156
+ setFeedback('');
157
+ }, 2000);
158
  } else {
159
+ setFeedback('Try again! 🤔');
160
+ setShowFeedback(true);
161
+ setTimeout(() => {
162
+ setShowFeedback(false);
163
+ setFeedback('');
164
+ }, 1000);
 
165
  }
166
  };
167
+
168
+ // Render emoji grid
169
+ const renderEmojiSide = (emojis: string[], side: 'left' | 'right') => {
170
+ const gridClass = `emoji-grid ${side}`;
171
+ return (
172
+ <div className={gridClass}>
173
+ {emojis.map((emoji, index) => (
174
+ <span key={index} className="emoji-item">
175
+ {emoji}
176
+ </span>
177
+ ))}
178
+ </div>
179
+ );
180
+ };
181
+
182
+ if (!currentQuestion) {
183
+ return <div className="loading">Loading...</div>;
184
  }
185
+
186
  return (
187
+ <div className="equal-game">
188
+ {gameStep === 'step1' || gameStep === 'correct-step1' ? (
189
+ // Step 1: Are they equal?
190
+ <>
191
+ <h1 className="question-title">Are they equal?</h1>
192
+
193
+ <div className="comparison-container">
194
+ <div className="side-container">
195
+ {renderEmojiSide(currentQuestion.leftSide, 'left')}
196
+ <div className="count-display">{currentQuestion.leftSide.length}</div>
197
+ </div>
198
+
199
+ <div className="vs-divider">VS</div>
200
+
201
+ <div className="side-container">
202
+ {renderEmojiSide(currentQuestion.rightSide, 'right')}
203
+ <div className="count-display">{currentQuestion.rightSide.length}</div>
204
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  </div>
206
+
207
+ {gameStep === 'step1' && (
208
+ <div className="answer-buttons">
209
+ <button
210
+ className="answer-btn equal-btn"
211
+ onClick={() => handleStep1Answer('equal')}
212
+ >
213
+ Equal
214
+ </button>
215
+ <button
216
+ className="answer-btn not-equal-btn"
217
+ onClick={() => handleStep1Answer('not-equal')}
218
+ >
219
+ Not Equal
220
+ </button>
221
+ </div>
222
+ )}
223
+ </>
224
+ ) : (
225
+ // Step 2: Which side has more/fewer? (Only if sides are NOT equal)
226
+ currentQuestion.leftSide.length !== currentQuestion.rightSide.length ? (
227
+ <>
228
+ <h1 className="question-title">
229
+ Which side has {currentQuestion.secondStepQuestion}?
230
+ </h1>
231
+
232
+ <div className="comparison-container">
233
+ <div className="side-container clickable" onClick={() => handleStep2Answer('left')}>
234
+ {renderEmojiSide(currentQuestion.leftSide, 'left')}
235
+ <div className="count-display">{currentQuestion.leftSide.length}</div>
236
+ </div>
237
+
238
+ <div className="vs-divider">VS</div>
239
+
240
+ <div className="side-container clickable" onClick={() => handleStep2Answer('right')}>
241
+ {renderEmojiSide(currentQuestion.rightSide, 'right')}
242
+ <div className="count-display">{currentQuestion.rightSide.length}</div>
243
+ </div>
244
+ </div>
245
+
246
+ <div className="step2-instruction">
247
+ Click on the side with {currentQuestion.secondStepQuestion} items
248
+ </div>
249
+ </>
250
+ ) : (
251
+ // If somehow we get to step 2 with equal sides, generate new question
252
+ <div className="loading">Generating new question...</div>
253
+ )
254
  )}
255
 
256
+ {showFeedback && (
257
+ <div className="feedback-message">
258
+ {feedback}
 
 
 
 
 
 
 
 
 
 
259
  </div>
260
  )}
261
  </div>