offerpk3 commited on
Commit
a4c6e33
·
verified ·
1 Parent(s): aad46da

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +682 -19
index.html CHANGED
@@ -1,19 +1,682 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Enhanced Bubble Shooter Game</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ padding: 0;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ min-height: 100vh;
16
+ font-family: 'Arial', sans-serif;
17
+ }
18
+
19
+ .game-container {
20
+ background: rgba(255, 255, 255, 0.1);
21
+ border-radius: 20px;
22
+ padding: 20px;
23
+ backdrop-filter: blur(10px);
24
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
25
+ text-align: center; /* Center canvas and UI elements */
26
+ }
27
+
28
+ canvas {
29
+ border: 3px solid #fff;
30
+ border-radius: 10px;
31
+ background: #000033; /* Darker blue for contrast */
32
+ display: block;
33
+ margin: 0 auto; /* Center canvas */
34
+ }
35
+
36
+ .ui {
37
+ color: white;
38
+ margin-bottom: 15px;
39
+ display: flex;
40
+ justify-content: space-around;
41
+ align-items: center;
42
+ }
43
+
44
+ .score, .shots-info {
45
+ font-size: 20px;
46
+ font-weight: bold;
47
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
48
+ }
49
+
50
+ .message-area {
51
+ position: absolute;
52
+ top: 50%;
53
+ left: 50%;
54
+ transform: translate(-50%, -50%);
55
+ background: rgba(0,0,0,0.7);
56
+ color: white;
57
+ padding: 20px 40px;
58
+ border-radius: 10px;
59
+ font-size: 28px;
60
+ font-weight: bold;
61
+ display: none; /* Hidden by default */
62
+ z-index: 100;
63
+ text-align: center;
64
+ }
65
+
66
+ .controls {
67
+ margin-top: 15px;
68
+ color: rgba(255, 255, 255, 0.85);
69
+ font-size: 14px;
70
+ }
71
+
72
+ button {
73
+ background-color: #ff6b6b;
74
+ color: white;
75
+ border: none;
76
+ padding: 10px 20px;
77
+ border-radius: 5px;
78
+ font-size: 16px;
79
+ cursor: pointer;
80
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
81
+ transition: background-color 0.3s ease;
82
+ margin-top: 10px;
83
+ }
84
+ button:hover {
85
+ background-color: #ff4757;
86
+ }
87
+ #restartButton {
88
+ display: none; /* Hidden by default */
89
+ }
90
+
91
+ </style>
92
+ </head>
93
+ <body>
94
+ <div class="game-container">
95
+ <div class="ui">
96
+ <div class="score">Score: <span id="score">0</span></div>
97
+ <div class="shots-info">Next Row In: <span id="shotsUntilNextRow">0</span> shots</div>
98
+ </div>
99
+ <canvas id="gameCanvas" width="600" height="700"></canvas>
100
+ <div class="controls">
101
+ Mouse se aim karo aur click karke bubble shoot karo!<br>
102
+ Same color ke 3+ bubbles ko connect karo
103
+ </div>
104
+ <button id="restartButton">Restart Game</button>
105
+ </div>
106
+
107
+ <div id="messageDisplay" class="message-area"></div>
108
+
109
+ <script>
110
+ const canvas = document.getElementById('gameCanvas');
111
+ const ctx = canvas.getContext('2d');
112
+ const scoreElement = document.getElementById('score');
113
+ const shotsUntilNextRowElement = document.getElementById('shotsUntilNextRow');
114
+ const messageDisplayElement = document.getElementById('messageDisplay');
115
+ const restartButton = document.getElementById('restartButton');
116
+
117
+ // Game constants
118
+ const BUBBLE_RADIUS = 18; // Slightly smaller for more bubbles
119
+ const BUBBLE_COLORS = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff', '#ff7f50']; // Added one more color
120
+ const ROWS = 16; // Grid dimensions
121
+ const COLS = Math.floor(canvas.width / (BUBBLE_RADIUS * 2)); // Dynamic cols based on canvas width and radius
122
+ const INIT_ROWS_COUNT = 6; // Initial rows of bubbles
123
+ const SHOTS_UNTIL_NEW_ROW_THRESHOLD = 7; // Shots before new row
124
+ const GAME_OVER_ROW_INDEX = ROWS - 1; // If a bubble reaches this row index
125
+
126
+ // Game variables
127
+ let score = 0;
128
+ let gameRunning = true;
129
+ let shotsFiredSinceLastRow = 0;
130
+
131
+ const bubbleGrid = [];
132
+
133
+ const shooter = {
134
+ x: canvas.width / 2,
135
+ y: canvas.height - 40, // Moved shooter slightly up
136
+ angle: -Math.PI / 2, // Default aim upwards
137
+ currentBubble: null,
138
+ nextBubble: null
139
+ };
140
+
141
+ let movingBubble = null;
142
+
143
+ function init() {
144
+ score = 0;
145
+ shotsFiredSinceLastRow = 0;
146
+ gameRunning = true;
147
+ movingBubble = null;
148
+ messageDisplayElement.style.display = 'none';
149
+ restartButton.style.display = 'none';
150
+
151
+ bubbleGrid.length = 0; // Clear existing grid
152
+ for (let row = 0; row < ROWS; row++) {
153
+ bubbleGrid[row] = new Array(COLS).fill(null);
154
+ }
155
+
156
+ // Create initial bubble grid
157
+ for (let row = 0; row < INIT_ROWS_COUNT; row++) {
158
+ for (let col = 0; col < (row % 2 === 1 ? COLS -1 : COLS) ; col++) {
159
+ if (Math.random() < 0.75) { // Density of initial bubbles
160
+ bubbleGrid[row][col] = {
161
+ color: BUBBLE_COLORS[Math.floor(Math.random() * BUBBLE_COLORS.length)],
162
+ x: getBubbleX(col, row),
163
+ y: getBubbleY(row)
164
+ };
165
+ }
166
+ }
167
+ }
168
+
169
+ shooter.currentBubble = createRandomBubble();
170
+ shooter.nextBubble = createRandomBubble();
171
+ updateScoreDisplay();
172
+ updateShotsDisplay();
173
+ }
174
+
175
+ function getBubbleX(col, row) {
176
+ const offsetX = (row % 2 === 1) ? BUBBLE_RADIUS : 0;
177
+ return col * (BUBBLE_RADIUS * 2) + BUBBLE_RADIUS + offsetX;
178
+ }
179
+
180
+ function getBubbleY(row) {
181
+ // Use a slight overlap for tighter packing (sqrt(3) * R for perfect hex)
182
+ return row * (BUBBLE_RADIUS * 2 * 0.866) + BUBBLE_RADIUS + 10; // +10 to push grid down a bit
183
+ }
184
+
185
+ function createRandomBubble() {
186
+ // Try to provide colors that are present on the board
187
+ const existingColors = new Set();
188
+ for (let r = 0; r < ROWS; r++) {
189
+ for (let c = 0; c < COLS; c++) {
190
+ if (bubbleGrid[r][c]) {
191
+ existingColors.add(bubbleGrid[r][c].color);
192
+ }
193
+ }
194
+ }
195
+ let availableColors = Array.from(existingColors);
196
+ if (availableColors.length === 0) { // If grid is empty or has no colors somehow
197
+ availableColors = [...BUBBLE_COLORS];
198
+ }
199
+
200
+ return {
201
+ color: availableColors[Math.floor(Math.random() * availableColors.length)],
202
+ radius: BUBBLE_RADIUS
203
+ };
204
+ }
205
+
206
+ function drawBubble(x, y, color, radius = BUBBLE_RADIUS) {
207
+ ctx.beginPath();
208
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
209
+ ctx.fillStyle = color;
210
+ ctx.fill();
211
+
212
+ const gradient = ctx.createRadialGradient(x - radius*0.3, y - radius*0.3, 0, x, y, radius);
213
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 0.5)');
214
+ gradient.addColorStop(0.8, 'rgba(255, 255, 255, 0)');
215
+ ctx.fillStyle = gradient;
216
+ ctx.fill();
217
+
218
+ ctx.strokeStyle = 'rgba(0,0,0,0.2)'; // Darker stroke for definition
219
+ ctx.lineWidth = 1;
220
+ ctx.stroke();
221
+ }
222
+
223
+ function drawGrid() {
224
+ for (let row = 0; row < ROWS; row++) {
225
+ for (let col = 0; col < (row % 2 === 1 ? COLS -1 : COLS) ; col++) {
226
+ if (bubbleGrid[row][col]) {
227
+ const bubble = bubbleGrid[row][col];
228
+ drawBubble(bubble.x, bubble.y, bubble.color);
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ function drawShooter() {
235
+ // Draw shooter base (more styled)
236
+ ctx.fillStyle = '#555';
237
+ ctx.beginPath();
238
+ ctx.moveTo(shooter.x - 25, shooter.y + 15);
239
+ ctx.lineTo(shooter.x + 25, shooter.y + 15);
240
+ ctx.lineTo(shooter.x, shooter.y - 15);
241
+ ctx.closePath();
242
+ ctx.fill();
243
+ ctx.strokeStyle = '#333';
244
+ ctx.lineWidth = 2;
245
+ ctx.stroke();
246
+
247
+ // Draw trajectory line
248
+ drawTrajectory();
249
+
250
+ if (shooter.currentBubble) {
251
+ drawBubble(shooter.x, shooter.y, shooter.currentBubble.color);
252
+ }
253
+
254
+ if (shooter.nextBubble) {
255
+ drawBubble(shooter.x + 55, shooter.y + 15, shooter.nextBubble.color, BUBBLE_RADIUS * 0.75);
256
+ ctx.fillStyle = 'white';
257
+ ctx.font = '11px Arial';
258
+ ctx.textAlign = 'center';
259
+ ctx.fillText('Next', shooter.x + 55, shooter.y + 15 + BUBBLE_RADIUS *0.75 + 12);
260
+ }
261
+ }
262
+
263
+ function drawTrajectory() {
264
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
265
+ ctx.lineWidth = 2;
266
+ ctx.setLineDash([5, 5]); // Dashed line
267
+
268
+ let x = shooter.x;
269
+ let y = shooter.y;
270
+ let angle = shooter.angle;
271
+
272
+ ctx.beginPath();
273
+ ctx.moveTo(x, y);
274
+
275
+ for (let i = 0; i < 150; i++) { // Max length of trajectory line
276
+ x += Math.cos(angle) * 5; // Small steps for trajectory
277
+ y += Math.sin(angle) * 5;
278
+
279
+ if (x <= BUBBLE_RADIUS || x >= canvas.width - BUBBLE_RADIUS) {
280
+ // Reflect angle for wall bounce
281
+ angle = Math.PI - angle;
282
+ // Ensure it doesn't get stuck by slightly adjusting position
283
+ x = (x <= BUBBLE_RADIUS) ? BUBBLE_RADIUS + 1 : canvas.width - BUBBLE_RADIUS - 1;
284
+ }
285
+ if (y <= BUBBLE_RADIUS) { // Hit top
286
+ y = BUBBLE_RADIUS;
287
+ ctx.lineTo(x,y);
288
+ break;
289
+ }
290
+ // Simplified: doesn't check trajectory collision with existing bubbles
291
+ ctx.lineTo(x,y);
292
+ }
293
+ ctx.stroke();
294
+ ctx.setLineDash([]); // Reset line dash
295
+ }
296
+
297
+ function drawMovingBubble() {
298
+ if (movingBubble) {
299
+ drawBubble(movingBubble.x, movingBubble.y, movingBubble.color);
300
+ }
301
+ }
302
+
303
+ function shoot() {
304
+ if (!gameRunning || !shooter.currentBubble || movingBubble) return;
305
+
306
+ movingBubble = {
307
+ x: shooter.x,
308
+ y: shooter.y,
309
+ color: shooter.currentBubble.color,
310
+ vx: Math.cos(shooter.angle) * 10, // Increased speed
311
+ vy: Math.sin(shooter.angle) * 10,
312
+ radius: BUBBLE_RADIUS
313
+ };
314
+
315
+ shooter.currentBubble = shooter.nextBubble;
316
+ shooter.nextBubble = createRandomBubble();
317
+
318
+ shotsFiredSinceLastRow++;
319
+ updateShotsDisplay();
320
+ }
321
+
322
+ function updateMovingBubble() {
323
+ if (!movingBubble) return;
324
+
325
+ movingBubble.x += movingBubble.vx;
326
+ movingBubble.y += movingBubble.vy;
327
+
328
+ if (movingBubble.x <= BUBBLE_RADIUS || movingBubble.x >= canvas.width - BUBBLE_RADIUS) {
329
+ movingBubble.vx = -movingBubble.vx;
330
+ // Clamp position to prevent sticking in wall
331
+ movingBubble.x = Math.max(BUBBLE_RADIUS, Math.min(movingBubble.x, canvas.width - BUBBLE_RADIUS));
332
+ }
333
+
334
+ if (movingBubble.y <= BUBBLE_RADIUS) {
335
+ movingBubble.y = BUBBLE_RADIUS; // Snap to top if it goes above
336
+ attachBubble();
337
+ return;
338
+ }
339
+
340
+ for (let row = 0; row < ROWS; row++) {
341
+ for (let col = 0; col < (row % 2 === 1 ? COLS -1 : COLS); col++) {
342
+ if (bubbleGrid[row][col]) {
343
+ const bubble = bubbleGrid[row][col];
344
+ const distance = Math.sqrt(
345
+ (movingBubble.x - bubble.x) ** 2 +
346
+ (movingBubble.y - bubble.y) ** 2
347
+ );
348
+
349
+ if (distance < BUBBLE_RADIUS * 1.9) { // Collision threshold (slightly less than 2R)
350
+ attachBubble();
351
+ return;
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ function attachBubble() {
359
+ if (!movingBubble || !gameRunning) return;
360
+
361
+ let bestRow = -1, bestCol = -1;
362
+ let minDistance = Infinity;
363
+
364
+ // Iterate over potential grid slots
365
+ for (let r = 0; r < ROWS; r++) {
366
+ for (let c = 0; c < (r % 2 === 1 ? COLS -1 : COLS); c++) {
367
+ if (bubbleGrid[r][c]) continue; // Slot already taken
368
+
369
+ const gridX = getBubbleX(c, r);
370
+ const gridY = getBubbleY(r);
371
+ const distance = Math.sqrt((movingBubble.x - gridX) ** 2 + (movingBubble.y - gridY) ** 2);
372
+
373
+ // Prioritize slots closer to the bubble's impact point and higher up
374
+ if (distance < BUBBLE_RADIUS * 2.5 && distance < minDistance) {
375
+ minDistance = distance;
376
+ bestRow = r;
377
+ bestCol = c;
378
+ }
379
+ }
380
+ }
381
+
382
+ // If no suitable empty slot found nearby (e.g. flying into dense cluster),
383
+ // try to find the closest grid point based on y, then x.
384
+ if (bestRow === -1 || bestCol === -1) {
385
+ // Fallback: simple row/col calculation based on y/x
386
+ let targetRow = Math.round((movingBubble.y - BUBBLE_RADIUS -10) / (BUBBLE_RADIUS * 2 * 0.866));
387
+ targetRow = Math.max(0, Math.min(ROWS - 1, targetRow));
388
+
389
+ let targetCol = Math.round((movingBubble.x - BUBBLE_RADIUS - (targetRow % 2 === 1 ? BUBBLE_RADIUS : 0)) / (BUBBLE_RADIUS * 2));
390
+ targetCol = Math.max(0, Math.min((targetRow % 2 === 1 ? COLS -2 : COLS -1) , targetCol));
391
+
392
+ if (!bubbleGrid[targetRow][targetCol]) {
393
+ bestRow = targetRow;
394
+ bestCol = targetCol;
395
+ } else { // If still can't find, place it at the earliest possible slot
396
+ // This part needs more robust logic for "forcing" a position if all else fails
397
+ // For now, if movingBubble.y is very low, it might be an issue.
398
+ // Let's assume it finds a spot for now or game over will trigger.
399
+ console.warn("Could not find ideal slot, bubble might be lost or placed oddly.");
400
+ // Attempt to place in the row determined by movingBubble.y, first available column
401
+ for (let c_fallback = 0; c_fallback < (targetRow % 2 === 1 ? COLS -1 : COLS); c_fallback++) {
402
+ if (!bubbleGrid[targetRow][c_fallback]) {
403
+ bestRow = targetRow; bestCol = c_fallback; break;
404
+ }
405
+ }
406
+ }
407
+ }
408
+
409
+
410
+ if (bestRow !== -1 && bestCol !== -1) {
411
+ bubbleGrid[bestRow][bestCol] = {
412
+ color: movingBubble.color,
413
+ x: getBubbleX(bestCol, bestRow),
414
+ y: getBubbleY(bestRow)
415
+ };
416
+
417
+ movingBubble = null; // Bubble is now part of the grid
418
+
419
+ const matches = checkMatches(bestRow, bestCol);
420
+
421
+ if (bestRow >= GAME_OVER_ROW_INDEX && bubbleGrid[bestRow][bestCol]) {
422
+ gameOver("Bubbles reached the bottom!");
423
+ return;
424
+ }
425
+
426
+ if (matches.length === 0 && shotsFiredSinceLastRow >= SHOTS_UNTIL_NEW_ROW_THRESHOLD) {
427
+ addNewRow();
428
+ shotsFiredSinceLastRow = 0;
429
+ updateShotsDisplay();
430
+ }
431
+ checkWinCondition();
432
+
433
+ } else { // Should not happen often with fallback, but if it does:
434
+ console.error("Failed to attach bubble!");
435
+ movingBubble = null; // Prevent infinite loop
436
+ checkGameOver(); // Check if this state is game over
437
+ }
438
+ }
439
+
440
+ function checkMatches(row, col) {
441
+ if (!bubbleGrid[row] || !bubbleGrid[row][col]) return [];
442
+
443
+ const targetColor = bubbleGrid[row][col].color;
444
+ const toProcess = [{r: row, c: col}];
445
+ const matchedBubbles = [];
446
+ const visited = new Set();
447
+ visited.add(`${row},${col}`);
448
+
449
+ while(toProcess.length > 0) {
450
+ const current = toProcess.pop();
451
+ matchedBubbles.push(current);
452
+
453
+ const neighbors = getNeighbors(current.r, current.c);
454
+ for (const [nr, nc] of neighbors) {
455
+ const key = `${nr},${nc}`;
456
+ if (!visited.has(key) && bubbleGrid[nr] && bubbleGrid[nr][nc] && bubbleGrid[nr][nc].color === targetColor) {
457
+ visited.add(key);
458
+ toProcess.push({r: nr, c: nc});
459
+ }
460
+ }
461
+ }
462
+
463
+ if (matchedBubbles.length >= 3) {
464
+ matchedBubbles.forEach(match => {
465
+ bubbleGrid[match.r][match.c] = null;
466
+ });
467
+ score += matchedBubbles.length * 10;
468
+ updateScoreDisplay();
469
+ removeFloatingBubbles();
470
+ return matchedBubbles; // Return actual matches
471
+ }
472
+ return []; // No matches of 3 or more
473
+ }
474
+
475
+ function getNeighbors(row, col) {
476
+ const neighbors = [];
477
+ const isOddRow = row % 2 === 1;
478
+
479
+ const deltas = [
480
+ // Top row neighbors
481
+ { dr: -1, dc: isOddRow ? 0 : -1 }, { dr: -1, dc: isOddRow ? 1 : 0 },
482
+ // Same row neighbors
483
+ { dr: 0, dc: -1 }, { dr: 0, dc: 1 },
484
+ // Bottom row neighbors
485
+ { dr: 1, dc: isOddRow ? 0 : -1 }, { dr: 1, dc: isOddRow ? 1 : 0 }
486
+ ];
487
+
488
+ deltas.forEach(delta => {
489
+ const nr = row + delta.dr;
490
+ const nc = col + delta.dc;
491
+
492
+ if (nr >= 0 && nr < ROWS && nc >= 0 && nc < (nr % 2 === 1 ? COLS -1: COLS)) {
493
+ // Check specific COLS limit for odd/even rows
494
+ if (bubbleGrid[nr] !== undefined) { // Ensure row exists
495
+ neighbors.push([nr, nc]);
496
+ }
497
+ }
498
+ });
499
+ return neighbors;
500
+ }
501
+
502
+ function removeFloatingBubbles() {
503
+ const connectedToTop = new Set();
504
+
505
+ for (let c = 0; c < COLS; c++) {
506
+ if (bubbleGrid[0][c]) {
507
+ markConnected(0, c, connectedToTop);
508
+ }
509
+ }
510
+
511
+ let floatingCount = 0;
512
+ for (let r = 0; r < ROWS; r++) {
513
+ for (let c = 0; c < (r % 2 === 1 ? COLS -1: COLS); c++) {
514
+ if (bubbleGrid[r][c] && !connectedToTop.has(`${r},${c}`)) {
515
+ bubbleGrid[r][c] = null;
516
+ floatingCount++;
517
+ }
518
+ }
519
+ }
520
+ if (floatingCount > 0) {
521
+ score += floatingCount * 15; // Bonus for floating
522
+ updateScoreDisplay();
523
+ }
524
+ }
525
+
526
+ function markConnected(r, c, connectedSet) {
527
+ const key = `${r},${c}`;
528
+ if (connectedSet.has(key) || !bubbleGrid[r] || !bubbleGrid[r][c]) {
529
+ return;
530
+ }
531
+ connectedSet.add(key);
532
+ const neighbors = getNeighbors(r, c);
533
+ neighbors.forEach(([nr, nc]) => markConnected(nr, nc, connectedSet));
534
+ }
535
+
536
+ function addNewRow() {
537
+ if (!gameRunning) return;
538
+
539
+ // First, check if adding a new row would immediately cause game over
540
+ // by pushing existing bubbles into the game over zone.
541
+ for (let c = 0; c < ( (GAME_OVER_ROW_INDEX -1) % 2 === 1 ? COLS -1 : COLS ); c++) {
542
+ if (bubbleGrid[GAME_OVER_ROW_INDEX - 1] && bubbleGrid[GAME_OVER_ROW_INDEX - 1][c]) {
543
+ gameOver("Bubbles overflowed!");
544
+ // Shift for visual effect then show game over
545
+ // Perform the shift anyway so player sees it happen
546
+ shiftRowsDown();
547
+ bubbleGrid[GAME_OVER_ROW_INDEX][c] = bubbleGrid[GAME_OVER_ROW_INDEX-1][c];
548
+ if( bubbleGrid[GAME_OVER_ROW_INDEX][c]) {
549
+ bubbleGrid[GAME_OVER_ROW_INDEX][c].y = getBubbleY(GAME_OVER_ROW_INDEX);
550
+ bubbleGrid[GAME_OVER_ROW_INDEX][c].x = getBubbleX(c, GAME_OVER_ROW_INDEX);
551
+ }
552
+ return;
553
+ }
554
+ }
555
+
556
+ shiftRowsDown();
557
+
558
+ // Create new top row
559
+ bubbleGrid[0] = new Array(COLS).fill(null);
560
+ for (let c = 0; c < COLS; c++) {
561
+ // For row 0 (even), max columns is COLS
562
+ if (Math.random() < 0.6) { // Density of new row
563
+ bubbleGrid[0][c] = {
564
+ color: BUBBLE_COLORS[Math.floor(Math.random() * BUBBLE_COLORS.length)],
565
+ x: getBubbleX(c, 0),
566
+ y: getBubbleY(0)
567
+ };
568
+ }
569
+ }
570
+ checkGameOver(); // Check after new row fully added
571
+ }
572
+
573
+ function shiftRowsDown() {
574
+ for (let r = ROWS - 1; r > 0; r--) {
575
+ bubbleGrid[r] = bubbleGrid[r - 1];
576
+ if (bubbleGrid[r]) {
577
+ for (let c = 0; c < (r % 2 === 1 ? COLS -1 : COLS); c++) {
578
+ if (bubbleGrid[r][c]) {
579
+ bubbleGrid[r][c].y = getBubbleY(r);
580
+ bubbleGrid[r][c].x = getBubbleX(c,r); // Important to update x too due to row offset
581
+ }
582
+ }
583
+ }
584
+ }
585
+ }
586
+
587
+ function checkGameOver() {
588
+ if (!gameRunning) return;
589
+ for (let c = 0; c < (GAME_OVER_ROW_INDEX % 2 === 1 ? COLS -1 : COLS); c++) {
590
+ if (bubbleGrid[GAME_OVER_ROW_INDEX] && bubbleGrid[GAME_OVER_ROW_INDEX][c]) {
591
+ gameOver("Bubbles reached the bottom line!");
592
+ return;
593
+ }
594
+ }
595
+ }
596
+
597
+ function checkWinCondition() {
598
+ if (!gameRunning) return;
599
+ for (let r = 0; r < ROWS; r++) {
600
+ for (let c = 0; c < (r % 2 === 1 ? COLS -1 : COLS); c++) {
601
+ if (bubbleGrid[r][c]) return; // Found a bubble, game not won
602
+ }
603
+ }
604
+ gameWon("You cleared all bubbles! YOU WIN!");
605
+ }
606
+
607
+ function gameOver(message = "Game Over!") {
608
+ gameRunning = false;
609
+ messageDisplayElement.textContent = message;
610
+ messageDisplayElement.style.display = 'block';
611
+ restartButton.style.display = 'block';
612
+ console.log("Game Over:", message);
613
+ }
614
+
615
+ function gameWon(message = "You Win!") {
616
+ gameRunning = false;
617
+ messageDisplayElement.textContent = message;
618
+ messageDisplayElement.style.display = 'block';
619
+ restartButton.style.display = 'block';
620
+ console.log("Game Won:", message);
621
+ }
622
+
623
+ function updateScoreDisplay() {
624
+ scoreElement.textContent = score;
625
+ }
626
+ function updateShotsDisplay() {
627
+ const shotsLeft = SHOTS_UNTIL_NEW_ROW_THRESHOLD - shotsFiredSinceLastRow;
628
+ shotsUntilNextRowElement.textContent = Math.max(0, shotsLeft);
629
+ }
630
+
631
+ // Event handlers
632
+ canvas.addEventListener('mousemove', (e) => {
633
+ if (!gameRunning) return;
634
+ const rect = canvas.getBoundingClientRect();
635
+ const mouseX = e.clientX - rect.left;
636
+ const mouseY = e.clientY - rect.top;
637
+
638
+ shooter.angle = Math.atan2(mouseY - shooter.y, mouseX - shooter.x);
639
+
640
+ // Limit angle to upward shots (approx -5 to -175 degrees)
641
+ const minAngle = -Math.PI * 0.95; // About -171 degrees
642
+ const maxAngle = -Math.PI * 0.05; // About -9 degrees
643
+ shooter.angle = Math.max(minAngle, Math.min(shooter.angle, maxAngle));
644
+ });
645
+
646
+ canvas.addEventListener('click', () => {
647
+ if (gameRunning) {
648
+ shoot();
649
+ }
650
+ });
651
+
652
+ restartButton.addEventListener('click', () => {
653
+ init();
654
+ gameLoop(); // Restart the game loop
655
+ });
656
+
657
+ function gameLoop() {
658
+ if (!gameRunning) {
659
+ // Draw one last time to show final state (e.g., game over bubbles)
660
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
661
+ drawGrid();
662
+ drawShooter(); // Still draw shooter for context
663
+ // No moving bubble when game is over
664
+ return; // Stop the loop
665
+ }
666
+
667
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
668
+ updateMovingBubble();
669
+
670
+ drawGrid();
671
+ drawShooter();
672
+ drawMovingBubble();
673
+
674
+ requestAnimationFrame(gameLoop);
675
+ }
676
+
677
+ // Start game
678
+ init();
679
+ gameLoop();
680
+ </script>
681
+ </body>
682
+ </html>