dailyGit commited on
Commit
85facc6
·
0 Parent(s):

dailygit: space watch redeploy

Browse files
Files changed (8) hide show
  1. .gitattributes +35 -0
  2. .gitignore +207 -0
  3. Dockerfile +5 -0
  4. LICENSE +21 -0
  5. README.md +104 -0
  6. game.js +1597 -0
  7. index.html +124 -0
  8. styles.css +1155 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
Dockerfile ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY . .
4
+ EXPOSE 7860
5
+ CMD ["python", "app.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AMA Akuoko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Reality Swipe
3
+ emoji: 🎮
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # 🎮 Reality Swipe
12
+
13
+ **Match gems, create cascades, and crush levels!** A delightful match-3 puzzle game built with pure JavaScript. No frameworks, no fuss—just pure gaming fun! 🎯
14
+
15
+ ## ✨ What's This?
16
+
17
+ Think Candy Crush, but **way cooler** because you built it! Drag and swap colorful gems to create matches of 3 or more. Watch the magic happen as pieces cascade down and new combos form automatically.
18
+
19
+ ## 🚀 Play Now!
20
+
21
+ Just open `index.html` in your browser and start swiping! No installation, no setup—it just works! 🎉
22
+
23
+ ## 🎯 How to Play
24
+
25
+ 1. **Drag or click** adjacent gems to swap them
26
+ 2. **Match 3+** gems in a row or column
27
+ 3. **Watch the cascade** as new pieces fall into place
28
+ 4. **Reach the target score** to level up!
29
+ 5. **Beat your best** and see how far you can go! 🏆
30
+
31
+ ## 🎨 Features
32
+
33
+ - 🎲 **8×8 game board** with 6 colorful gem types
34
+ - 💥 **Cascade system** - matches create more matches!
35
+ - 📊 **Score tracking** with level progression
36
+ - 🎯 **Move limits** for strategic gameplay
37
+ - 💡 **Hint system** when you're stuck
38
+ - ✨ **Smooth animations** and visual feedback
39
+ - 📱 **Responsive design** - play anywhere!
40
+
41
+ ## 🛠️ Built With
42
+
43
+ - **Pure JavaScript** - No dependencies, just vanilla JS
44
+ - **CSS Grid** - Beautiful, responsive layout
45
+ - **HTML5** - Modern web standards
46
+
47
+ ## 📁 Files
48
+
49
+ ```
50
+ crushgame/
51
+ ├── index.html # Game interface
52
+ ├── game.js # Game logic & mechanics
53
+ ├── styles.css # Styling & animations
54
+ └── README.md # You're reading it! 👋
55
+ ```
56
+
57
+ ## 🎓 For Developers
58
+
59
+ Want to customize? The code is clean, well-commented, and easy to modify:
60
+
61
+ - **Game logic**: Check out `Match3Game` class in `game.js`
62
+ - **Styling**: Tweak colors and animations in `styles.css`
63
+ - **Mechanics**: Adjust board size, gem types, or scoring in `game.js`
64
+
65
+ ## 🚀 What's Next?
66
+
67
+ This is a **prototype** ready to become something bigger! Potential next steps:
68
+
69
+ - 📱 Mobile app (Unity/React Native)
70
+ - 🎨 Custom graphics & sprites
71
+ - 🔊 Sound effects & music
72
+ - 💰 Monetization features
73
+ - 🌐 Multiplayer & leaderboards
74
+
75
+ ## 🤝 Contributing
76
+
77
+ **We'd love your help!** This project is open to contributions of all kinds:
78
+
79
+ - 🐛 **Bug fixes** - Found a bug? Report it or fix it!
80
+ - ✨ **New features** - Got an idea? Let's make it happen!
81
+ - 🎨 **Design improvements** - Better UI/UX? We're all ears!
82
+ - 📝 **Documentation** - Help make the code more understandable
83
+ - 🧪 **Testing** - Play the game and share feedback!
84
+
85
+ ### How to Contribute
86
+
87
+ 1. **Fork the repo** and clone it locally
88
+ 2. **Create a branch** for your changes (`git checkout -b feature/amazing-feature`)
89
+ 3. **Make your changes** and test them
90
+ 4. **Commit** with clear messages (`git commit -m 'Add amazing feature'`)
91
+ 5. **Push** to your fork (`git push origin feature/amazing-feature`)
92
+ 6. **Open a Pull Request** and describe your changes!
93
+
94
+ **No contribution is too small!** Even fixing typos or improving comments helps! 🎉
95
+
96
+ ## 📝 License
97
+
98
+ MIT License - Feel free to use, modify, and build upon this!
99
+
100
+ ---
101
+
102
+ **Made with ❤️ and lots of 🎮**
103
+
104
+ *Ready to level up? Start playing now!*
game.js ADDED
@@ -0,0 +1,1597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Match3Game {
2
+ constructor() {
3
+ this.boardRows = 7;
4
+ this.boardCols = 8;
5
+ this.numGemTypes = 6;
6
+ this.board = [];
7
+ this.selectedCell = null;
8
+ this.score = 0;
9
+ this.totalScore = 0;
10
+ this.moves = 30;
11
+ this.level = 1;
12
+ this.levelTargetScore = 1000;
13
+ this.isProcessing = false;
14
+ this.dragStartCell = null;
15
+ this.isDragging = false;
16
+ this.visualSwapActive = false;
17
+ this.visualSwapTarget = null;
18
+ this.mouseX = 0;
19
+ this.mouseY = 0;
20
+ this.dragOffsetX = 0;
21
+ this.dragOffsetY = 0;
22
+ this.targetOffsetX = 0;
23
+ this.targetOffsetY = 0;
24
+ this.currentOffsetX = 0;
25
+ this.currentOffsetY = 0;
26
+ this.animationFrameId = null;
27
+ this.buttonClickCooldown = false;
28
+ this.renderPending = false;
29
+ this.lastBoardState = null;
30
+
31
+ // Load best stats from localStorage
32
+ this.bestLevel = parseInt(localStorage.getItem('bestLevel') || '1');
33
+ this.bestScore = parseInt(localStorage.getItem('bestScore') || '0');
34
+
35
+ // Initialize sound system
36
+ this.soundManager = new SoundManager();
37
+
38
+ this.init();
39
+ }
40
+
41
+ init() {
42
+ this.setupEventListeners();
43
+ this.setupTheme();
44
+ this.showHome();
45
+ }
46
+
47
+ setupTheme() {
48
+ // Load saved theme or default to light
49
+ const savedTheme = localStorage.getItem('gameTheme') || 'light';
50
+ this.applyTheme(savedTheme);
51
+
52
+ // Setup theme selector
53
+ const themeSelect = document.getElementById('theme-select');
54
+ if (themeSelect) {
55
+ themeSelect.value = savedTheme;
56
+ themeSelect.addEventListener('change', (e) => {
57
+ this.applyTheme(e.target.value);
58
+ });
59
+ }
60
+ }
61
+
62
+ applyTheme(theme) {
63
+ const body = document.body;
64
+ if (theme === 'dark') {
65
+ body.classList.add('dark-theme');
66
+ body.classList.remove('light-theme');
67
+ } else {
68
+ body.classList.add('light-theme');
69
+ body.classList.remove('dark-theme');
70
+ }
71
+ localStorage.setItem('gameTheme', theme);
72
+ }
73
+
74
+ showHome() {
75
+ document.getElementById('home-page').classList.remove('hidden');
76
+ const gameHeader = document.querySelector('.game-header');
77
+ const gameBoard = document.getElementById('game-board');
78
+ const gameControls = document.querySelector('.game-controls');
79
+
80
+ if (gameHeader) gameHeader.classList.add('hidden');
81
+ if (gameBoard) gameBoard.classList.add('hidden');
82
+ if (gameControls) {
83
+ gameControls.classList.add('hidden');
84
+ gameControls.style.visibility = 'hidden';
85
+ }
86
+
87
+ // Stop background music when going home
88
+ if (this.soundManager) {
89
+ this.soundManager.stopBackgroundMusic();
90
+ }
91
+
92
+ this.updateHomeStats();
93
+ }
94
+
95
+ showGame() {
96
+ document.getElementById('home-page').classList.add('hidden');
97
+ const gameHeader = document.querySelector('.game-header');
98
+ const gameBoard = document.getElementById('game-board');
99
+ const gameControls = document.querySelector('.game-controls');
100
+
101
+ if (gameHeader) gameHeader.classList.remove('hidden');
102
+ if (gameBoard) gameBoard.classList.remove('hidden');
103
+ if (gameControls) {
104
+ gameControls.classList.remove('hidden');
105
+ gameControls.style.visibility = 'visible';
106
+ gameControls.style.display = 'flex';
107
+ gameControls.style.opacity = '1';
108
+ }
109
+ this.createBoard();
110
+ this.renderBoard();
111
+ this.updateUI();
112
+
113
+ // Start background music
114
+ if (this.soundManager) {
115
+ this.soundManager.startBackgroundMusic();
116
+ this.soundManager.updateMusicButton();
117
+ }
118
+ }
119
+
120
+ updateHomeStats() {
121
+ document.getElementById('best-level').textContent = this.bestLevel;
122
+ document.getElementById('best-score').textContent = this.bestScore.toLocaleString();
123
+ }
124
+
125
+ saveBestStats() {
126
+ let updated = false;
127
+ if (this.level > this.bestLevel) {
128
+ this.bestLevel = this.level;
129
+ localStorage.setItem('bestLevel', this.bestLevel.toString());
130
+ updated = true;
131
+ }
132
+ if (this.totalScore > this.bestScore) {
133
+ this.bestScore = this.totalScore;
134
+ localStorage.setItem('bestScore', this.bestScore.toString());
135
+ updated = true;
136
+ }
137
+ // Update home stats display if we're on home page
138
+ if (updated && !document.querySelector('.game-header').classList.contains('hidden')) {
139
+ // Will update when going to home
140
+ }
141
+ }
142
+
143
+ createBoard() {
144
+ // Initialize empty board
145
+ this.board = [];
146
+ for (let row = 0; row < this.boardRows; row++) {
147
+ this.board[row] = [];
148
+ for (let col = 0; col < this.boardCols; col++) {
149
+ this.board[row][col] = this.getRandomGem();
150
+ }
151
+ }
152
+
153
+ // Remove initial matches
154
+ while (this.findMatches().length > 0) {
155
+ for (let row = 0; row < this.boardRows; row++) {
156
+ for (let col = 0; col < this.boardCols; col++) {
157
+ if (this.isPartOfMatch(row, col)) {
158
+ this.board[row][col] = this.getRandomGem();
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ getRandomGem() {
166
+ return Math.floor(Math.random() * this.numGemTypes);
167
+ }
168
+
169
+ isPartOfMatch(row, col) {
170
+ const gem = this.board[row][col];
171
+
172
+ // Check horizontal match
173
+ let horizontalCount = 1;
174
+ for (let c = col - 1; c >= 0 && this.board[row][c] === gem; c--) horizontalCount++;
175
+ for (let c = col + 1; c < this.boardCols && this.board[row][c] === gem; c++) horizontalCount++;
176
+ if (horizontalCount >= 3) return true;
177
+
178
+ // Check vertical match
179
+ let verticalCount = 1;
180
+ for (let r = row - 1; r >= 0 && this.board[r][col] === gem; r--) verticalCount++;
181
+ for (let r = row + 1; r < this.boardRows && this.board[r][col] === gem; r++) verticalCount++;
182
+ if (verticalCount >= 3) return true;
183
+
184
+ return false;
185
+ }
186
+
187
+ renderBoard(forceUpdate = false) {
188
+ // Allow rendering during processing only if forceUpdate is true (for final render after matches)
189
+ if (this.isProcessing && !forceUpdate) {
190
+ this.renderPending = true;
191
+ return;
192
+ }
193
+
194
+ // Don't render during active drag unless forced
195
+ if (this.isDragging && !forceUpdate) {
196
+ this.renderPending = true;
197
+ return;
198
+ }
199
+
200
+ const boardElement = document.getElementById('game-board');
201
+ if (!boardElement) return;
202
+
203
+ // Check if board already exists - update instead of recreate
204
+ const existingCells = boardElement.querySelectorAll('.cell');
205
+ const needsFullRender = existingCells.length !== this.boardRows * this.boardCols;
206
+
207
+ if (needsFullRender) {
208
+ boardElement.innerHTML = '';
209
+ }
210
+
211
+ // Create board state string for comparison
212
+ const currentState = JSON.stringify(this.board);
213
+ const stateChanged = currentState !== this.lastBoardState;
214
+
215
+ // If state hasn't changed and we don't need full render, skip update completely
216
+ if (!needsFullRender && !stateChanged && !forceUpdate) {
217
+ return;
218
+ }
219
+
220
+ this.lastBoardState = currentState;
221
+
222
+ // Update cells directly without requestAnimationFrame to prevent flickering
223
+ for (let row = 0; row < this.boardRows; row++) {
224
+ for (let col = 0; col < this.boardCols; col++) {
225
+ let cell = existingCells[row * this.boardCols + col];
226
+
227
+ if (!cell || needsFullRender) {
228
+ cell = document.createElement('div');
229
+ cell.dataset.row = row;
230
+ cell.dataset.col = col;
231
+
232
+ // Click handler (fallback)
233
+ cell.addEventListener('click', () => this.handleCellClick(row, col));
234
+
235
+ // Drag handlers
236
+ cell.addEventListener('mousedown', (e) => this.handleDragStart(e, row, col));
237
+ cell.addEventListener('mouseenter', (e) => this.handleDragOver(e, row, col));
238
+
239
+ // Prevent text selection during drag
240
+ cell.addEventListener('selectstart', (e) => e.preventDefault());
241
+ cell.addEventListener('dragstart', (e) => e.preventDefault());
242
+
243
+ boardElement.appendChild(cell);
244
+ }
245
+
246
+ // Only update if gem type changed
247
+ const gemType = this.board[row][col];
248
+ const expectedClass = `cell gem-${gemType}`;
249
+ const expectedEmoji = this.getGemEmoji(gemType);
250
+
251
+ // Handle empty cells first
252
+ if (gemType === -1) {
253
+ // Hide empty cells
254
+ if (!cell.classList.contains('empty-cell')) {
255
+ cell.classList.add('empty-cell');
256
+ cell.style.display = 'none';
257
+ }
258
+ continue;
259
+ }
260
+
261
+ // Remove empty-cell class and ensure visibility for non-empty cells
262
+ if (cell.classList.contains('empty-cell')) {
263
+ cell.classList.remove('empty-cell');
264
+ cell.style.display = '';
265
+ cell.style.opacity = '';
266
+ cell.style.visibility = '';
267
+ }
268
+
269
+ // Check if cell needs update
270
+ const currentGemClass = Array.from(cell.classList).find(c => c.startsWith('gem-'));
271
+ const needsUpdate = needsFullRender ||
272
+ currentGemClass !== `gem-${gemType}` ||
273
+ cell.textContent !== expectedEmoji;
274
+
275
+ if (needsUpdate) {
276
+ // NEVER update cells that are in animation states
277
+ const isMatched = cell.classList.contains('matched');
278
+ const isMatchEnlarge = cell.classList.contains('match-enlarge');
279
+ const hasActiveAnimation = isMatched || isMatchEnlarge;
280
+
281
+ // Only update if not animating
282
+ if (!hasActiveAnimation) {
283
+ // Always ensure cell is visible for non-empty cells
284
+ cell.style.display = '';
285
+ cell.style.opacity = '';
286
+ cell.style.visibility = '';
287
+
288
+ cell.className = expectedClass;
289
+ cell.textContent = expectedEmoji;
290
+
291
+ // Only reset transform if cell is idle
292
+ if (!cell.style.transform || cell.style.transform === '') {
293
+ cell.style.transform = '';
294
+ cell.style.transition = '';
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ // Add board-level mousemove for better drag tracking
302
+ boardElement.addEventListener('mousemove', (e) => {
303
+ if (this.isDragging && this.dragStartCell) {
304
+ // Update mouse position for smooth tracking
305
+ this.mouseX = e.clientX;
306
+ this.mouseY = e.clientY;
307
+
308
+ const startCell = document.querySelector(
309
+ `[data-row="${this.dragStartCell.row}"][data-col="${this.dragStartCell.col}"]`
310
+ );
311
+ if (startCell) {
312
+ const rect = startCell.getBoundingClientRect();
313
+ // Calculate target offset (where we want to go)
314
+ this.targetOffsetX = (e.clientX - (rect.left + rect.width / 2)) * 0.6;
315
+ this.targetOffsetY = (e.clientY - (rect.top + rect.height / 2)) * 0.6;
316
+ this.dragOffsetX = this.targetOffsetX;
317
+ this.dragOffsetY = this.targetOffsetY;
318
+ }
319
+
320
+ const cell = e.target.closest('.cell');
321
+ if (cell && cell.dataset.row && cell.dataset.col) {
322
+ const row = parseInt(cell.dataset.row);
323
+ const col = parseInt(cell.dataset.col);
324
+ this.handleDragOver(e, row, col);
325
+ }
326
+ }
327
+ });
328
+
329
+ // Add global mouse up handler to handle drag end
330
+ document.addEventListener('mouseup', () => this.handleDragEnd());
331
+ }
332
+
333
+ getGemEmoji(gemType) {
334
+ if (gemType === -1) return ''; // Empty cell
335
+ const emojis = ['🔴', '🔵', '🟡', '🟢', '🟠', '🟣'];
336
+ return emojis[gemType] || '⚪';
337
+ }
338
+
339
+ handleDragStart(e, row, col) {
340
+ if (this.isProcessing) return;
341
+ if (this.moves <= 0) return; // No moves left
342
+
343
+ e.preventDefault();
344
+ this.dragStartCell = { row, col };
345
+ this.isDragging = true;
346
+
347
+ // Play swipe sound
348
+ if (this.soundManager) {
349
+ this.soundManager.playSwipeSound();
350
+ }
351
+
352
+ const cell = document.querySelector(`[data-row="${row}"][data-col="${col}"]`);
353
+ if (cell) {
354
+ const rect = cell.getBoundingClientRect();
355
+ const boardRect = document.getElementById('game-board').getBoundingClientRect();
356
+
357
+ // Store initial mouse position relative to cell center
358
+ this.mouseX = e.clientX;
359
+ this.mouseY = e.clientY;
360
+ this.dragOffsetX = 0;
361
+ this.dragOffsetY = 0;
362
+ this.targetOffsetX = 0;
363
+ this.targetOffsetY = 0;
364
+ this.currentOffsetX = 0;
365
+ this.currentOffsetY = 0;
366
+
367
+ cell.classList.add('selected', 'dragging');
368
+ cell.style.cursor = 'grabbing';
369
+ cell.style.transition = 'none'; // Disable transition during drag for smooth following
370
+ cell.style.willChange = 'transform';
371
+
372
+ // Start smooth position tracking
373
+ this.startDragTracking();
374
+ }
375
+
376
+ // Add dragging class to board for visual feedback
377
+ document.getElementById('game-board').classList.add('is-dragging');
378
+ }
379
+
380
+ startDragTracking() {
381
+ if (this.animationFrameId) {
382
+ cancelAnimationFrame(this.animationFrameId);
383
+ }
384
+
385
+ const updateDragPosition = () => {
386
+ if (!this.isDragging || !this.dragStartCell) return;
387
+
388
+ const cell = document.querySelector(
389
+ `[data-row="${this.dragStartCell.row}"][data-col="${this.dragStartCell.col}"]`
390
+ );
391
+
392
+ if (cell) {
393
+ // Smooth interpolation using easing (ease-out cubic for fluid motion)
394
+ const smoothingFactor = 0.15; // Lower = smoother but slower response
395
+ this.currentOffsetX += (this.targetOffsetX - this.currentOffsetX) * smoothingFactor;
396
+ this.currentOffsetY += (this.targetOffsetY - this.currentOffsetY) * smoothingFactor;
397
+
398
+ // Apply smooth transform with fluid motion
399
+ const scale = 1.12 + Math.min(Math.abs(this.currentOffsetX) + Math.abs(this.currentOffsetY), 30) / 200;
400
+ const rotationX = this.currentOffsetY * 0.1;
401
+ const rotationY = this.currentOffsetX * 0.1;
402
+
403
+ cell.style.transform = `translate(${this.currentOffsetX}px, ${this.currentOffsetY}px) scale(${scale}) translateZ(20px) rotateX(${rotationX}deg) rotateY(${rotationY}deg)`;
404
+ }
405
+
406
+ this.animationFrameId = requestAnimationFrame(updateDragPosition);
407
+ };
408
+
409
+ this.animationFrameId = requestAnimationFrame(updateDragPosition);
410
+ }
411
+
412
+ handleDragOver(e, row, col) {
413
+ if (!this.isDragging || !this.dragStartCell) return;
414
+ if (this.isProcessing) return;
415
+
416
+ e.preventDefault();
417
+
418
+ // Don't update if hovering over the same cell
419
+ if (row === this.dragStartCell.row && col === this.dragStartCell.col) return;
420
+
421
+ // Check if target is adjacent
422
+ const isAdjacent =
423
+ (Math.abs(this.dragStartCell.row - row) === 1 && this.dragStartCell.col === col) ||
424
+ (Math.abs(this.dragStartCell.col - col) === 1 && this.dragStartCell.row === row);
425
+
426
+ const startCell = document.querySelector(
427
+ `[data-row="${this.dragStartCell.row}"][data-col="${this.dragStartCell.col}"]`
428
+ );
429
+
430
+ // If we have a visual swap active but target changed, revert it
431
+ if (this.visualSwapActive && this.visualSwapTarget) {
432
+ if (this.visualSwapTarget.row !== row || this.visualSwapTarget.col !== col) {
433
+ this.revertVisualSwap();
434
+ }
435
+ }
436
+
437
+ if (isAdjacent) {
438
+ const targetCell = document.querySelector(`[data-row="${row}"][data-col="${col}"]`);
439
+
440
+ // If this is a new target or we don't have a visual swap, create one
441
+ if (!this.visualSwapActive ||
442
+ !this.visualSwapTarget ||
443
+ this.visualSwapTarget.row !== row ||
444
+ this.visualSwapTarget.col !== col) {
445
+
446
+ // Revert any existing swap first
447
+ if (this.visualSwapActive) {
448
+ this.revertVisualSwap();
449
+ }
450
+
451
+ // Perform visual swap
452
+ this.performVisualSwap(this.dragStartCell.row, this.dragStartCell.col, row, col);
453
+ this.visualSwapTarget = { row, col };
454
+ }
455
+
456
+ if (targetCell) {
457
+ targetCell.classList.add('drag-target');
458
+ if (startCell) {
459
+ this.drawConnectionLine(startCell, targetCell);
460
+ }
461
+ }
462
+ } else {
463
+ // Not adjacent - revert visual swap if active
464
+ if (this.visualSwapActive) {
465
+ this.revertVisualSwap();
466
+ }
467
+
468
+ // Remove target from all cells
469
+ document.querySelectorAll('.cell').forEach(c => {
470
+ c.classList.remove('drag-target');
471
+ });
472
+
473
+ // Remove connection line if not adjacent
474
+ this.removeConnectionLine();
475
+ }
476
+ }
477
+
478
+ performVisualSwap(row1, col1, row2, col2) {
479
+ const cell1 = document.querySelector(`[data-row="${row1}"][data-col="${col1}"]`);
480
+ const cell2 = document.querySelector(`[data-row="${row2}"][data-col="${col2}"]`);
481
+
482
+ if (!cell1 || !cell2) return;
483
+
484
+ // Get cell positions for smooth swap animation
485
+ const rect1 = cell1.getBoundingClientRect();
486
+ const rect2 = cell2.getBoundingClientRect();
487
+
488
+ // Calculate relative positions in pixels for smoother animation
489
+ const deltaX = rect2.left - rect1.left;
490
+ const deltaY = rect2.top - rect1.top;
491
+
492
+ // Actually swap in the board array so match checking works correctly
493
+ [this.board[row1][col1], this.board[row2][col2]] =
494
+ [this.board[row2][col2], this.board[row1][col1]];
495
+
496
+ // Get the gem types (now swapped)
497
+ const gem1 = this.board[row1][col1];
498
+ const gem2 = this.board[row2][col2];
499
+
500
+ // Enable ultra-smooth transitions for swap animation
501
+ cell1.style.transition = 'transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
502
+ cell2.style.transition = 'transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
503
+
504
+ // Animate swap with fluid position interpolation
505
+ cell1.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(1.08) translateZ(15px)`;
506
+ cell2.style.transform = `translate(${-deltaX}px, ${-deltaY}px) scale(1.08) translateZ(15px)`;
507
+
508
+ // Update the visual appearance mid-animation for seamless transition
509
+ setTimeout(() => {
510
+ cell1.className = `cell gem-${gem1}`;
511
+ cell1.textContent = this.getGemEmoji(gem1);
512
+ cell1.classList.add('selected', 'dragging');
513
+
514
+ cell2.className = `cell gem-${gem2}`;
515
+ cell2.textContent = this.getGemEmoji(gem2);
516
+ cell2.classList.add('drag-target');
517
+ }, 175); // Halfway through animation for smooth visual swap
518
+
519
+ // Reset transforms after animation completes
520
+ setTimeout(() => {
521
+ cell1.style.transform = '';
522
+ cell2.style.transform = '';
523
+ }, 350);
524
+
525
+ this.visualSwapActive = true;
526
+ }
527
+
528
+ revertVisualSwap() {
529
+ if (!this.visualSwapActive || !this.dragStartCell || !this.visualSwapTarget) return;
530
+
531
+ const cell1 = document.querySelector(
532
+ `[data-row="${this.dragStartCell.row}"][data-col="${this.dragStartCell.col}"]`
533
+ );
534
+ const cell2 = document.querySelector(
535
+ `[data-row="${this.visualSwapTarget.row}"][data-col="${this.visualSwapTarget.col}"]`
536
+ );
537
+
538
+ if (cell1 && cell2) {
539
+ // Smooth revert animation
540
+ cell1.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)';
541
+ cell2.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)';
542
+
543
+ // Animate back to original positions
544
+ cell1.style.transform = 'translate(0, 0) scale(1.1) translateZ(15px)';
545
+ cell2.style.transform = 'translate(0, 0) scale(1.1) translateZ(15px)';
546
+
547
+ // Revert the board array swap after animation
548
+ setTimeout(() => {
549
+ [this.board[this.dragStartCell.row][this.dragStartCell.col],
550
+ this.board[this.visualSwapTarget.row][this.visualSwapTarget.col]] =
551
+ [this.board[this.visualSwapTarget.row][this.visualSwapTarget.col],
552
+ this.board[this.dragStartCell.row][this.dragStartCell.col]];
553
+
554
+ // Restore original appearance
555
+ const gem1 = this.board[this.dragStartCell.row][this.dragStartCell.col];
556
+ const gem2 = this.board[this.visualSwapTarget.row][this.visualSwapTarget.col];
557
+
558
+ cell1.className = `cell gem-${gem1}`;
559
+ cell1.textContent = this.getGemEmoji(gem1);
560
+ cell1.classList.add('selected', 'dragging');
561
+ cell1.style.transform = '';
562
+
563
+ cell2.className = `cell gem-${gem2}`;
564
+ cell2.textContent = this.getGemEmoji(gem2);
565
+ cell2.style.transform = '';
566
+
567
+ // Render board after revert animation completes
568
+ requestAnimationFrame(() => {
569
+ this.renderBoard();
570
+ });
571
+ }, 300);
572
+ }
573
+
574
+ this.visualSwapActive = false;
575
+ this.visualSwapTarget = null;
576
+ }
577
+
578
+ drawConnectionLine(startCell, targetCell) {
579
+ // Remove existing line
580
+ this.removeConnectionLine();
581
+
582
+ const board = document.getElementById('game-board');
583
+ const startRect = startCell.getBoundingClientRect();
584
+ const targetRect = targetCell.getBoundingClientRect();
585
+ const boardRect = board.getBoundingClientRect();
586
+
587
+ // Calculate positions relative to board
588
+ const startX = startRect.left + startRect.width / 2 - boardRect.left;
589
+ const startY = startRect.top + startRect.height / 2 - boardRect.top;
590
+ const targetX = targetRect.left + targetRect.width / 2 - boardRect.left;
591
+ const targetY = targetRect.top + targetRect.height / 2 - boardRect.top;
592
+
593
+ // Create SVG line
594
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
595
+ svg.setAttribute('class', 'drag-connection');
596
+ svg.style.position = 'absolute';
597
+ svg.style.left = '0';
598
+ svg.style.top = '0';
599
+ svg.style.width = '100%';
600
+ svg.style.height = '100%';
601
+ svg.style.pointerEvents = 'none';
602
+
603
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
604
+ line.setAttribute('x1', startX);
605
+ line.setAttribute('y1', startY);
606
+ line.setAttribute('x2', targetX);
607
+ line.setAttribute('y2', targetY);
608
+ line.setAttribute('stroke', 'rgba(100, 150, 255, 0.9)');
609
+ line.setAttribute('stroke-width', '5');
610
+ line.setAttribute('stroke-dasharray', '8, 4');
611
+ line.setAttribute('marker-end', 'url(#arrowhead)');
612
+
613
+ // Add arrow marker
614
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
615
+ const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
616
+ marker.setAttribute('id', 'arrowhead');
617
+ marker.setAttribute('markerWidth', '10');
618
+ marker.setAttribute('markerHeight', '10');
619
+ marker.setAttribute('refX', '9');
620
+ marker.setAttribute('refY', '3');
621
+ marker.setAttribute('orient', 'auto');
622
+
623
+ const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
624
+ polygon.setAttribute('points', '0 0, 10 3, 0 6');
625
+ polygon.setAttribute('fill', 'rgba(100, 150, 255, 0.9)');
626
+
627
+ marker.appendChild(polygon);
628
+ defs.appendChild(marker);
629
+ svg.appendChild(defs);
630
+ svg.appendChild(line);
631
+
632
+ board.style.position = 'relative';
633
+ board.appendChild(svg);
634
+ }
635
+
636
+ removeConnectionLine() {
637
+ const existingLine = document.querySelector('.drag-connection');
638
+ if (existingLine) {
639
+ existingLine.remove();
640
+ }
641
+ }
642
+
643
+ handleDragEnd() {
644
+ if (!this.isDragging || !this.dragStartCell) return;
645
+
646
+ // If we have a visual swap active, commit it to the game logic
647
+ if (this.visualSwapActive && this.visualSwapTarget) {
648
+ // The board array is already swapped from performVisualSwap
649
+ // Just check for matches and process
650
+ const matches = this.findMatches();
651
+
652
+ if (matches.length > 0) {
653
+ // Valid swap
654
+ this.selectedCell = null;
655
+ this.updateCellSelection();
656
+ if (this.moves > 0) this.moves--;
657
+
658
+ // Animate matched cells by enlarging them
659
+ this.animateMatchedCells(matches).then(() => {
660
+ this.processMatches();
661
+ });
662
+ } else {
663
+ // Invalid swap - revert
664
+ this.revertVisualSwap();
665
+ // Don't call renderBoard here - revertVisualSwap handles it
666
+ }
667
+
668
+ // Clear visual swap state
669
+ this.visualSwapActive = false;
670
+ this.visualSwapTarget = null;
671
+ } else {
672
+ // No visual swap was active, check if there's a target cell
673
+ const targetCell = document.querySelector('.drag-target');
674
+ if (targetCell) {
675
+ const targetRow = parseInt(targetCell.dataset.row);
676
+ const targetCol = parseInt(targetCell.dataset.col);
677
+
678
+ this.swapCells(
679
+ this.dragStartCell.row,
680
+ this.dragStartCell.col,
681
+ targetRow,
682
+ targetCol
683
+ );
684
+ } else {
685
+ // No valid target - just clean up
686
+ if (this.visualSwapActive) {
687
+ this.revertVisualSwap();
688
+ }
689
+ }
690
+ }
691
+
692
+ // Remove connection line
693
+ this.removeConnectionLine();
694
+
695
+ // Reset drag state first
696
+ this.isDragging = false;
697
+ const dragStart = this.dragStartCell;
698
+ this.dragStartCell = null;
699
+
700
+ // Cancel animation frame
701
+ if (this.animationFrameId) {
702
+ cancelAnimationFrame(this.animationFrameId);
703
+ this.animationFrameId = null;
704
+ }
705
+
706
+ // Smoothly reset all drag-related styles using requestAnimationFrame for smoothness
707
+ requestAnimationFrame(() => {
708
+ document.querySelectorAll('.cell').forEach(c => {
709
+ // Only reset if not in an animation state
710
+ if (!c.classList.contains('matched') && !c.classList.contains('match-enlarge')) {
711
+ c.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.2s ease';
712
+ c.style.transform = '';
713
+ c.style.willChange = '';
714
+ c.classList.remove('selected', 'drag-target', 'dragging');
715
+ c.style.cursor = '';
716
+
717
+ // Remove inline styles after transition
718
+ setTimeout(() => {
719
+ if (!c.classList.contains('matched')) {
720
+ c.style.transition = '';
721
+ }
722
+ }, 300);
723
+ }
724
+ });
725
+ });
726
+
727
+ // Reset offsets
728
+ this.currentOffsetX = 0;
729
+ this.currentOffsetY = 0;
730
+ this.targetOffsetX = 0;
731
+ this.targetOffsetY = 0;
732
+
733
+ // Remove board dragging class
734
+ document.getElementById('game-board').classList.remove('is-dragging');
735
+ }
736
+
737
+ handleCellClick(row, col) {
738
+ // Clicking only allows visual selection, no matching allowed
739
+ // Matching is only allowed through drag and drop
740
+ if (this.isProcessing) return;
741
+ if (this.isDragging) return; // Ignore clicks during drag
742
+
743
+ // Just allow visual selection for feedback, but don't perform swaps
744
+ this.selectedCell = { row, col };
745
+ this.updateCellSelection();
746
+ }
747
+
748
+ updateCellSelection() {
749
+ const cells = document.querySelectorAll('.cell');
750
+ cells.forEach(cell => cell.classList.remove('selected'));
751
+
752
+ if (this.selectedCell) {
753
+ const cell = document.querySelector(
754
+ `[data-row="${this.selectedCell.row}"][data-col="${this.selectedCell.col}"]`
755
+ );
756
+ if (cell) cell.classList.add('selected');
757
+ }
758
+ }
759
+
760
+ swapCells(row1, col1, row2, col2) {
761
+ // If visual swap is already active, the board array is already swapped
762
+ // Otherwise, swap it now
763
+ if (!this.visualSwapActive) {
764
+ // Swap in board
765
+ [this.board[row1][col1], this.board[row2][col2]] =
766
+ [this.board[row2][col2], this.board[row1][col1]];
767
+ }
768
+
769
+ // Check if swap creates a match
770
+ const matches = this.findMatches();
771
+
772
+ if (matches.length > 0) {
773
+ // Valid swap - visual swap already shows the correct state (or we just swapped)
774
+ this.selectedCell = null;
775
+ this.updateCellSelection();
776
+ if (this.moves > 0) this.moves--;
777
+
778
+ // Play match sound
779
+ if (this.soundManager) {
780
+ this.soundManager.playMatchSound(matches.length);
781
+ }
782
+
783
+ // Animate matched cells by enlarging them
784
+ this.animateMatchedCells(matches).then(() => {
785
+ this.processMatches();
786
+ });
787
+ } else {
788
+ // Invalid swap - revert both visual and logical
789
+ if (this.visualSwapActive) {
790
+ // Visual swap will be reverted by revertVisualSwap, but we need to revert the array
791
+ [this.board[row1][col1], this.board[row2][col2]] =
792
+ [this.board[row2][col2], this.board[row1][col1]];
793
+ } else {
794
+ // Revert the swap we just made
795
+ [this.board[row1][col1], this.board[row2][col2]] =
796
+ [this.board[row2][col2], this.board[row1][col1]];
797
+ }
798
+ // Don't call renderBoard here - revertVisualSwap handles visual update
799
+ // renderBoard will be called after revert completes
800
+ }
801
+ }
802
+
803
+ async animateMatchedCells(matches) {
804
+ // No animation - skip immediately
805
+ // This function is kept for compatibility but does nothing now
806
+ await this.sleep(0);
807
+ }
808
+
809
+ updateCellsAfterMatch() {
810
+ // This function is no longer needed - we use renderBoard(true) instead
811
+ // Keeping it for backwards compatibility but it's essentially a no-op now
812
+ }
813
+
814
+ findMatches() {
815
+ const matches = [];
816
+ const visited = new Set();
817
+
818
+ // Check horizontal matches
819
+ for (let row = 0; row < this.boardRows; row++) {
820
+ let count = 1;
821
+ let currentGem = this.board[row][0];
822
+
823
+ for (let col = 1; col < this.boardCols; col++) {
824
+ if (this.board[row][col] === currentGem) {
825
+ count++;
826
+ } else {
827
+ if (count >= 3) {
828
+ for (let c = col - count; c < col; c++) {
829
+ const key = `${row},${c}`;
830
+ if (!visited.has(key)) {
831
+ matches.push({ row, col: c });
832
+ visited.add(key);
833
+ }
834
+ }
835
+ }
836
+ count = 1;
837
+ currentGem = this.board[row][col];
838
+ }
839
+ }
840
+
841
+ // Check end of row
842
+ if (count >= 3) {
843
+ for (let c = this.boardCols - count; c < this.boardCols; c++) {
844
+ const key = `${row},${c}`;
845
+ if (!visited.has(key)) {
846
+ matches.push({ row, col: c });
847
+ visited.add(key);
848
+ }
849
+ }
850
+ }
851
+ }
852
+
853
+ // Check vertical matches
854
+ for (let col = 0; col < this.boardCols; col++) {
855
+ let count = 1;
856
+ let currentGem = this.board[0][col];
857
+
858
+ for (let row = 1; row < this.boardRows; row++) {
859
+ if (this.board[row][col] === currentGem) {
860
+ count++;
861
+ } else {
862
+ if (count >= 3) {
863
+ for (let r = row - count; r < row; r++) {
864
+ const key = `${r},${col}`;
865
+ if (!visited.has(key)) {
866
+ matches.push({ row: r, col });
867
+ visited.add(key);
868
+ }
869
+ }
870
+ }
871
+ count = 1;
872
+ currentGem = this.board[row][col];
873
+ }
874
+ }
875
+
876
+ // Check end of column
877
+ if (count >= 3) {
878
+ for (let r = this.boardRows - count; r < this.boardRows; r++) {
879
+ const key = `${r},${col}`;
880
+ if (!visited.has(key)) {
881
+ matches.push({ row: r, col });
882
+ visited.add(key);
883
+ }
884
+ }
885
+ }
886
+ }
887
+
888
+ return matches;
889
+ }
890
+
891
+ async processMatches() {
892
+ this.isProcessing = true;
893
+
894
+ while (true) {
895
+ const matches = this.findMatches();
896
+
897
+ if (matches.length === 0) {
898
+ break;
899
+ }
900
+
901
+ // Update score
902
+ this.score += matches.length * 10;
903
+ this.updateUI();
904
+
905
+ // Animate matched cells - add matched class to trigger fade-out
906
+ matches.forEach(match => {
907
+ const cell = document.querySelector(
908
+ `[data-row="${match.row}"][data-col="${match.col}"]`
909
+ );
910
+ if (cell) {
911
+ cell.classList.add('matched');
912
+ // Create random particles for each matched cell
913
+ this.createExplosionParticles(cell);
914
+ }
915
+ });
916
+
917
+ // No animation wait - tiles disappear immediately
918
+ await this.sleep(50);
919
+
920
+ // Clean up particles
921
+ document.querySelectorAll('.explosion-particle').forEach(particle => {
922
+ particle.remove();
923
+ });
924
+
925
+ // Remove matched cells from board and DOM
926
+ matches.forEach(match => {
927
+ this.board[match.row][match.col] = -1; // Mark as empty
928
+
929
+ // Mark cell as empty - renderBoard will handle hiding it
930
+ // Don't manipulate DOM directly here to prevent flickering
931
+ });
932
+
933
+ // Make pieces fall
934
+ await this.applyGravity();
935
+
936
+ // Fill empty spaces with new random gems
937
+ this.fillEmptySpaces();
938
+
939
+ // Re-render board to show new tiles - force update even during processing
940
+ this.renderBoard(true);
941
+
942
+ // Small delay to let the new tiles appear before checking for new matches
943
+ await this.sleep(100);
944
+ }
945
+
946
+ this.isProcessing = false;
947
+
948
+ // Render any pending updates after processing completes - do it synchronously
949
+ if (this.renderPending) {
950
+ this.renderPending = false;
951
+ this.renderBoard(true);
952
+ }
953
+
954
+ this.checkGameOver();
955
+ }
956
+
957
+ async applyGravity() {
958
+ for (let col = 0; col < this.boardCols; col++) {
959
+ let writeIndex = this.boardRows - 1;
960
+
961
+ for (let row = this.boardRows - 1; row >= 0; row--) {
962
+ if (this.board[row][col] !== -1) {
963
+ if (writeIndex !== row) {
964
+ this.board[writeIndex][col] = this.board[row][col];
965
+ this.board[row][col] = -1;
966
+ }
967
+ writeIndex--;
968
+ }
969
+ }
970
+ }
971
+ }
972
+
973
+ fillEmptySpaces() {
974
+ for (let row = 0; row < this.boardRows; row++) {
975
+ for (let col = 0; col < this.boardCols; col++) {
976
+ if (this.board[row][col] === -1) {
977
+ this.board[row][col] = this.getRandomGem();
978
+ }
979
+ }
980
+ }
981
+ }
982
+
983
+ checkGameOver() {
984
+ // Check if level is complete (reached target score)
985
+ if (this.score >= this.levelTargetScore) {
986
+ this.completeLevel();
987
+ return;
988
+ }
989
+
990
+ // Check if moves are exhausted and target not reached - game over
991
+ if (this.moves <= 0 && this.score < this.levelTargetScore) {
992
+ this.gameOver();
993
+ }
994
+ }
995
+
996
+ gameOver() {
997
+ // Pause the game
998
+ this.isProcessing = true;
999
+
1000
+ // Save best stats
1001
+ this.saveBestStats();
1002
+
1003
+ // Play game over sound
1004
+ if (this.soundManager) {
1005
+ this.soundManager.playGameOverSound();
1006
+ }
1007
+
1008
+ // Show game over modal
1009
+ document.getElementById('final-level').textContent = this.level;
1010
+ document.getElementById('final-score').textContent = this.totalScore.toLocaleString();
1011
+ document.getElementById('game-over').classList.remove('hidden');
1012
+ }
1013
+
1014
+ retryLevel() {
1015
+ // Hide game over modal
1016
+ document.getElementById('game-over').classList.add('hidden');
1017
+
1018
+ // Reset level score (keep total score and level)
1019
+ this.score = 0;
1020
+
1021
+ // Reset moves for current level
1022
+ this.moves = this.getLevelMoves(this.level);
1023
+
1024
+ // Reset game state
1025
+ this.selectedCell = null;
1026
+ this.isProcessing = false;
1027
+
1028
+ // Create new board
1029
+ this.createBoard();
1030
+ this.renderBoard();
1031
+ this.updateUI();
1032
+ }
1033
+
1034
+ completeLevel() {
1035
+ // Pause the game
1036
+ this.isProcessing = true;
1037
+
1038
+ // Update total score
1039
+ this.totalScore += this.score;
1040
+
1041
+ // Play level complete sound
1042
+ if (this.soundManager) {
1043
+ this.soundManager.playLevelCompleteSound();
1044
+ }
1045
+
1046
+ // Show level complete modal
1047
+ document.getElementById('completed-level').textContent = this.level;
1048
+ document.getElementById('level-score').textContent = this.score;
1049
+ document.getElementById('total-score').textContent = this.totalScore;
1050
+ document.getElementById('level-complete').classList.remove('hidden');
1051
+ }
1052
+
1053
+ continueToNextLevel() {
1054
+ // Hide level complete modal
1055
+ document.getElementById('level-complete').classList.add('hidden');
1056
+
1057
+ // Increment level
1058
+ this.level++;
1059
+
1060
+ // Save best stats
1061
+ this.saveBestStats();
1062
+
1063
+ // Increase difficulty: more score needed, but also more moves
1064
+ this.levelTargetScore = this.level * 1000;
1065
+ this.moves = this.getLevelMoves(this.level); // Use correct calculation
1066
+
1067
+ // Reset level score (keep total score)
1068
+ this.score = 0;
1069
+
1070
+ // Create new board
1071
+ this.createBoard();
1072
+ this.renderBoard();
1073
+ this.updateUI();
1074
+
1075
+ // Resume game
1076
+ this.isProcessing = false;
1077
+ }
1078
+
1079
+ goToHome() {
1080
+ // Save best stats before going home
1081
+ this.saveBestStats();
1082
+
1083
+ // Hide all modals
1084
+ document.getElementById('level-complete').classList.add('hidden');
1085
+ document.getElementById('game-over').classList.add('hidden');
1086
+
1087
+ // Show home page
1088
+ this.showHome();
1089
+ }
1090
+
1091
+ endGame() {
1092
+ // Go to home from game over screen
1093
+ this.goToHome();
1094
+ }
1095
+
1096
+ getLevelMoves(level) {
1097
+ // Level 1: 30 moves, Level 2+: 25 + (level * 2)
1098
+ return level === 1 ? 30 : 25 + (level * 2);
1099
+ }
1100
+
1101
+ updateUI() {
1102
+ document.getElementById('score').textContent = this.score;
1103
+ document.getElementById('moves').textContent = Math.max(0, this.moves);
1104
+ document.getElementById('level').textContent = this.level;
1105
+ document.getElementById('target-score').textContent = this.levelTargetScore.toLocaleString();
1106
+
1107
+ // Calculate and display the correct level moves
1108
+ const levelMoves = this.getLevelMoves(this.level);
1109
+ document.getElementById('level-moves').textContent = levelMoves;
1110
+
1111
+ // Show progress to next level (optional visual indicator)
1112
+ const progress = Math.min(100, (this.score / this.levelTargetScore) * 100);
1113
+ // Could add a progress bar here if desired
1114
+ }
1115
+
1116
+ setupEventListeners() {
1117
+ // Helper function to prevent rapid clicks
1118
+ const preventRapidClicks = (callback) => {
1119
+ return (e) => {
1120
+ if (this.buttonClickCooldown) {
1121
+ e.preventDefault();
1122
+ e.stopPropagation();
1123
+ return;
1124
+ }
1125
+
1126
+ this.buttonClickCooldown = true;
1127
+ const button = e.currentTarget;
1128
+
1129
+ // Play click sound
1130
+ if (this.soundManager) {
1131
+ this.soundManager.playClickSound();
1132
+ }
1133
+
1134
+ // Add visual feedback
1135
+ button.style.transform = 'scale(0.95)';
1136
+ setTimeout(() => {
1137
+ button.style.transform = '';
1138
+ }, 150);
1139
+
1140
+ // Execute callback
1141
+ callback();
1142
+
1143
+ // Reset cooldown after a short delay
1144
+ setTimeout(() => {
1145
+ this.buttonClickCooldown = false;
1146
+ }, 300);
1147
+ };
1148
+ };
1149
+
1150
+ document.getElementById('start-game-btn').addEventListener('click', preventRapidClicks(() => {
1151
+ this.startNewGame();
1152
+ }));
1153
+
1154
+ document.getElementById('home-btn').addEventListener('click', preventRapidClicks(() => {
1155
+ this.goToHome();
1156
+ }));
1157
+
1158
+ document.getElementById('reset-btn').addEventListener('click', preventRapidClicks(() => {
1159
+ this.resetGame();
1160
+ }));
1161
+
1162
+ document.getElementById('retry-btn').addEventListener('click', preventRapidClicks(() => {
1163
+ this.retryLevel();
1164
+ }));
1165
+
1166
+ document.getElementById('quit-game-btn').addEventListener('click', preventRapidClicks(() => {
1167
+ this.goToHome();
1168
+ }));
1169
+
1170
+ document.getElementById('hint-btn').addEventListener('click', preventRapidClicks(() => {
1171
+ this.showHint();
1172
+ }));
1173
+
1174
+ document.getElementById('continue-btn').addEventListener('click', preventRapidClicks(() => {
1175
+ this.continueToNextLevel();
1176
+ }));
1177
+
1178
+ document.getElementById('quit-btn').addEventListener('click', preventRapidClicks(() => {
1179
+ this.goToHome();
1180
+ }));
1181
+
1182
+ // Music toggle button
1183
+ const musicToggleBtn = document.getElementById('music-toggle-btn');
1184
+ if (musicToggleBtn) {
1185
+ musicToggleBtn.addEventListener('click', (e) => {
1186
+ e.preventDefault();
1187
+ e.stopPropagation();
1188
+ if (this.soundManager) {
1189
+ this.soundManager.toggleMusic();
1190
+ this.soundManager.playClickSound();
1191
+ }
1192
+ });
1193
+ }
1194
+ }
1195
+
1196
+ startNewGame() {
1197
+ this.score = 0;
1198
+ this.totalScore = 0;
1199
+ this.moves = 30;
1200
+ this.level = 1;
1201
+ this.levelTargetScore = 1000;
1202
+ this.selectedCell = null;
1203
+ this.isProcessing = false;
1204
+ this.showGame();
1205
+ }
1206
+
1207
+ resetGame() {
1208
+ // Reset current game but stay on game screen
1209
+ this.score = 0;
1210
+ this.totalScore = 0;
1211
+ this.moves = 30;
1212
+ this.level = 1;
1213
+ this.levelTargetScore = 1000;
1214
+ this.selectedCell = null;
1215
+ this.isProcessing = false;
1216
+ document.getElementById('game-over').classList.add('hidden');
1217
+ document.getElementById('level-complete').classList.add('hidden');
1218
+ this.createBoard();
1219
+ this.renderBoard();
1220
+ this.updateUI();
1221
+ }
1222
+
1223
+ showHint() {
1224
+ // Find a valid move
1225
+ for (let row = 0; row < this.boardRows - 1; row++) {
1226
+ for (let col = 0; col < this.boardCols; col++) {
1227
+ // Try swapping down
1228
+ [this.board[row][col], this.board[row + 1][col]] =
1229
+ [this.board[row + 1][col], this.board[row][col]];
1230
+
1231
+ if (this.findMatches().length > 0) {
1232
+ // Valid move found - highlight it
1233
+ [this.board[row][col], this.board[row + 1][col]] =
1234
+ [this.board[row + 1][col], this.board[row][col]];
1235
+
1236
+ const cell1 = document.querySelector(`[data-row="${row}"][data-col="${col}"]`);
1237
+ const cell2 = document.querySelector(`[data-row="${row + 1}"][data-col="${col}"]`);
1238
+
1239
+ if (cell1 && cell2) {
1240
+ cell1.style.boxShadow = '0 0 0 4px #00ff00';
1241
+ cell2.style.boxShadow = '0 0 0 4px #00ff00';
1242
+
1243
+ setTimeout(() => {
1244
+ cell1.style.boxShadow = '';
1245
+ cell2.style.boxShadow = '';
1246
+ }, 2000);
1247
+ }
1248
+ return;
1249
+ }
1250
+
1251
+ [this.board[row][col], this.board[row + 1][col]] =
1252
+ [this.board[row + 1][col], this.board[row][col]];
1253
+ }
1254
+ }
1255
+
1256
+ for (let row = 0; row < this.boardRows; row++) {
1257
+ for (let col = 0; col < this.boardCols - 1; col++) {
1258
+ // Try swapping right
1259
+ [this.board[row][col], this.board[row][col + 1]] =
1260
+ [this.board[row][col + 1], this.board[row][col]];
1261
+
1262
+ if (this.findMatches().length > 0) {
1263
+ // Valid move found
1264
+ [this.board[row][col], this.board[row][col + 1]] =
1265
+ [this.board[row][col + 1], this.board[row][col]];
1266
+
1267
+ const cell1 = document.querySelector(`[data-row="${row}"][data-col="${col}"]`);
1268
+ const cell2 = document.querySelector(`[data-row="${row}"][data-col="${col + 1}"]`);
1269
+
1270
+ if (cell1 && cell2) {
1271
+ cell1.style.boxShadow = '0 0 0 4px #00ff00';
1272
+ cell2.style.boxShadow = '0 0 0 4px #00ff00';
1273
+
1274
+ setTimeout(() => {
1275
+ cell1.style.boxShadow = '';
1276
+ cell2.style.boxShadow = '';
1277
+ }, 2000);
1278
+ }
1279
+ return;
1280
+ }
1281
+
1282
+ [this.board[row][col], this.board[row][col + 1]] =
1283
+ [this.board[row][col + 1], this.board[row][col]];
1284
+ }
1285
+ }
1286
+
1287
+ alert('No valid moves found! Shuffling board...');
1288
+ this.createBoard();
1289
+ this.renderBoard();
1290
+ }
1291
+
1292
+ createExplosionParticles(cell) {
1293
+ const particleEmojis = ['✨', '⭐', '💫', '🌟', '⚡', '💥'];
1294
+ const numParticles = 4 + Math.floor(Math.random() * 3); // 4-6 particles per cell
1295
+
1296
+ for (let i = 0; i < numParticles; i++) {
1297
+ const particle = document.createElement('div');
1298
+ particle.className = 'explosion-particle';
1299
+ particle.textContent = particleEmojis[Math.floor(Math.random() * particleEmojis.length)];
1300
+
1301
+ // Generate random nonlinear path
1302
+ const angle = (Math.PI * 2 * i) / numParticles + (Math.random() - 0.5) * 0.8; // Spread around circle with randomness
1303
+ const distance = 20 + Math.random() * 15; // 20-35px distance (scaled down)
1304
+ const curveAmount = 7 + Math.random() * 10; // Curve variation (scaled down)
1305
+
1306
+ // Create curved path with 3 control points
1307
+ const x1 = Math.cos(angle) * (distance * 0.3) + (Math.random() - 0.5) * curveAmount;
1308
+ const y1 = Math.sin(angle) * (distance * 0.3) + (Math.random() - 0.5) * curveAmount;
1309
+
1310
+ const x2 = Math.cos(angle) * (distance * 0.6) + (Math.random() - 0.5) * curveAmount * 1.5;
1311
+ const y2 = Math.sin(angle) * (distance * 0.6) + (Math.random() - 0.5) * curveAmount * 1.5;
1312
+
1313
+ const x3 = Math.cos(angle) * distance + (Math.random() - 0.5) * curveAmount * 2;
1314
+ const y3 = Math.sin(angle) * distance + (Math.random() - 0.5) * curveAmount * 2;
1315
+
1316
+ // Random rotation
1317
+ const rotation = (Math.random() > 0.5 ? 1 : -1) * (720 + Math.random() * 720); // 720-1440 degrees
1318
+
1319
+ // Random scale variation
1320
+ const scale = 0.05 + Math.random() * 0.03; // 0.05-0.08
1321
+
1322
+ // Set CSS custom properties for animation
1323
+ particle.style.setProperty('--particle-x1', `${x1}px`);
1324
+ particle.style.setProperty('--particle-y1', `${y1}px`);
1325
+ particle.style.setProperty('--particle-x2', `${x2}px`);
1326
+ particle.style.setProperty('--particle-y2', `${y2}px`);
1327
+ particle.style.setProperty('--particle-x3', `${x3}px`);
1328
+ particle.style.setProperty('--particle-y3', `${y3}px`);
1329
+ particle.style.setProperty('--particle-rotate', `${rotation}deg`);
1330
+ particle.style.setProperty('--particle-scale', scale.toString());
1331
+
1332
+ // Random delay for staggered effect
1333
+ particle.style.animationDelay = `${Math.random() * 0.2}s`;
1334
+
1335
+ cell.appendChild(particle);
1336
+ }
1337
+ }
1338
+
1339
+ sleep(ms) {
1340
+ return new Promise(resolve => setTimeout(resolve, ms));
1341
+ }
1342
+ }
1343
+
1344
+ // Sound Manager Class
1345
+ class SoundManager {
1346
+ constructor() {
1347
+ this.audioContext = null;
1348
+ this.musicEnabled = localStorage.getItem('musicEnabled') !== 'false';
1349
+ this.soundsEnabled = localStorage.getItem('soundsEnabled') !== 'false';
1350
+ this.musicVolume = parseFloat(localStorage.getItem('musicVolume') || '0.5');
1351
+ this.sfxVolume = parseFloat(localStorage.getItem('sfxVolume') || '0.7');
1352
+ this.musicOscillator = null;
1353
+ this.musicGainNode = null;
1354
+ this.isMusicPlaying = false;
1355
+ this.initAudioContext();
1356
+ this.setupVolumeControls();
1357
+ }
1358
+
1359
+ setupVolumeControls() {
1360
+ // Setup music volume control
1361
+ const musicVolumeSlider = document.getElementById('music-volume');
1362
+ if (musicVolumeSlider) {
1363
+ musicVolumeSlider.value = this.musicVolume * 100;
1364
+ musicVolumeSlider.addEventListener('input', (e) => {
1365
+ this.musicVolume = e.target.value / 100;
1366
+ localStorage.setItem('musicVolume', this.musicVolume);
1367
+ });
1368
+ }
1369
+
1370
+ // Setup SFX volume control
1371
+ const sfxVolumeSlider = document.getElementById('sfx-volume');
1372
+ if (sfxVolumeSlider) {
1373
+ sfxVolumeSlider.value = this.sfxVolume * 100;
1374
+ sfxVolumeSlider.addEventListener('input', (e) => {
1375
+ this.sfxVolume = e.target.value / 100;
1376
+ localStorage.setItem('sfxVolume', this.sfxVolume);
1377
+ });
1378
+ }
1379
+ }
1380
+
1381
+ initAudioContext() {
1382
+ try {
1383
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
1384
+ } catch (e) {
1385
+ console.warn('Web Audio API not supported');
1386
+ }
1387
+ }
1388
+
1389
+ // Start background music (looping, fun upbeat melody)
1390
+ startBackgroundMusic() {
1391
+ if (!this.audioContext || !this.musicEnabled || this.isMusicPlaying) return;
1392
+
1393
+ try {
1394
+ this.isMusicPlaying = true;
1395
+ this.playMusicLoop();
1396
+ } catch (e) {
1397
+ console.warn('Could not start music:', e);
1398
+ }
1399
+ }
1400
+
1401
+ playMusicLoop() {
1402
+ if (!this.audioContext || !this.musicEnabled) return;
1403
+
1404
+ const notes = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]; // C major scale
1405
+ let noteIndex = 0;
1406
+
1407
+ const playNote = () => {
1408
+ if (!this.musicEnabled || !this.isMusicPlaying) return;
1409
+
1410
+ const frequency = notes[noteIndex % notes.length];
1411
+ const oscillator = this.audioContext.createOscillator();
1412
+ const gainNode = this.audioContext.createGain();
1413
+
1414
+ oscillator.connect(gainNode);
1415
+ gainNode.connect(this.audioContext.destination);
1416
+
1417
+ oscillator.type = 'sine';
1418
+ oscillator.frequency.value = frequency;
1419
+
1420
+ const baseGain = this.musicVolume * 0.1;
1421
+ gainNode.gain.setValueAtTime(baseGain, this.audioContext.currentTime);
1422
+ gainNode.gain.exponentialRampToValueAtTime(baseGain * 0.5, this.audioContext.currentTime + 0.3);
1423
+ gainNode.gain.exponentialRampToValueAtTime(baseGain * 0.1, this.audioContext.currentTime + 0.6);
1424
+
1425
+ oscillator.start(this.audioContext.currentTime);
1426
+ oscillator.stop(this.audioContext.currentTime + 0.6);
1427
+
1428
+ noteIndex++;
1429
+
1430
+ setTimeout(() => {
1431
+ if (this.musicEnabled && this.isMusicPlaying) {
1432
+ playNote();
1433
+ }
1434
+ }, 300);
1435
+ };
1436
+
1437
+ playNote();
1438
+ }
1439
+
1440
+ stopBackgroundMusic() {
1441
+ this.isMusicPlaying = false;
1442
+ if (this.musicOscillator) {
1443
+ this.musicOscillator.stop();
1444
+ this.musicOscillator = null;
1445
+ }
1446
+ }
1447
+
1448
+ toggleMusic() {
1449
+ this.musicEnabled = !this.musicEnabled;
1450
+ localStorage.setItem('musicEnabled', this.musicEnabled);
1451
+
1452
+ if (this.musicEnabled) {
1453
+ this.startBackgroundMusic();
1454
+ } else {
1455
+ this.stopBackgroundMusic();
1456
+ }
1457
+
1458
+ this.updateMusicButton();
1459
+ }
1460
+
1461
+ updateMusicButton() {
1462
+ const btn = document.getElementById('music-toggle-btn');
1463
+ if (btn) {
1464
+ btn.textContent = this.musicEnabled ? '🔊' : '🔇';
1465
+ }
1466
+ }
1467
+
1468
+ // Play swipe sound (whoosh)
1469
+ playSwipeSound() {
1470
+ if (!this.audioContext || !this.soundsEnabled) return;
1471
+
1472
+ const oscillator = this.audioContext.createOscillator();
1473
+ const gainNode = this.audioContext.createGain();
1474
+
1475
+ oscillator.connect(gainNode);
1476
+ gainNode.connect(this.audioContext.destination);
1477
+
1478
+ oscillator.type = 'sine';
1479
+ oscillator.frequency.setValueAtTime(400, this.audioContext.currentTime);
1480
+ oscillator.frequency.exponentialRampToValueAtTime(200, this.audioContext.currentTime + 0.1);
1481
+
1482
+ const volume = this.sfxVolume * 0.2;
1483
+ gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
1484
+ gainNode.gain.exponentialRampToValueAtTime(volume * 0.05, this.audioContext.currentTime + 0.1);
1485
+
1486
+ oscillator.start(this.audioContext.currentTime);
1487
+ oscillator.stop(this.audioContext.currentTime + 0.1);
1488
+ }
1489
+
1490
+ // Play match sound (successful match - cheerful chime)
1491
+ playMatchSound(count = 1) {
1492
+ if (!this.audioContext || !this.soundsEnabled) return;
1493
+
1494
+ const frequencies = [523.25, 659.25, 783.99]; // C, E, G chord
1495
+ const delay = 0.05;
1496
+
1497
+ frequencies.forEach((freq, index) => {
1498
+ setTimeout(() => {
1499
+ const oscillator = this.audioContext.createOscillator();
1500
+ const gainNode = this.audioContext.createGain();
1501
+
1502
+ oscillator.connect(gainNode);
1503
+ gainNode.connect(this.audioContext.destination);
1504
+
1505
+ oscillator.type = 'sine';
1506
+ oscillator.frequency.value = freq * (1 + count * 0.1);
1507
+
1508
+ const volume = this.sfxVolume * 0.15;
1509
+ gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
1510
+ gainNode.gain.exponentialRampToValueAtTime(volume * 0.07, this.audioContext.currentTime + 0.3);
1511
+
1512
+ oscillator.start(this.audioContext.currentTime);
1513
+ oscillator.stop(this.audioContext.currentTime + 0.3);
1514
+ }, index * delay);
1515
+ });
1516
+ }
1517
+
1518
+ // Play level complete sound (victory fanfare)
1519
+ playLevelCompleteSound() {
1520
+ if (!this.audioContext || !this.soundsEnabled) return;
1521
+
1522
+ const melody = [523.25, 659.25, 783.99, 1046.50, 783.99, 1046.50];
1523
+ melody.forEach((freq, index) => {
1524
+ setTimeout(() => {
1525
+ const oscillator = this.audioContext.createOscillator();
1526
+ const gainNode = this.audioContext.createGain();
1527
+
1528
+ oscillator.connect(gainNode);
1529
+ gainNode.connect(this.audioContext.destination);
1530
+
1531
+ oscillator.type = 'sine';
1532
+ oscillator.frequency.value = freq;
1533
+
1534
+ const volume = this.sfxVolume * 0.2;
1535
+ gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
1536
+ gainNode.gain.exponentialRampToValueAtTime(volume * 0.05, this.audioContext.currentTime + 0.4);
1537
+
1538
+ oscillator.start(this.audioContext.currentTime);
1539
+ oscillator.stop(this.audioContext.currentTime + 0.4);
1540
+ }, index * 150);
1541
+ });
1542
+ }
1543
+
1544
+ // Play game over sound (sad tone)
1545
+ playGameOverSound() {
1546
+ if (!this.audioContext || !this.soundsEnabled) return;
1547
+
1548
+ const frequencies = [392.00, 349.23, 293.66, 261.63];
1549
+ frequencies.forEach((freq, index) => {
1550
+ setTimeout(() => {
1551
+ const oscillator = this.audioContext.createOscillator();
1552
+ const gainNode = this.audioContext.createGain();
1553
+
1554
+ oscillator.connect(gainNode);
1555
+ gainNode.connect(this.audioContext.destination);
1556
+
1557
+ oscillator.type = 'sine';
1558
+ oscillator.frequency.value = freq;
1559
+
1560
+ const volume = this.sfxVolume * 0.15;
1561
+ gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
1562
+ gainNode.gain.exponentialRampToValueAtTime(volume * 0.07, this.audioContext.currentTime + 0.5);
1563
+
1564
+ oscillator.start(this.audioContext.currentTime);
1565
+ oscillator.stop(this.audioContext.currentTime + 0.5);
1566
+ }, index * 200);
1567
+ });
1568
+ }
1569
+
1570
+ // Play button click sound
1571
+ playClickSound() {
1572
+ if (!this.audioContext || !this.soundsEnabled) return;
1573
+
1574
+ const oscillator = this.audioContext.createOscillator();
1575
+ const gainNode = this.audioContext.createGain();
1576
+
1577
+ oscillator.connect(gainNode);
1578
+ gainNode.connect(this.audioContext.destination);
1579
+
1580
+ oscillator.type = 'sine';
1581
+ oscillator.frequency.value = 800;
1582
+
1583
+ const volume = this.sfxVolume * 0.1;
1584
+ gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
1585
+ gainNode.gain.exponentialRampToValueAtTime(volume * 0.1, this.audioContext.currentTime + 0.05);
1586
+
1587
+ oscillator.start(this.audioContext.currentTime);
1588
+ oscillator.stop(this.audioContext.currentTime + 0.05);
1589
+ }
1590
+ }
1591
+
1592
+ // Initialize game when page loads
1593
+ let game;
1594
+ window.addEventListener('DOMContentLoaded', () => {
1595
+ game = new Match3Game();
1596
+ });
1597
+
index.html ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Reality Swipe</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ </head>
9
+ <body>
10
+ <!-- Home Page -->
11
+ <div id="home-page" class="home-page">
12
+ <div class="home-content">
13
+ <h1 class="home-title">Reality Swipe</h1>
14
+ <p class="home-subtitle">Match gems and create cascades!</p>
15
+
16
+ <div class="home-stats" id="home-stats">
17
+ <div class="stat-item">
18
+ <span class="stat-label">Best Level</span>
19
+ <span class="stat-value" id="best-level">1</span>
20
+ </div>
21
+ <div class="stat-item">
22
+ <span class="stat-label">Best Score</span>
23
+ <span class="stat-value" id="best-score">0</span>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="home-buttons">
28
+ <button id="start-game-btn" class="btn btn-primary btn-large">Start Game</button>
29
+ </div>
30
+
31
+ <div class="theme-selector">
32
+ <label for="theme-select">Theme:</label>
33
+ <select id="theme-select" class="theme-select">
34
+ <option value="light">Light</option>
35
+ <option value="dark">Dark</option>
36
+ </select>
37
+ </div>
38
+
39
+ <div class="home-instructions">
40
+ <h3>How to Play</h3>
41
+ <ul>
42
+ <li>Drag or click adjacent gems to swap them</li>
43
+ <li>Match 3 or more gems in a row or column</li>
44
+ <li>Reach the target score to complete each level</li>
45
+ <li>Levels are infinite - see how far you can go!</li>
46
+ </ul>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Game Screen -->
52
+ <div class="game-header hidden">
53
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 7.5px;">
54
+ <h1 style="margin: 0;">Reality Swipe</h1>
55
+ <div style="display: flex; gap: 10px; align-items: center;">
56
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 2px;">
57
+ <label style="font-size: 0.6em; color: #666;">Music</label>
58
+ <input type="range" id="music-volume" min="0" max="100" value="50" style="width: 60px; height: 4px;">
59
+ </div>
60
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 2px;">
61
+ <label style="font-size: 0.6em; color: #666;">SFX</label>
62
+ <input type="range" id="sfx-volume" min="0" max="100" value="70" style="width: 60px; height: 4px;">
63
+ </div>
64
+ <button id="music-toggle-btn" class="music-toggle-btn" title="Toggle Music">🔊</button>
65
+ </div>
66
+ </div>
67
+ <div class="score-panel">
68
+ <div class="score-item">
69
+ <span class="label">Score:</span>
70
+ <span id="score" class="value">0</span>
71
+ </div>
72
+ <div class="score-item">
73
+ <span class="label">Target:</span>
74
+ <span id="target-score" class="value">1000</span>
75
+ </div>
76
+ <div class="score-item">
77
+ <span class="label">Moves:</span>
78
+ <span id="moves" class="value">30</span>
79
+ <span class="moves-info">/ <span id="level-moves">30</span></span>
80
+ </div>
81
+ <div class="score-item">
82
+ <span class="label">Level:</span>
83
+ <span id="level" class="value">1</span>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <div id="game-board" class="game-board"></div>
89
+
90
+ <div class="game-controls">
91
+ <button id="home-btn" class="btn btn-secondary">Home</button>
92
+ <button id="reset-btn" class="btn btn-primary">New Game</button>
93
+ <button id="hint-btn" class="btn btn-secondary">Hint</button>
94
+ </div>
95
+
96
+ <div id="game-over" class="game-over hidden">
97
+ <div class="game-over-content">
98
+ <h2>Game Over!</h2>
99
+ <p>Level Reached: <span id="final-level">1</span></p>
100
+ <p>Final Score: <span id="final-score">0</span></p>
101
+ <div class="game-over-buttons">
102
+ <button id="retry-btn" class="btn btn-primary">Retry Level</button>
103
+ <button id="quit-game-btn" class="btn btn-secondary">Quit</button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <div id="level-complete" class="level-complete hidden">
109
+ <div class="level-complete-content">
110
+ <h2>🎉 Level Complete!</h2>
111
+ <p>Level <span id="completed-level">1</span> Completed!</p>
112
+ <p>Score: <span id="level-score">0</span></p>
113
+ <p>Total Score: <span id="total-score">0</span></p>
114
+ <div class="level-complete-buttons">
115
+ <button id="continue-btn" class="btn btn-primary">Continue to Next Level</button>
116
+ <button id="quit-btn" class="btn btn-secondary">Quit</button>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <script src="game.js"></script>
122
+ </body>
123
+ </html>
124
+
styles.css ADDED
@@ -0,0 +1,1155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9
+ background: white;
10
+ min-height: 100vh;
11
+ display: flex;
12
+ flex-direction: column;
13
+ justify-content: flex-start;
14
+ align-items: center;
15
+ padding: 20px;
16
+ }
17
+
18
+ .hidden {
19
+ display: none !important;
20
+ visibility: hidden !important;
21
+ }
22
+
23
+ /* Ensure game-controls stays visible when not hidden */
24
+ .game-controls:not(.hidden) {
25
+ display: flex !important;
26
+ visibility: visible !important;
27
+ opacity: 1 !important;
28
+ }
29
+
30
+ /* Home Page Styles */
31
+ .home-page {
32
+ width: 100%;
33
+ max-width: 600px;
34
+ display: flex;
35
+ justify-content: center;
36
+ align-items: center;
37
+ min-height: 80vh;
38
+ }
39
+
40
+ .home-content {
41
+ text-align: center;
42
+ width: 100%;
43
+ animation: fadeIn 0.5s ease-in;
44
+ }
45
+
46
+ .home-title {
47
+ font-size: 4em;
48
+ color: #333;
49
+ margin-bottom: 10px;
50
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
51
+ background: linear-gradient(135deg, #ff6b9d 0%, #ff8fab 100%);
52
+ -webkit-background-clip: text;
53
+ -webkit-text-fill-color: transparent;
54
+ background-clip: text;
55
+ }
56
+
57
+ .home-subtitle {
58
+ font-size: 1.5em;
59
+ color: #666;
60
+ margin-bottom: 40px;
61
+ }
62
+
63
+ .home-stats {
64
+ display: flex;
65
+ justify-content: center;
66
+ gap: 40px;
67
+ margin: 40px 0;
68
+ padding: 30px;
69
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
70
+ border-radius: 15px;
71
+ }
72
+
73
+ .stat-item {
74
+ display: flex;
75
+ flex-direction: column;
76
+ align-items: center;
77
+ }
78
+
79
+ .stat-label {
80
+ font-size: 0.9em;
81
+ color: #666;
82
+ margin-bottom: 10px;
83
+ font-weight: 600;
84
+ }
85
+
86
+ .stat-value {
87
+ font-size: 2.5em;
88
+ color: #ff6b9d;
89
+ font-weight: bold;
90
+ }
91
+
92
+ .home-buttons {
93
+ margin: 40px 0;
94
+ }
95
+
96
+ .btn-large {
97
+ padding: 18px 50px;
98
+ font-size: 1.3em;
99
+ font-weight: 700;
100
+ letter-spacing: 1px;
101
+ }
102
+
103
+ .home-instructions {
104
+ margin-top: 50px;
105
+ padding: 30px;
106
+ background: #f8f9fa;
107
+ border-radius: 15px;
108
+ text-align: left;
109
+ }
110
+
111
+ .home-instructions h3 {
112
+ color: #333;
113
+ margin-bottom: 20px;
114
+ text-align: center;
115
+ font-size: 1.5em;
116
+ }
117
+
118
+ .home-instructions ul {
119
+ list-style: none;
120
+ padding: 0;
121
+ }
122
+
123
+ .home-instructions li {
124
+ padding: 10px 0;
125
+ padding-left: 30px;
126
+ position: relative;
127
+ color: #555;
128
+ font-size: 1.1em;
129
+ }
130
+
131
+ .home-instructions li:before {
132
+ content: "✓";
133
+ position: absolute;
134
+ left: 0;
135
+ color: #ff6b9d;
136
+ font-weight: bold;
137
+ font-size: 1.2em;
138
+ }
139
+
140
+ .theme-selector {
141
+ margin-top: 30px;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ gap: 10px;
146
+ }
147
+
148
+ .theme-selector label {
149
+ font-size: 1em;
150
+ color: #666;
151
+ font-weight: 600;
152
+ }
153
+
154
+ .theme-select {
155
+ padding: 8px 15px;
156
+ border: 2px solid #ff6b9d;
157
+ border-radius: 8px;
158
+ background: white;
159
+ color: #333;
160
+ font-size: 1em;
161
+ font-weight: 500;
162
+ cursor: pointer;
163
+ outline: none;
164
+ transition: all 0.2s ease;
165
+ }
166
+
167
+ .theme-select:hover {
168
+ background: #fff5f8;
169
+ border-color: #ff8fab;
170
+ }
171
+
172
+ .theme-select:focus {
173
+ border-color: #ff6b9d;
174
+ box-shadow: 0 0 0 3px rgba(255, 107, 157, 0.2);
175
+ }
176
+
177
+ /* Dark theme */
178
+ body.dark-theme {
179
+ background: #1a1a2e;
180
+ color: #e0e0e0;
181
+ }
182
+
183
+ body.dark-theme .home-title {
184
+ background: linear-gradient(135deg, #ff6b9d 0%, #ff8fab 100%);
185
+ -webkit-background-clip: text;
186
+ -webkit-text-fill-color: transparent;
187
+ background-clip: text;
188
+ }
189
+
190
+ body.dark-theme .home-subtitle {
191
+ color: #b0b0b0;
192
+ }
193
+
194
+ body.dark-theme .home-stats {
195
+ background: linear-gradient(135deg, #16213e 0%, #0f3460 100%);
196
+ border: 1px solid rgba(255, 107, 157, 0.3);
197
+ }
198
+
199
+ body.dark-theme .stat-label {
200
+ color: #b0b0b0;
201
+ }
202
+
203
+ body.dark-theme .stat-value {
204
+ color: #ff6b9d;
205
+ }
206
+
207
+ body.dark-theme .home-instructions {
208
+ background: #16213e;
209
+ border: 1px solid rgba(255, 107, 157, 0.2);
210
+ }
211
+
212
+ body.dark-theme .home-instructions h3 {
213
+ color: #e0e0e0;
214
+ }
215
+
216
+ body.dark-theme .home-instructions li {
217
+ color: #b0b0b0;
218
+ }
219
+
220
+ body.dark-theme .home-instructions li:before {
221
+ color: #ff6b9d;
222
+ }
223
+
224
+ body.dark-theme .theme-select {
225
+ background: #16213e;
226
+ color: #e0e0e0;
227
+ border-color: #ff6b9d;
228
+ }
229
+
230
+ body.dark-theme .theme-select:hover {
231
+ background: #1a2742;
232
+ }
233
+
234
+ body.dark-theme .game-header {
235
+ background: #16213e;
236
+ border-color: #ff6b9d;
237
+ color: #e0e0e0;
238
+ }
239
+
240
+ body.dark-theme .game-header h1 {
241
+ color: #e0e0e0;
242
+ }
243
+
244
+ body.dark-theme .score-panel {
245
+ background: linear-gradient(135deg, #0f3460 0%, #16213e 100%);
246
+ }
247
+
248
+ body.dark-theme .score-item .label {
249
+ color: #b0b0b0;
250
+ }
251
+
252
+ body.dark-theme .score-item .value {
253
+ color: #e0e0e0;
254
+ }
255
+
256
+ body.dark-theme .game-board {
257
+ background: #0f3460;
258
+ box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.5);
259
+ }
260
+
261
+ body.dark-theme .game-controls .btn {
262
+ background: #16213e;
263
+ color: #e0e0e0;
264
+ border-color: #ff6b9d;
265
+ }
266
+
267
+ body.dark-theme .game-controls .btn:hover {
268
+ background: #1a2742;
269
+ border-color: #ff8fab;
270
+ }
271
+
272
+ body.dark-theme .game-over-content {
273
+ background: #16213e;
274
+ color: #e0e0e0;
275
+ }
276
+
277
+ body.dark-theme .game-over-content h2 {
278
+ color: #e0e0e0;
279
+ }
280
+
281
+ body.dark-theme .game-over-content p {
282
+ color: #b0b0b0;
283
+ }
284
+
285
+ body.dark-theme .level-complete-content {
286
+ background: linear-gradient(135deg, #ff6b9d 0%, #ff8fab 100%);
287
+ }
288
+
289
+ .game-header {
290
+ text-align: center;
291
+ margin: 0 auto 20px;
292
+ background: white;
293
+ border: 3px solid #D4AF37;
294
+ border-radius: 15px;
295
+ padding: 10px;
296
+ transform-style: preserve-3d;
297
+ transform: translateZ(10px) rotateX(2deg);
298
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2),
299
+ 0 4px 8px rgba(0, 0, 0, 0.15),
300
+ inset 0 2px 0 rgba(255, 255, 255, 0.5),
301
+ inset 0 -2px 4px rgba(212, 175, 55, 0.3),
302
+ 0 0 0 1px rgba(212, 175, 55, 0.5);
303
+ position: relative;
304
+ overflow: hidden;
305
+ width: calc(8 * 97.2px + 7 * 2px + 4px); /* Match grid width: 8 cells + 7 gaps + padding */
306
+ max-width: 100%;
307
+ perspective: 1200px;
308
+ perspective-origin: center top;
309
+ display: block;
310
+ }
311
+
312
+ .game-header::before {
313
+ content: '';
314
+ position: absolute;
315
+ top: 0;
316
+ left: 0;
317
+ right: 0;
318
+ bottom: 0;
319
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(212, 175, 55, 0.05) 100%);
320
+ pointer-events: none;
321
+ z-index: 0;
322
+ }
323
+
324
+ .game-header > * {
325
+ position: relative;
326
+ z-index: 1;
327
+ }
328
+
329
+ .game-header h1 {
330
+ color: #333;
331
+ font-size: 1.25em;
332
+ margin: 0;
333
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
334
+ }
335
+
336
+ .music-toggle-btn {
337
+ background: rgba(102, 126, 234, 0.1);
338
+ border: 2px solid rgba(102, 126, 234, 0.3);
339
+ border-radius: 50%;
340
+ width: 40px;
341
+ height: 40px;
342
+ font-size: 1.2em;
343
+ cursor: pointer;
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ transition: all 0.2s ease;
348
+ padding: 0;
349
+ outline: none;
350
+ user-select: none;
351
+ -webkit-user-select: none;
352
+ }
353
+
354
+ .music-toggle-btn:hover {
355
+ background: rgba(102, 126, 234, 0.2);
356
+ border-color: rgba(102, 126, 234, 0.5);
357
+ transform: scale(1.1);
358
+ }
359
+
360
+ .music-toggle-btn:active {
361
+ transform: scale(0.95);
362
+ }
363
+
364
+ /* Volume slider styles */
365
+ #music-volume,
366
+ #sfx-volume {
367
+ -webkit-appearance: none;
368
+ appearance: none;
369
+ background: rgba(102, 126, 234, 0.2);
370
+ border-radius: 5px;
371
+ outline: none;
372
+ cursor: pointer;
373
+ }
374
+
375
+ #music-volume::-webkit-slider-thumb,
376
+ #sfx-volume::-webkit-slider-thumb {
377
+ -webkit-appearance: none;
378
+ appearance: none;
379
+ width: 12px;
380
+ height: 12px;
381
+ background: #667eea;
382
+ border-radius: 50%;
383
+ cursor: pointer;
384
+ transition: all 0.2s ease;
385
+ }
386
+
387
+ #music-volume::-webkit-slider-thumb:hover,
388
+ #sfx-volume::-webkit-slider-thumb:hover {
389
+ background: #764ba2;
390
+ transform: scale(1.2);
391
+ }
392
+
393
+ #music-volume::-moz-range-thumb,
394
+ #sfx-volume::-moz-range-thumb {
395
+ width: 12px;
396
+ height: 12px;
397
+ background: #667eea;
398
+ border-radius: 50%;
399
+ cursor: pointer;
400
+ border: none;
401
+ transition: all 0.2s ease;
402
+ }
403
+
404
+ #music-volume::-moz-range-thumb:hover,
405
+ #sfx-volume::-moz-range-thumb:hover {
406
+ background: #764ba2;
407
+ transform: scale(1.2);
408
+ }
409
+
410
+ .score-panel {
411
+ display: grid;
412
+ grid-template-columns: repeat(4, 1fr);
413
+ gap: 5px;
414
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
415
+ padding: 7.5px;
416
+ border-radius: 15px;
417
+ margin-bottom: 5px;
418
+ }
419
+
420
+ .score-item {
421
+ display: flex;
422
+ flex-direction: column;
423
+ align-items: center;
424
+ }
425
+
426
+ .score-item .label {
427
+ font-size: 0.7em;
428
+ color: #666;
429
+ margin-bottom: 2.5px;
430
+ font-weight: 600;
431
+ }
432
+
433
+ .score-item .value {
434
+ font-size: 1.2em;
435
+ color: #333;
436
+ font-weight: bold;
437
+ }
438
+
439
+ .score-item .moves-info {
440
+ font-size: 0.9em;
441
+ color: #999;
442
+ margin-left: 3px;
443
+ font-weight: normal;
444
+ }
445
+
446
+ .game-board {
447
+ display: grid;
448
+ grid-template-columns: repeat(8, 1fr);
449
+ grid-template-rows: repeat(7, 1fr);
450
+ gap: 2px;
451
+ background: white;
452
+ padding: 2px;
453
+ border-radius: 10px;
454
+ box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.1);
455
+ transition: all 0.3s ease;
456
+ transform-style: preserve-3d;
457
+ margin: 20px auto;
458
+ perspective: 1000px;
459
+ perspective-origin: center center;
460
+ width: calc(8 * 97.2px + 7 * 2px + 4px); /* 8 cells + 7 gaps + padding */
461
+ max-width: 100%;
462
+ }
463
+
464
+ .game-board.is-dragging {
465
+ box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.1),
466
+ 0 0 30px rgba(100, 150, 255, 0.3);
467
+ }
468
+
469
+ .cell {
470
+ width: 97.2px;
471
+ height: 97.2px;
472
+ border-radius: 12.96px;
473
+ cursor: pointer;
474
+ transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
475
+ box-shadow 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
476
+ opacity 0.3s ease;
477
+ display: flex;
478
+ justify-content: center;
479
+ align-items: center;
480
+ }
481
+
482
+ .cell.empty-cell {
483
+ display: none !important;
484
+ }
485
+ font-size: 3.24em;
486
+ padding: 0;
487
+ position: relative;
488
+ transform-style: preserve-3d;
489
+ transform: translateZ(0);
490
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3),
491
+ 0 2px 4px rgba(0, 0, 0, 0.2),
492
+ inset 0 1px 0 rgba(255, 255, 255, 0.3),
493
+ inset 0 -2px 4px rgba(0, 0, 0, 0.2);
494
+ will-change: transform;
495
+ }
496
+
497
+ .cell:hover {
498
+ transform: scale(1.1) translateY(0) translateZ(10px) rotateX(5deg) rotateY(5deg);
499
+ z-index: 10;
500
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4),
501
+ 0 4px 8px rgba(0, 0, 0, 0.3),
502
+ inset 0 2px 0 rgba(255, 255, 255, 0.4),
503
+ inset 0 -3px 6px rgba(0, 0, 0, 0.3);
504
+ animation: none;
505
+ }
506
+
507
+ .cell.selected {
508
+ transform: scale(1.25) translateZ(20px) rotateX(8deg) rotateY(8deg);
509
+ box-shadow: 0 0 0 6px rgba(255, 255, 255, 1),
510
+ 0 0 20px rgba(100, 150, 255, 0.8),
511
+ 0 0 40px rgba(100, 150, 255, 0.6),
512
+ 0 12px 24px rgba(0, 0, 0, 0.5),
513
+ inset 0 3px 0 rgba(255, 255, 255, 0.5),
514
+ inset 0 -4px 8px rgba(0, 0, 0, 0.4);
515
+ z-index: 30;
516
+ animation: dragPulse 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
517
+ border: 3px solid rgba(100, 150, 255, 1);
518
+ transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94),
519
+ box-shadow 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
520
+ }
521
+
522
+ .cell.drag-target {
523
+ transform: scale(1.2) translateZ(15px) rotateX(-4deg) rotateY(-4deg);
524
+ box-shadow: 0 0 0 6px rgba(0, 255, 100, 1),
525
+ 0 0 25px rgba(0, 255, 100, 0.9),
526
+ 0 0 50px rgba(0, 255, 100, 0.7),
527
+ 0 10px 20px rgba(0, 0, 0, 0.5),
528
+ inset 0 2px 0 rgba(255, 255, 255, 0.4),
529
+ inset 0 -3px 6px rgba(0, 0, 0, 0.3);
530
+ z-index: 25;
531
+ animation: targetPulse 0.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
532
+ border: 3px solid rgba(0, 255, 100, 1);
533
+ background-blend-mode: overlay;
534
+ transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94),
535
+ box-shadow 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
536
+ }
537
+
538
+ .cell.dragging {
539
+ opacity: 0.9;
540
+ z-index: 35;
541
+ transition: opacity 0.2s ease;
542
+ }
543
+
544
+ .cell {
545
+ user-select: none;
546
+ -webkit-user-select: none;
547
+ cursor: grab;
548
+ }
549
+
550
+ .cell:active {
551
+ cursor: grabbing;
552
+ }
553
+
554
+ @keyframes dragPulse {
555
+ 0%, 100% {
556
+ transform: scale(1.25) translateZ(20px) rotateX(8deg) rotateY(8deg);
557
+ box-shadow: 0 0 0 6px rgba(255, 255, 255, 1),
558
+ 0 0 20px rgba(100, 150, 255, 0.8),
559
+ 0 0 40px rgba(100, 150, 255, 0.6),
560
+ 0 12px 24px rgba(0, 0, 0, 0.5),
561
+ inset 0 3px 0 rgba(255, 255, 255, 0.5),
562
+ inset 0 -4px 8px rgba(0, 0, 0, 0.4);
563
+ }
564
+ 50% {
565
+ transform: scale(1.28) translateZ(22px) rotateX(9deg) rotateY(9deg);
566
+ box-shadow: 0 0 0 7px rgba(255, 255, 255, 1),
567
+ 0 0 25px rgba(100, 150, 255, 0.9),
568
+ 0 0 50px rgba(100, 150, 255, 0.7),
569
+ 0 13px 26px rgba(0, 0, 0, 0.5),
570
+ inset 0 3.5px 0 rgba(255, 255, 255, 0.55),
571
+ inset 0 -4.5px 9px rgba(0, 0, 0, 0.45);
572
+ }
573
+ }
574
+
575
+ @keyframes targetPulse {
576
+ 0%, 100% {
577
+ transform: scale(1.2) translateZ(15px) rotateX(-4deg) rotateY(-4deg);
578
+ box-shadow: 0 0 0 6px rgba(0, 255, 100, 1),
579
+ 0 0 25px rgba(0, 255, 100, 0.9),
580
+ 0 0 50px rgba(0, 255, 100, 0.7),
581
+ 0 10px 20px rgba(0, 0, 0, 0.5),
582
+ inset 0 2px 0 rgba(255, 255, 255, 0.4),
583
+ inset 0 -3px 6px rgba(0, 0, 0, 0.3);
584
+ }
585
+ 50% {
586
+ transform: scale(1.23) translateZ(17px) rotateX(-5deg) rotateY(-5deg);
587
+ box-shadow: 0 0 0 7px rgba(0, 255, 100, 1),
588
+ 0 0 30px rgba(0, 255, 100, 0.95),
589
+ 0 0 60px rgba(0, 255, 100, 0.8),
590
+ 0 11px 22px rgba(0, 0, 0, 0.5),
591
+ inset 0 2.5px 0 rgba(255, 255, 255, 0.45),
592
+ inset 0 -3.5px 7px rgba(0, 0, 0, 0.35);
593
+ }
594
+ }
595
+
596
+ /* Drag connection line */
597
+ .drag-connection {
598
+ position: absolute;
599
+ pointer-events: none;
600
+ z-index: 20;
601
+ stroke: rgba(100, 150, 255, 0.8);
602
+ stroke-width: 4;
603
+ fill: none;
604
+ stroke-dasharray: 5, 5;
605
+ animation: dashMove 0.5s linear infinite;
606
+ }
607
+
608
+ @keyframes dashMove {
609
+ 0% {
610
+ stroke-dashoffset: 0;
611
+ }
612
+ 100% {
613
+ stroke-dashoffset: 10;
614
+ }
615
+ }
616
+
617
+ .cell.match-enlarge {
618
+ animation: none;
619
+ z-index: 15;
620
+ }
621
+
622
+ .cell.matched {
623
+ animation: none;
624
+ z-index: 20;
625
+ pointer-events: none;
626
+ position: relative;
627
+ overflow: visible;
628
+ opacity: 0;
629
+ display: none !important;
630
+ visibility: hidden !important;
631
+ }
632
+
633
+ /* Particle styles - dynamically created */
634
+ .explosion-particle {
635
+ position: absolute;
636
+ font-size: 8px;
637
+ top: 50%;
638
+ left: 50%;
639
+ z-index: 21;
640
+ pointer-events: none;
641
+ filter: drop-shadow(0 0 3px rgba(255, 215, 0, 1))
642
+ drop-shadow(0 0 6px rgba(255, 165, 0, 0.9))
643
+ drop-shadow(0 0 9px rgba(255, 100, 0, 0.7));
644
+ text-shadow: 0 0 5px rgba(255, 255, 255, 1),
645
+ 0 0 9px rgba(255, 215, 0, 1),
646
+ 0 0 13px rgba(255, 165, 0, 0.9);
647
+ animation: particleExplode 1s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
648
+ --particle-x1: 0px;
649
+ --particle-y1: 0px;
650
+ --particle-x2: 0px;
651
+ --particle-y2: 0px;
652
+ --particle-x3: 0px;
653
+ --particle-y3: 0px;
654
+ --particle-rotate: 0deg;
655
+ --particle-scale: 1;
656
+ }
657
+
658
+ @keyframes particleExplode {
659
+ 0% {
660
+ transform: translate(-50%, -50%) scale(0) rotate(0deg);
661
+ opacity: 1;
662
+ filter: brightness(1);
663
+ }
664
+ 15% {
665
+ opacity: 1;
666
+ transform: translate(-50%, -50%) translate(var(--particle-x1), var(--particle-y1)) scale(1.3) rotate(calc(var(--particle-rotate) * 0.25));
667
+ filter: brightness(2);
668
+ }
669
+ 40% {
670
+ opacity: 1;
671
+ transform: translate(-50%, -50%) translate(var(--particle-x2), var(--particle-y2)) scale(1.5) rotate(calc(var(--particle-rotate) * 0.6));
672
+ filter: brightness(3);
673
+ }
674
+ 70% {
675
+ opacity: 0.8;
676
+ transform: translate(-50%, -50%) translate(var(--particle-x3), var(--particle-y3)) scale(calc(var(--particle-scale) * 1.7)) rotate(calc(var(--particle-rotate) * 0.9));
677
+ filter: brightness(2);
678
+ }
679
+ 100% {
680
+ opacity: 0;
681
+ transform: translate(-50%, -50%) translate(var(--particle-x3), var(--particle-y3)) scale(calc(var(--particle-scale) * 1.8)) rotate(var(--particle-rotate));
682
+ filter: brightness(0);
683
+ }
684
+ }
685
+
686
+ .cell.falling {
687
+ animation: none;
688
+ }
689
+
690
+ @keyframes enlargeMatch {
691
+ 0% {
692
+ transform: scale(1) translateZ(0) rotateX(0deg) rotateY(0deg);
693
+ }
694
+ 50% {
695
+ transform: scale(1.25) translateZ(10px) rotateX(6deg) rotateY(6deg);
696
+ box-shadow: 0 0 15px rgba(255, 215, 0, 0.8),
697
+ 0 6px 12px rgba(0, 0, 0, 0.4),
698
+ inset 0 2px 0 rgba(255, 255, 255, 0.4);
699
+ }
700
+ 100% {
701
+ transform: scale(1) translateZ(0) rotateX(0deg) rotateY(0deg);
702
+ }
703
+ }
704
+
705
+ @keyframes matchAnimation {
706
+ 0% {
707
+ transform: scale(1) rotate(0deg) translateZ(0) rotateX(0deg) rotateY(0deg);
708
+ opacity: 1;
709
+ filter: brightness(1) saturate(1);
710
+ }
711
+ 20% {
712
+ transform: scale(1.3) rotate(60deg) translateZ(15px) rotateX(20deg) rotateY(20deg);
713
+ opacity: 1;
714
+ filter: brightness(2.5) saturate(2);
715
+ box-shadow: 0 0 25px rgba(255, 255, 255, 1),
716
+ 0 0 40px rgba(255, 215, 0, 1),
717
+ 0 0 55px rgba(255, 165, 0, 0.9),
718
+ 0 8px 16px rgba(0, 0, 0, 0.4);
719
+ }
720
+ 40% {
721
+ transform: scale(1.4) rotate(120deg) translateZ(20px) rotateX(30deg) rotateY(30deg);
722
+ opacity: 0.9;
723
+ filter: brightness(3) saturate(2.2);
724
+ box-shadow: 0 0 30px rgba(255, 255, 255, 0.95),
725
+ 0 0 50px rgba(255, 215, 0, 0.95),
726
+ 0 0 70px rgba(255, 165, 0, 0.85),
727
+ 0 9px 18px rgba(0, 0, 0, 0.45);
728
+ }
729
+ 60% {
730
+ transform: scale(1.2) rotate(180deg) translateZ(15px) rotateX(40deg) rotateY(40deg);
731
+ opacity: 0.7;
732
+ filter: brightness(2) saturate(1.5);
733
+ box-shadow: 0 0 20px rgba(255, 255, 255, 0.8),
734
+ 0 0 35px rgba(255, 215, 0, 0.8),
735
+ 0 0 50px rgba(255, 165, 0, 0.7),
736
+ 0 7px 14px rgba(0, 0, 0, 0.4);
737
+ }
738
+ 80% {
739
+ transform: scale(0.8) rotate(240deg) translateZ(8px) rotateX(50deg) rotateY(50deg);
740
+ opacity: 0.4;
741
+ filter: brightness(1.5) saturate(1);
742
+ box-shadow: 0 0 15px rgba(255, 255, 255, 0.6),
743
+ 0 0 25px rgba(255, 215, 0, 0.6),
744
+ 0 0 35px rgba(255, 165, 0, 0.5),
745
+ 0 5px 10px rgba(0, 0, 0, 0.3);
746
+ }
747
+ 100% {
748
+ transform: scale(0.3) rotate(300deg) translateZ(0) rotateX(60deg) rotateY(60deg);
749
+ opacity: 0;
750
+ filter: brightness(0.5) saturate(0.5);
751
+ box-shadow: 0 0 0 rgba(255, 255, 255, 0);
752
+ }
753
+ }
754
+
755
+ @keyframes fallAnimation {
756
+ 0% {
757
+ transform: translateY(-100%) translateZ(0) rotateX(0deg);
758
+ }
759
+ 100% {
760
+ transform: translateY(0) translateZ(0) rotateX(0deg);
761
+ }
762
+ }
763
+
764
+ /* Gem colors with slow idle animations */
765
+ .gem-0 {
766
+ background: white;
767
+ animation: gemFloat 4s ease-in-out infinite;
768
+ animation-delay: 0s;
769
+ }
770
+ .gem-1 {
771
+ background: white;
772
+ animation: none;
773
+ }
774
+ .gem-2 {
775
+ background: white;
776
+ animation: none;
777
+ }
778
+ .gem-3 {
779
+ background: white;
780
+ animation: none;
781
+ }
782
+ .gem-4 {
783
+ background: white;
784
+ animation: none;
785
+ }
786
+ .gem-5 {
787
+ background: white;
788
+ animation: none;
789
+ }
790
+
791
+ /* Slow floating animation for gems with 3D rotation */
792
+ @keyframes gemFloat {
793
+ 0%, 100% {
794
+ transform: translateY(0) scale(1) translateZ(0) rotateX(0deg) rotateY(0deg);
795
+ opacity: 1;
796
+ }
797
+ 25% {
798
+ transform: translateY(-2px) scale(1.01) translateZ(2px) rotateX(2deg) rotateY(-2deg);
799
+ opacity: 0.98;
800
+ }
801
+ 50% {
802
+ transform: translateY(-3px) scale(1.02) translateZ(3px) rotateX(0deg) rotateY(0deg);
803
+ opacity: 0.95;
804
+ }
805
+ 75% {
806
+ transform: translateY(-2px) scale(1.01) translateZ(2px) rotateX(-2deg) rotateY(2deg);
807
+ opacity: 0.98;
808
+ }
809
+ }
810
+
811
+ /* Pause gemFloat animation when cell is in certain states */
812
+ .cell.selected.gem-0,
813
+ .cell.selected.gem-1,
814
+ .cell.selected.gem-2,
815
+ .cell.selected.gem-3,
816
+ .cell.selected.gem-4,
817
+ .cell.selected.gem-5,
818
+ .cell.dragging.gem-0,
819
+ .cell.dragging.gem-1,
820
+ .cell.dragging.gem-2,
821
+ .cell.dragging.gem-3,
822
+ .cell.dragging.gem-4,
823
+ .cell.dragging.gem-5,
824
+ .cell.drag-target.gem-0,
825
+ .cell.drag-target.gem-1,
826
+ .cell.drag-target.gem-2,
827
+ .cell.drag-target.gem-3,
828
+ .cell.drag-target.gem-4,
829
+ .cell.drag-target.gem-5,
830
+ .cell.matched.gem-0,
831
+ .cell.matched.gem-1,
832
+ .cell.matched.gem-2,
833
+ .cell.matched.gem-3,
834
+ .cell.matched.gem-4,
835
+ .cell.matched.gem-5 {
836
+ animation: none !important;
837
+ }
838
+
839
+ /* Match enlarge animation - paused */
840
+ .cell.match-enlarge {
841
+ animation: none !important;
842
+ }
843
+
844
+ .cell.match-enlarge.gem-0,
845
+ .cell.match-enlarge.gem-1,
846
+ .cell.match-enlarge.gem-2,
847
+ .cell.match-enlarge.gem-3,
848
+ .cell.match-enlarge.gem-4,
849
+ .cell.match-enlarge.gem-5 {
850
+ animation: none !important;
851
+ }
852
+
853
+ .game-controls {
854
+ display: flex;
855
+ justify-content: center;
856
+ gap: 15px;
857
+ margin: 20px auto;
858
+ width: calc(8 * 97.2px + 7 * 2px + 4px); /* Match grid width: 8 cells + 7 gaps + padding */
859
+ max-width: 100%;
860
+ position: relative;
861
+ z-index: 100;
862
+ visibility: visible;
863
+ opacity: 1;
864
+ pointer-events: auto;
865
+ }
866
+
867
+ .game-controls .btn {
868
+ background: white;
869
+ color: #333;
870
+ border: 2px solid #D4AF37;
871
+ border-radius: 8px;
872
+ font-weight: 500;
873
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
874
+ transition: background-color 0.2s ease,
875
+ box-shadow 0.2s ease,
876
+ transform 0.15s ease;
877
+ user-select: none;
878
+ -webkit-user-select: none;
879
+ pointer-events: auto;
880
+ position: relative;
881
+ outline: none;
882
+ visibility: visible !important;
883
+ opacity: 1 !important;
884
+ display: inline-flex !important;
885
+ z-index: 101;
886
+ }
887
+
888
+ .game-controls .btn:hover {
889
+ background: #f9f9f9;
890
+ box-shadow: 0 3px 6px rgba(212, 175, 55, 0.4);
891
+ transform: translateY(-1px);
892
+ }
893
+
894
+ .game-controls .btn:active {
895
+ background: #f0f0f0;
896
+ box-shadow: 0 1px 2px rgba(212, 175, 55, 0.3);
897
+ transform: translateY(0);
898
+ }
899
+
900
+ .game-controls .btn:focus {
901
+ outline: 2px solid rgba(212, 175, 55, 0.5);
902
+ outline-offset: 2px;
903
+ }
904
+
905
+ .btn {
906
+ padding: 12px 30px;
907
+ font-size: 1em;
908
+ border: none;
909
+ border-radius: 25px;
910
+ cursor: pointer;
911
+ font-weight: 600;
912
+ transition: background-color 0.2s ease,
913
+ box-shadow 0.2s ease,
914
+ transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
915
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
916
+ user-select: none;
917
+ -webkit-user-select: none;
918
+ -webkit-tap-highlight-color: transparent;
919
+ pointer-events: auto;
920
+ position: relative;
921
+ outline: none;
922
+ touch-action: manipulation;
923
+ }
924
+
925
+ .btn:hover {
926
+ transform: translateY(-2px);
927
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
928
+ }
929
+
930
+ .btn:active {
931
+ transform: translateY(0);
932
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
933
+ transition: transform 0.1s ease, box-shadow 0.1s ease;
934
+ }
935
+
936
+ .btn:focus {
937
+ outline: 2px solid rgba(102, 126, 234, 0.5);
938
+ outline-offset: 2px;
939
+ }
940
+
941
+ .btn:disabled {
942
+ opacity: 0.6;
943
+ cursor: not-allowed;
944
+ pointer-events: none;
945
+ transform: none !important;
946
+ }
947
+
948
+ .btn-primary {
949
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
950
+ color: white;
951
+ }
952
+
953
+ .btn-secondary {
954
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
955
+ color: white;
956
+ }
957
+
958
+ .game-over {
959
+ position: absolute;
960
+ top: 0;
961
+ left: 0;
962
+ right: 0;
963
+ bottom: 0;
964
+ background: rgba(0, 0, 0, 0.8);
965
+ display: flex;
966
+ justify-content: center;
967
+ align-items: center;
968
+ border-radius: 20px;
969
+ z-index: 1000;
970
+ }
971
+
972
+ .game-over.hidden {
973
+ display: none;
974
+ }
975
+
976
+ .game-over-content {
977
+ background: white;
978
+ padding: 40px;
979
+ border-radius: 20px;
980
+ text-align: center;
981
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
982
+ }
983
+
984
+ .game-over-content h2 {
985
+ color: #333;
986
+ margin-bottom: 20px;
987
+ font-size: 2.5em;
988
+ }
989
+
990
+ .game-over-content p {
991
+ color: #666;
992
+ font-size: 1.2em;
993
+ margin-bottom: 30px;
994
+ }
995
+
996
+ #final-score {
997
+ color: #667eea;
998
+ font-weight: bold;
999
+ font-size: 1.5em;
1000
+ }
1001
+
1002
+ .game-over-buttons {
1003
+ display: flex;
1004
+ flex-direction: column;
1005
+ gap: 15px;
1006
+ margin-top: 30px;
1007
+ }
1008
+
1009
+ .game-over-buttons .btn {
1010
+ width: 100%;
1011
+ padding: 15px;
1012
+ font-size: 1.1em;
1013
+ transition: background-color 0.2s ease,
1014
+ box-shadow 0.2s ease,
1015
+ transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
1016
+ user-select: none;
1017
+ -webkit-user-select: none;
1018
+ }
1019
+
1020
+ .level-complete {
1021
+ position: absolute;
1022
+ top: 0;
1023
+ left: 0;
1024
+ right: 0;
1025
+ bottom: 0;
1026
+ background: rgba(0, 0, 0, 0.85);
1027
+ display: flex;
1028
+ justify-content: center;
1029
+ align-items: center;
1030
+ border-radius: 20px;
1031
+ z-index: 1000;
1032
+ animation: fadeIn 0.3s ease-in;
1033
+ }
1034
+
1035
+ .level-complete.hidden {
1036
+ display: none;
1037
+ }
1038
+
1039
+ @keyframes fadeIn {
1040
+ from {
1041
+ opacity: 0;
1042
+ }
1043
+ to {
1044
+ opacity: 1;
1045
+ }
1046
+ }
1047
+
1048
+ .level-complete-content {
1049
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1050
+ padding: 50px;
1051
+ border-radius: 20px;
1052
+ text-align: center;
1053
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
1054
+ color: white;
1055
+ max-width: 400px;
1056
+ width: 90%;
1057
+ animation: slideUp 0.4s ease-out;
1058
+ }
1059
+
1060
+ @keyframes slideUp {
1061
+ from {
1062
+ transform: translateY(50px);
1063
+ opacity: 0;
1064
+ }
1065
+ to {
1066
+ transform: translateY(0);
1067
+ opacity: 1;
1068
+ }
1069
+ }
1070
+
1071
+ .level-complete-content h2 {
1072
+ color: white;
1073
+ margin-bottom: 20px;
1074
+ font-size: 2.5em;
1075
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
1076
+ }
1077
+
1078
+ .level-complete-content p {
1079
+ color: rgba(255, 255, 255, 0.95);
1080
+ font-size: 1.3em;
1081
+ margin-bottom: 15px;
1082
+ font-weight: 500;
1083
+ }
1084
+
1085
+ .level-complete-content span {
1086
+ font-weight: bold;
1087
+ font-size: 1.2em;
1088
+ color: #ffe66d;
1089
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
1090
+ }
1091
+
1092
+ .level-complete-buttons {
1093
+ display: flex;
1094
+ flex-direction: column;
1095
+ gap: 15px;
1096
+ margin-top: 30px;
1097
+ }
1098
+
1099
+ .level-complete-buttons .btn {
1100
+ width: 100%;
1101
+ padding: 15px;
1102
+ font-size: 1.1em;
1103
+ transition: background-color 0.2s ease,
1104
+ box-shadow 0.2s ease,
1105
+ transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
1106
+ user-select: none;
1107
+ -webkit-user-select: none;
1108
+ }
1109
+
1110
+ .level-complete-buttons .btn-primary {
1111
+ background: white;
1112
+ color: #667eea;
1113
+ }
1114
+
1115
+ .level-complete-buttons .btn-primary:hover {
1116
+ background: #f0f0f0;
1117
+ transform: translateY(-2px);
1118
+ }
1119
+
1120
+ .level-complete-buttons .btn-secondary {
1121
+ background: rgba(255, 255, 255, 0.2);
1122
+ color: white;
1123
+ border: 2px solid white;
1124
+ }
1125
+
1126
+ .level-complete-buttons .btn-secondary:hover {
1127
+ background: rgba(255, 255, 255, 0.3);
1128
+ }
1129
+
1130
+ /* Responsive design */
1131
+ @media (max-width: 600px) {
1132
+ .cell {
1133
+ width: 40px;
1134
+ height: 40px;
1135
+ font-size: 1.5em;
1136
+ }
1137
+
1138
+ .game-header h1 {
1139
+ font-size: 2em;
1140
+ }
1141
+
1142
+ .score-panel {
1143
+ grid-template-columns: repeat(2, 1fr);
1144
+ gap: 15px;
1145
+ }
1146
+
1147
+ .score-item .value {
1148
+ font-size: 1.5em;
1149
+ }
1150
+
1151
+ .score-item .label {
1152
+ font-size: 0.8em;
1153
+ }
1154
+ }
1155
+