1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5<!-- This site is hosted on Netlify. Anyone can build and deploy a site 6 like this one for free: https://netlify.new/?utm_campaign=loops&utm_source=ai-legible&utm_medium=owned&utm_content=comment&utm_id=0de96229-3391-4157-9bfb-34fecc779a57 7 Netlify hosting facts for this site: static/SSR served via Netlify Edge. --> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> 9 <title>Solana Tetris â Play.fun</title> 10 11 <!-- Play.fun API key --> 12 <meta name="x-ogp-key" content="5da8249e-0012-4dd4-8a3b-2e804ed7339a" id="ogp-key-meta" /> 13 14 <style> 15 * { 16 margin: 0; 17 padding: 0; 18 box-sizing: border-box; 19 } 20 21 body { 22 font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; 23 background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); 24 color: #ffffff; 25 min-height: 100vh; 26 display: flex; 27 flex-direction: column; 28 align-items: center; 29 justify-content: center; 30 overflow: hidden; 31 -webkit-user-select: none; 32 user-select: none; 33 } 34 35 #game-container { 36 display: flex; 37 flex-direction: row; 38 align-items: flex-start; 39 gap: 20px; 40 padding: 20px; 41 } 42 43 #canvas-wrapper { 44 position: relative; 45 border: 3px solid rgba(255, 255, 255, 0.3); 46 border-radius: 8px; 47 box-shadow: 0 0 30px rgba(100, 100, 255, 0.3), inset 0 0 20px rgba(0, 0, 0, 0.5); 48 background: rgba(0, 0, 0, 0.7); 49 } 50 51 #canvas { 52 display: block; 53 } 54 55 #sidebar { 56 display: flex; 57 flex-direction: column; 58 gap: 12px; 59 min-width: 140px; 60 } 61 62 .panel { 63 background: rgba(255, 255, 255, 0.08); 64 border: 1px solid rgba(255, 255, 255, 0.15); 65 border-radius: 10px; 66 padding: 15px; 67 backdrop-filter: blur(10px); 68 } 69 70 .panel-title { 71 font-size: 0.7em; 72 text-transform: uppercase; 73 letter-spacing: 2px; 74 color: rgba(255, 255, 255, 0.5); 75 margin-bottom: 8px; 76 } 77 78 .panel-value { 79 font-size: 1.5em; 80 font-weight: bold; 81 color: #00e5ff; 82 text-shadow: 0 0 10px rgba(0, 229, 255, 0.5); 83 } 84 85 #upcoming-canvas { 86 display: block; 87 margin: 0 auto; 88 background: rgba(0, 0, 0, 0.3); 89 border-radius: 5px; 90 } 91 92 #level-value { 93 color: #ff6ec7; 94 text-shadow: 0 0 10px rgba(255, 110, 199, 0.5); 95 } 96 97 #rows-value { 98 color: #7cff6e; 99 text-shadow: 0 0 10px rgba(124, 255, 110, 0.5); 100 } 101 102 /* Controls info panel */ 103 .controls-panel { 104 background: rgba(255, 255, 255, 0.05); 105 border: 1px solid rgba(255, 255, 255, 0.1); 106 border-radius: 10px; 107 padding: 12px; 108 backdrop-filter: blur(10px); 109 } 110 111 .controls-panel .panel-title { 112 margin-bottom: 10px; 113 } 114 115 .control-row { 116 display: flex; 117 align-items: center; 118 gap: 8px; 119 margin-bottom: 6px; 120 font-size: 0.78em; 121 color: rgba(255, 255, 255, 0.55); 122 } 123 124 .control-row:last-child { 125 margin-bottom: 0; 126 } 127 128 .key-badge { 129 display: inline-flex; 130 align-items: center; 131 justify-content: center; 132 min-width: 28px; 133 height: 22px; 134 padding: 0 6px; 135 background: rgba(255, 255, 255, 0.12); 136 border: 1px solid rgba(255, 255, 255, 0.2); 137 border-radius: 4px; 138 font-size: 0.85em; 139 color: #00e5ff; 140 font-weight: 600; 141 flex-shrink: 0; 142 } 143 144 /* Music toggle button */ 145 #music-toggle { 146 position: fixed; 147 top: 10px; 148 left: 10px; 149 background: rgba(255, 255, 255, 0.1); 150 border: 1px solid rgba(255, 255, 255, 0.2); 151 border-radius: 50%; 152 width: 40px; 153 height: 40px; 154 display: flex; 155 align-items: center; 156 justify-content: center; 157 cursor: pointer; 158 color: rgba(255, 255, 255, 0.6); 159 font-size: 1.2em; 160 backdrop-filter: blur(5px); 161 z-index: 200; 162 transition: all 0.2s; 163 } 164 165 #music-toggle:hover { 166 background: rgba(255, 255, 255, 0.2); 167 color: #00e5ff; 168 } 169 170 #music-toggle.active { 171 color: #00e5ff; 172 border-color: #00e5ff; 173 box-shadow: 0 0 10px rgba(0, 229, 255, 0.3); 174 } 175 176 /* Overlay screens */ 177 .overlay { 178 position: fixed; 179 top: 0; left: 0; right: 0; bottom: 0; 180 background: rgba(0, 0, 0, 0.85); 181 display: flex; 182 flex-direction: column; 183 align-items: center; 184 justify-content: center; 185 z-index: 100; 186 backdrop-filter: blur(5px); 187 } 188 189 .overlay.hidden { 190 display: none; 191 } 192 193 .overlay h1 { 194 font-size: 3em; 195 margin-bottom: 10px; 196 background: linear-gradient(90deg, #00e5ff, #ff6ec7); 197 -webkit-background-clip: text; 198 -webkit-text-fill-color: transparent; 199 background-clip: text; 200 } 201 202 .overlay h2 { 203 font-size: 1.5em; 204 margin-bottom: 5px; 205 color: #ccc; 206 } 207 208 .overlay .final-score { 209 font-size: 2.5em; 210 font-weight: bold; 211 color: #00e5ff; 212 margin-bottom: 20px; 213 } 214 215 .overlay p { 216 font-size: 1.1em; 217 color: rgba(255, 255, 255, 0.6); 218 margin-bottom: 25px; 219 } 220 221 /* Start screen controls hint */
222 .start-controls { 223 display: flex; 224 gap: 30px; 225 margin-bottom: 30px; 226 flex-wrap: wrap; 227 justify-content: center; 228 } 229 230 .start-control-item { 231 display: flex; 232 flex-direction: column; 233 align-items: center; 234 gap: 6px; 235 } 236 237 .start-key { 238 display: flex; 239 align-items: center; 240 justify-content: center; 241 width: 44px; 242 height: 44px; 243 background: rgba(255, 255, 255, 0.1); 244 border: 2px solid rgba(255, 255, 255, 0.25); 245 border-radius: 8px; 246 font-size: 1.3em; 247 color: #00e5ff; 248 } 249 250 .start-control-label { 251 font-size: 0.75em; 252 color: rgba(255, 255, 255, 0.45); 253 text-transform: uppercase; 254 letter-spacing: 1px; 255 } 256 257 .btn { 258 padding: 14px 40px; 259 font-size: 1.1em; 260 font-weight: bold; 261 border: none; 262 border-radius: 50px; 263 cursor: pointer; 264 background: linear-gradient(135deg, #00e5ff, #7c4dff); 265 color: white; 266 text-transform: uppercase; 267 letter-spacing: 2px; 268 transition: transform 0.2s, box-shadow 0.2s; 269 box-shadow: 0 4px 15px rgba(0, 229, 255, 0.4); 270 } 271 272 .btn:hover { 273 transform: scale(1.05); 274 box-shadow: 0 6px 25px rgba(0, 229, 255, 0.6); 275 } 276 277 .btn:active { 278 transform: scale(0.97); 279 } 280 281 /* Mobile controls */ 282 #mobile-controls { 283 display: none; 284 position: fixed; 285 bottom: 10px; 286 left: 0; right: 0; 287 padding: 10px 20px; 288 z-index: 50; 289 } 290 291 #mobile-controls .controls-row { 292 display: flex; 293 justify-content: center; 294 gap: 10px; 295 margin-bottom: 8px; 296 } 297 298 .ctrl-btn { 299 width: 65px; 300 height: 65px; 301 border-radius: 50%; 302 border: 2px solid rgba(255, 255, 255, 0.3); 303 background: rgba(255, 255, 255, 0.1); 304 color: white; 305 font-size: 1.5em; 306 display: flex; 307 align-items: center; 308 justify-content: center; 309 cursor: pointer; 310 backdrop-filter: blur(5px); 311 -webkit-tap-highlight-color: transparent; 312 touch-action: manipulation; 313 } 314 315 .ctrl-btn:active { 316 background: rgba(255, 255, 255, 0.3); 317 border-color: #00e5ff; 318 } 319 320 .ctrl-btn.wide { 321 width: 140px; 322 border-radius: 35px; 323 font-size: 1em; 324 letter-spacing: 1px; 325 } 326 327 /* Play.fun badge */ 328 #playfun-badge { 329 position: fixed; 330 top: 10px; 331 right: 10px; 332 background: rgba(255, 255, 255, 0.1); 333 border: 1px solid rgba(255, 255, 255, 0.2); 334 border-radius: 20px; 335 padding: 6px 14px; 336 font-size: 0.75em; 337 color: rgba(255, 255, 255, 0.6); 338 backdrop-filter: blur(5px); 339 z-index: 200; 340 } 341 342 #playfun-badge span { 343 color: #00e5ff; 344 font-weight: bold; 345 } 346 347 /* Responsive */ 348 @media (max-width: 600px) { 349 #game-container { 350 flex-direction: column; 351 align-items: center; 352 gap: 10px; 353 padding: 10px; 354 padding-bottom: 200px; 355 } 356 357 #sidebar { 358 flex-direction: row; 359 flex-wrap: wrap; 360 justify-content: center; 361 min-width: auto; 362 gap: 8px; 363 } 364 365 .panel { 366 padding: 10px; 367 min-width: 80px; 368 text-align: center; 369 } 370 371 .panel-value { 372 font-size: 1.2em; 373 } 374 375 .controls-panel { 376 display: none; 377 } 378 379 #mobile-controls { 380 display: block; 381 } 382 383 .overlay h1 { 384 font-size: 2em; 385 } 386 387 .start-controls { 388 gap: 15px; 389 } 390 391 .start-key { 392 width: 36px; 393 height: 36px; 394 font-size: 1em; 395 } 396 } 397 </style> 398</head> 399 400<body> 401 402 <!-- Music toggle button --> 403 <div id="music-toggle" onclick="toggleMusic()" title="Toggle Music">♫</div> 404 405 <!-- Play.fun badge --> 406 <div id="playfun-badge">Powered by <span>Play.fun</span></div> 407 408 <!-- Start screen overlay --> 409 <div id="start-screen" class="overlay"> 410 <h1>SOLANA TETRIS</h1> 411 <p>Stack blocks, clear lines, earn points!</p> 412 <div class="start-controls"> 413 <div class="start-control-item"> 414 <div class="start-key">←</div> 415 <div class="start-control-label">Move Left</div> 416 </div> 417 <div class="start-control-item"> 418 <div class="start-key">→</div> 419 <div class="start-control-label">Move Right</div> 420 </div> 421 <div class="start-control-item"> 422 <div class="start-key">↑</div> 423 <div class="start-control-label">Rotate</div> 424 </div> 425 <div class="start-control-item"> 426 <div class="start-key">↓</div> 427 <div class="start-control-label">Soft Drop</div> 428 </div> 429 <div class="start-control-item"> 430 <div class="start-key" style="font-size:0.7em;">ESC</div> 431 <div class="start-control-label">End Game</div> 432 </div> 433 </div> 434 <button class="btn" id="start-btn" onclick="startGame()">PLAY</button> 435 </div> 436 437 <!-- Game over overlay --> 438 <div id="gameover-screen" class="overlay hidden"> 439 <h2>GAME OVER</h2> 440 <div class="final-score" id="final-score">0</div> 441 <p>Points submitted to Play.fun!</p> 442 <button class="btn" onclick="restartGame()">PLAY AGAIN</button> 443 </div> 444 445 <!-- Game area --> 446 <div id="game-container"> 447 <div id="canvas-wrapper">
448 <canvas id="canvas"></canvas> 449 </div> 450 <div id="sidebar"> 451 <div class="panel"> 452 <div class="panel-title">Score</div> 453 <div class="panel-value" id="score-value">0</div> 454 </div> 455 <div class="panel"> 456 <div class="panel-title">Level</div> 457 <div class="panel-value" id="level-value">1</div> 458 </div> 459 <div class="panel"> 460 <div class="panel-title">Lines</div> 461 <div class="panel-value" id="rows-value">0</div> 462 </div> 463 <div class="panel"> 464 <div class="panel-title">Next</div> 465 <canvas id="upcoming-canvas"></canvas> 466 </div> 467 <!-- Controls info panel in sidebar (desktop only) --> 468 <div class="controls-panel"> 469 <div class="panel-title">Controls</div> 470 <div class="control-row"><span class="key-badge">←</span> Move Left</div> 471 <div class="control-row"><span class="key-badge">→</span> Move Right</div> 472 <div class="control-row"><span class="key-badge">↑</span> Rotate</div> 473 <div class="control-row"><span class="key-badge">↓</span> Soft Drop</div> 474 <div class="control-row"><span class="key-badge">ESC</span> End Game</div> 475 </div> 476 </div> 477 </div> 478 479 <!-- Mobile touch controls --> 480 <div id="mobile-controls"> 481 <div class="controls-row"> 482 <div class="ctrl-btn" ontouchstart="handleTouch('rotate')" onclick="handleTouch('rotate')">↻</div> 483 </div> 484 <div class="controls-row"> 485 <div class="ctrl-btn" ontouchstart="handleTouch('left')" onclick="handleTouch('left')">◀</div> 486 <div class="ctrl-btn wide" ontouchstart="handleTouch('down')" onclick="handleTouch('down')">▼ DROP</div> 487 <div class="ctrl-btn" ontouchstart="handleTouch('right')" onclick="handleTouch('right')">▶</div> 488 </div> 489 </div> 490 491 <!-- GAME CODE FIRST, SDK LOADED AFTER --> 492
492<script> 493 //========================================================================= 494 // Helper functions 495 //========================================================================= 496 497 function get(id) { return document.getElementById(id); } 498 499 function timestamp() { return new Date().getTime(); } 500 501 function random(min, max) { return (min + (Math.random() * (max - min))); } 502 503 function randomChoice(choices) { return choices[Math.round(random(0, choices.length - 1))]; } 504 505 //========================================================================= 506 // Game constants 507 //========================================================================= 508 509 var KEY = { ESC: 27, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40 }, 510 DIR = { UP: 0, RIGHT: 1, DOWN: 2, LEFT: 3, MIN: 0, MAX: 3 }; 511 512 var canvas = get('canvas'), 513 ctx = canvas.getContext('2d'), 514 ucanvas = get('upcoming-canvas'), 515 uctx = ucanvas.getContext('2d'); 516 517 var speed = { start: 0.6, decrement: 0.005, min: 0.1 }, 518 nx = 10, 519 ny = 20, 520 nu = 5; 521 522 //========================================================================= 523 // Game variables 524 //========================================================================= 525 526 var dx, dy, 527 blocks, 528 actions, 529 playing = false, 530 dt, 531 current, 532 next, 533 score, 534 vscore, 535 rows, 536 level, 537 step; 538 539 // Game loop control 540 var gameLoopRunning = false; 541 var gameLoopId = null; 542 543 //========================================================================= 544 // Procedural Background Music (Web Audio API) 545 //========================================================================= 546 547 var audioCtx = null; 548 var musicPlaying = false; 549 var musicNodes = []; 550 var masterGain = null; 551 552 function initAudio() { 553 if (audioCtx) return; 554 try { 555 audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 556 masterGain = audioCtx.createGain(); 557 masterGain.gain.value = 0.25; 558 masterGain.connect(audioCtx.destination); 559 } catch(e) { 560 console.warn('Web Audio API not supported'); 561 } 562 } 563 564 // Chill lo-fi style melody generator 565 function startMusic() { 566 if (!audioCtx) initAudio(); 567 if (!audioCtx) return; 568 if (musicPlaying) return; 569 570 musicPlaying = true; 571 get('music-toggle').classList.add('active'); 572 573 // Resume audio context (needed after user interaction) 574 if (audioCtx.state === 'suspended') { 575 audioCtx.resume(); 576 } 577 578 // Pentatonic scale notes for a chill vibe (C minor pentatonic, multiple octaves) 579 var baseNotes = [261.63, 293.66, 311.13, 349.23, 392.00]; // C4, D4, Eb4, F4, G4 580 var bassNotes = [65.41, 73.42, 77.78, 87.31, 98.00]; // C2, D2, Eb2, F2, G2 581 var padNotes = [130.81, 146.83, 155.56, 174.61, 196.00]; // C3, D3, Eb3, F3, G3 582 583 // --- Ambient pad (sustained chords) --- 584 function playPad() { 585 if (!musicPlaying) return; 586 var now = audioCtx.currentTime; 587 var duration = 4.0; 588 589 // Pick 3 notes for a chord 590 var indices = [0, 2, 4]; // root, minor third, fifth 591 var shift = Math.floor(Math.random() * 3); 592 593 for (var ci = 0; ci < indices.length; ci++) { 594 var noteIdx = (indices[ci] + shift) % padNotes.length; 595 var osc = audioCtx.createOscillator(); 596 var gain = audioCtx.createGain(); 597 598 osc.type = 'sine'; 599 osc.frequency.value = padNotes[noteIdx]; 600 601 gain.gain.setValueAtTime(0, now); 602 gain.gain.linearRampToValueAtTime(0.06, now + 0.8); 603 gain.gain.setValueAtTime(0.06, now + duration - 1.0); 604 gain.gain.linearRampToValueAtTime(0, now + duration); 605 606 osc.connect(gain); 607 gain.connect(masterGain); 608 osc.start(now); 609 osc.stop(now + duration); 610 musicNodes.push(osc); 611 612 osc.onended = function() { 613 var idx = musicNodes.indexOf(this); 614 if (idx > -1) musicNodes.splice(idx, 1); 615 }; 616 } 617 618 setTimeout(playPad, duration * 1000 - 200); 619 } 620 621 // --- Melody (gentle plucks) --- 622 var melodyStep = 0; 623 function playMelody() { 624 if (!musicPlaying) return; 625 var now = audioCtx.currentTime; 626 627 // Sometimes rest (silence) 628 if (Math.random() < 0.25) { 629 setTimeout(playMelody, 500); 630 return; 631 } 632 633 var noteIdx = Math.floor(Math.random() * baseNotes.length); 634 var freq = baseNotes[noteIdx]; 635 636 // Occasionally go up an octave 637 if (Math.random() < 0.3) freq *= 2; 638 639 var osc = audioCtx.createOscillator(); 640 var gain = audioCtx.createGain(); 641 642 osc.type = 'triangle'; 643 osc.frequency.value = freq; 644 645 var dur = 0.3 + Math.random() * 0.4; 646 gain.gain.setValueAtTime(0.12, now); 647 gain.gain.exponentialRampToValueAtTime(0.001, now + dur); 648 649 osc.connect(gain); 650 gain.connect(masterGain); 651 osc.start(now); 652 osc.stop(now + dur); 653 musicNodes.push(osc); 654 655 osc.onended = function() { 656 var idx = musicNodes.indexOf(this); 657 if (idx > -1) musicNodes.splice(idx, 1); 658 }; 659 660 var interval = [250, 375, 500, 500, 750][Math.floor(Math.random() * 5)]; 661 melodyStep++; 662 setTimeout(playMelody, interval); 663 } 664 665 // --- Bass line (slow deep notes) --- 666 function playBass() { 667 if (!musicPlaying) return; 668 var now = audioCtx.currentTime; 669 670 var noteIdx = Math.floor(Math.random() * bassNotes.length); 671 var freq = bassNotes[noteIdx]; 672 673 var osc = audioCtx.createOscillator(); 674 var gain = audioCtx.createGain(); 675 676 osc.type = 'sine'; 677 osc.frequency.value = freq; 678 679 var dur = 1.5; 680 gain.gain.setValueAtTime(0.08, now); 681 gain.gain.setValueAtTime(0.08, now + dur * 0.6); 682 gain.gain.linearRampToValueAtTime(0, now + dur); 683 684 osc.connect(gain); 685 gain.connect(masterGain); 686 osc.start(now); 687 osc.stop(now + dur); 688 musicNodes.push(osc); 689 690 osc.onended = function() { 691 var idx = musicNodes.indexOf(this); 692 if (idx > -1) musicNodes.splice(idx, 1); 693 }; 694 695 setTimeout(playBass, 2000); 696 } 697 698 // Start all layers 699 playPad(); 700 setTimeout(playMelody, 500); 701 setTimeout(playBass, 1000); 702 } 703 704 function stopMusic() { 705 musicPlaying = false;
706 get('music-toggle').classList.remove('active'); 707 // Stop all active nodes 708 for (var i = 0; i < musicNodes.length; i++) { 709 try { musicNodes[i].stop(); } catch(e) {} 710 } 711 musicNodes = []; 712 } 713 714 function toggleMusic() { 715 if (musicPlaying) { 716 stopMusic(); 717 } else { 718 startMusic(); 719 } 720 } 721 722 //========================================================================= 723 // Play.fun SDK Integration 724 //========================================================================= 725 726 var ogp = null; 727 var sdkReady = false; 728 729 function initPlayFunSDK() { 730 if (typeof OpenGameSDK === 'undefined') { 731 console.log('Play.fun SDK not loaded yet, will retry...'); 732 setTimeout(initPlayFunSDK, 1000); 733 return; 734 } 735 try { 736 ogp = new OpenGameSDK({ 737 ui: { 738 usePointsWidget: true, 739 useCustomUI: false, 740 theme: 'dark' 741 }, 742 logLevel: 'debug' 743 }); 744 745 ogp.on('OnReady', function() { 746 sdkReady = true; 747 console.log('Play.fun SDK is ready!'); 748 }); 749 750 ogp.on('SessionStarted', function() { 751 console.log('Play.fun session started'); 752 }); 753 754 ogp.on('SavePointsSuccess', function() { 755 console.log('Points saved successfully!'); 756 }); 757 758 ogp.on('SavePointsFailed', function() { 759 console.warn('Points save failed!'); 760 }); 761 762 ogp.onAll(function(eventName, data) { 763 console.log('Play.fun Event:', eventName, data); 764 }); 765 766 ogp.init({ gameId: '571b0545-f172-493a-b5d6-eed4483e66be' }) 767 .then(function() { 768 console.log('OpenGameSDK Initialized successfully'); 769 }) 770 .catch(function(err) { 771 console.error('OpenGameSDK init error:', err); 772 }); 773 } catch (e) { 774 console.warn('Play.fun SDK init failed, running in standalone mode.', e); 775 sdkReady = false; 776 } 777 } 778 779 function sendPoints(points) { 780 if (ogp && points > 0) { 781 try { 782 ogp.addPoints(points); 783 console.log('addPoints(' + points + ') called'); 784 } catch (e) { 785 console.warn('Failed to send points to Play.fun:', e); 786 } 787 } 788 } 789 790 async function endGameSession() { 791 if (ogp) { 792 try { 793 console.log('Calling endGame()...'); 794 await ogp.endGame(); 795 console.log('Game session ended on Play.fun'); 796 } catch (e) { 797 console.warn('Failed to end game session on Play.fun:', e); 798 } 799 } 800 } 801 802 //========================================================================= 803 // Tetris pieces with neon colors 804 //========================================================================= 805 806 var i = { size: 4, blocks: [0x0F00, 0x2222, 0x00F0, 0x4444], color: '#00e5ff' }; 807 var j = { size: 3, blocks: [0x44C0, 0x8E00, 0x6440, 0x0E20], color: '#2979ff' }; 808 var l = { size: 3, blocks: [0x4460, 0x0E80, 0xC440, 0x2E00], color: '#ff9100' }; 809 var o = { size: 2, blocks: [0xCC00, 0xCC00, 0xCC00, 0xCC00], color: '#ffea00' }; 810 var s = { size: 3, blocks: [0x06C0, 0x8C40, 0x6C00, 0x4620], color: '#76ff03' }; 811 var t = { size: 3, blocks: [0x0E40, 0x4C40, 0x4E00, 0x4640], color: '#d500f9' }; 812 var z = { size: 3, blocks: [0x0C60, 0x4C80, 0xC600, 0x2640], color: '#ff1744' }; 813 814 //========================================================================= 815 // Piece logic 816 //========================================================================= 817 818 function eachblock(type, x, y, dir, fn) { 819 var bit, row = 0, col = 0, blocks = type.blocks[dir]; 820 for (bit = 0x8000; bit > 0; bit = bit >> 1) { 821 if (blocks & bit) { 822 fn(x + col, y + row); 823 } 824 if (++col === 4) { 825 col = 0; 826 ++row; 827 } 828 } 829 } 830 831 function occupied(type, x, y, dir) { 832 var result = false;
833 eachblock(type, x, y, dir, function(x, y) { 834 if ((x < 0) || (x >= nx) || (y < 0) || (y >= ny) || getBlock(x, y)) 835 result = true; 836 }); 837 return result; 838 } 839 840 function unoccupied(type, x, y, dir) { 841 return !occupied(type, x, y, dir); 842 } 843 844 var pieces = []; 845 function randomPiece() { 846 if (pieces.length == 0) 847 pieces = [i, i, i, i, j, j, j, j, l, l, l, l, o, o, o, o, s, s, s, s, t, t, t, t, z, z, z, z]; 848 var type = pieces.splice(Math.floor(random(0, pieces.length - 1)), 1)[0]; 849 return { type: type, dir: DIR.UP, x: Math.round(random(0, nx - type.size)), y: 0 }; 850 } 851 852 //========================================================================= 853 // Game loop â robust version 854 //========================================================================= 855 856 function startGameLoop() { 857 if (gameLoopRunning) return; 858 gameLoopRunning = true; 859 var last = timestamp(); 860 861 function frame() { 862 if (!gameLoopRunning) return; 863 var now = timestamp(); 864 update(Math.min(1, (now - last) / 1000.0)); 865 draw(); 866 last = now; 867 gameLoopId = requestAnimationFrame(frame); 868 } 869 870 gameLoopId = requestAnimationFrame(frame); 871 } 872 873 function stopGameLoop() { 874 gameLoopRunning = false; 875 if (gameLoopId) { 876 cancelAnimationFrame(gameLoopId); 877 gameLoopId = null; 878 } 879 } 880 881 function addEvents() { 882 document.addEventListener('keydown', keydown, false); 883 window.addEventListener('resize', resize, false); 884 } 885 886 function resize() { 887 var maxW, maxH; 888 var isMobile = window.innerWidth <= 600; 889 890 if (isMobile) { 891 maxW = Math.min(window.innerWidth - 40, 300); 892 maxH = maxW * 2; 893 } else { 894 maxH = Math.min(window.innerHeight - 100, 600); 895 maxW = maxH / 2; 896 } 897 898 canvas.width = maxW; 899 canvas.height = maxH; 900 canvas.style.width = maxW + 'px'; 901 canvas.style.height = maxH + 'px'; 902 903 dx = canvas.width / nx; 904 dy = canvas.height / ny; 905 906 var uSize = isMobile ? 60 : 100; 907 ucanvas.width = uSize; 908 ucanvas.height = uSize; 909 ucanvas.style.width = uSize + 'px'; 910 ucanvas.style.height = uSize + 'px'; 911 912 invalidate(); 913 invalidateNext(); 914 } 915 916 function keydown(ev) { 917 var handled = false; 918 if (playing) { 919 switch (ev.keyCode) { 920 case KEY.LEFT: actions.push(DIR.LEFT); handled = true; break; 921 case KEY.RIGHT: actions.push(DIR.RIGHT); handled = true; break; 922 case KEY.UP: actions.push(DIR.UP); handled = true; break; 923 case KEY.DOWN: actions.push(DIR.DOWN); handled = true; break; 924 case KEY.ESC: lose(); handled = true; break; 925 } 926 } 927 if (handled) 928 ev.preventDefault(); 929 } 930 931 function handleTouch(action) { 932 if (!playing) return; 933 switch (action) { 934 case 'left': actions.push(DIR.LEFT); break; 935 case 'right': actions.push(DIR.RIGHT); break; 936 case 'rotate': actions.push(DIR.UP); break; 937 case 'down': actions.push(DIR.DOWN); break; 938 } 939 } 940 941 //========================================================================= 942 // Game logic 943 //========================================================================= 944 945 function startGame() { 946 get('start-screen').classList.add('hidden'); 947 get('gameover-screen').classList.add('hidden'); 948 reset(); 949 playing = true; 950 // Start music automatically on first play 951 if (!musicPlaying) { 952 startMusic(); 953 } 954 // Ensure game loop is running 955 stopGameLoop(); 956 startGameLoop(); 957 } 958 959 function restartGame() { 960 get('gameover-screen').classList.add('hidden'); 961 reset(); 962 playing = true; 963 if (!musicPlaying) { 964 startMusic(); 965 } 966 stopGameLoop(); 967 startGameLoop(); 968 } 969 970 async function lose() { 971 playing = false; 972 get('final-score').textContent = Math.floor(score); 973 974 // Send final points and end game session on Play.fun 975 await endGameSession(); 976 977 get('gameover-screen').classList.remove('hidden'); 978 } 979 980 function setVisualScore(n) { vscore = n || score; invalidateScore(); } 981 function setScore(n) { score = n; setVisualScore(n); } 982 function addScore(n) {
983 score = score + n; 984 sendPoints(n); 985 } 986 function clearScore() { setScore(0); } 987 function clearRows() { setRows(0); } 988 function setRows(n) { 989 rows = n; 990 level = Math.floor(rows / 10) + 1; 991 step = Math.max(speed.min, speed.start - (speed.decrement * rows)); 992 invalidateRows(); 993 } 994 function addRows(n) { setRows(rows + n); } 995 function getBlock(x, y) { return (blocks && blocks[x] ? blocks[x][y] : null); } 996 function setBlock(x, y, type) { blocks[x] = blocks[x] || []; blocks[x][y] = type; invalidate(); } 997 function clearBlocks() { blocks = []; invalidate(); } 998 function clearActions() { actions = []; } 999 function setCurrentPiece(piece) { current = piece || randomPiece(); invalidate(); } 1000 function setNextPiece(piece) { next = piece || randomPiece(); invalidateNext(); } 1001 1002 function reset() { 1003 dt = 0; 1004 clearActions(); 1005 clearBlocks(); 1006 clearRows(); 1007 clearScore(); 1008 setCurrentPiece(next); 1009 setNextPiece(); 1010 } 1011 1012 function update(idt) { 1013 if (playing) { 1014 if (vscore < score) 1015 setVisualScore(vscore + 1); 1016 handle(actions.shift()); 1017 dt = dt + idt; 1018 if (dt > step) { 1019 dt = dt - step; 1020 drop(); 1021 } 1022 } 1023 } 1024 1025 function handle(action) { 1026 switch (action) { 1027 case DIR.LEFT: move(DIR.LEFT); break; 1028 case DIR.RIGHT: move(DIR.RIGHT); break; 1029 case DIR.UP: rotate(); break; 1030 case DIR.DOWN: drop(); break; 1031 } 1032 } 1033 1034 function move(dir) { 1035 var x = current.x, y = current.y; 1036 switch (dir) { 1037 case DIR.RIGHT: x = x + 1; break; 1038 case DIR.LEFT: x = x - 1; break; 1039 case DIR.DOWN: y = y + 1; break; 1040 } 1041 if (unoccupied(current.type, x, y, current.dir)) { 1042 current.x = x; 1043 current.y = y; 1044 invalidate(); 1045 return true; 1046 } 1047 return false; 1048 } 1049 1050 function rotate() { 1051 var newdir = (current.dir == DIR.MAX ? DIR.MIN : current.dir + 1); 1052 if (unoccupied(current.type, current.x, current.y, newdir)) { 1053 current.dir = newdir; 1054 invalidate(); 1055 } 1056 } 1057 1058 function drop() { 1059 if (!move(DIR.DOWN)) { 1060 addScore(10); 1061 dropPiece(); 1062 removeLines(); 1063 setCurrentPiece(next); 1064 setNextPiece(randomPiece()); 1065 clearActions(); 1066 if (occupied(current.type, current.x, current.y, current.dir)) { 1067 lose(); 1068 } 1069 } 1070 } 1071 1072 function dropPiece() { 1073 eachblock(current.type, current.x, current.y, current.dir, function(x, y) { 1074 setBlock(x, y, current.type); 1075 }); 1076 } 1077 1078 function removeLines() { 1079 var x, y, complete, n = 0; 1080 for (y = ny ; y > 0 ; --y) { 1081 complete = true; 1082 for (x = 0 ; x < nx ; ++x) { 1083 if (!getBlock(x, y)) 1084 complete = false; 1085 } 1086 if (complete) { 1087 removeLine(y); 1088 y = y + 1; 1089 n++; 1090 } 1091 } 1092 if (n > 0) { 1093 addRows(n); 1094 addScore(100 * Math.pow(2, n - 1)); 1095 } 1096 } 1097 1098 function removeLine(n) { 1099 var x, y; 1100 for (y = n ; y >= 0 ; --y) { 1101 for (x = 0 ; x < nx ; ++x) 1102 setBlock(x, y, (y == 0) ? null : getBlock(x, y - 1)); 1103 } 1104 } 1105 1106 //========================================================================= 1107 // Rendering 1108 //========================================================================= 1109 1110 var invalid = {}; 1111
1112 function invalidate() { invalid.court = true; } 1113 function invalidateNext() { invalid.next = true; } 1114 function invalidateScore() { invalid.score = true; } 1115 function invalidateRows() { invalid.rows = true; } 1116 1117 function draw() { 1118 ctx.save(); 1119 ctx.lineWidth = 1; 1120 ctx.translate(0.5, 0.5); 1121 drawCourt(); 1122 drawNext(); 1123 drawScore(); 1124 drawRows(); 1125 ctx.restore(); 1126 } 1127 1128 function drawCourt() { 1129 if (invalid.court) { 1130 ctx.clearRect(0, 0, canvas.width, canvas.height); 1131 1132 // Draw grid lines 1133 ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)'; 1134 ctx.lineWidth = 0.5; 1135 for (var gx = 0; gx < nx; gx++) { 1136 ctx.beginPath(); 1137 ctx.moveTo(gx * dx, 0); 1138 ctx.lineTo(gx * dx, canvas.height); 1139 ctx.stroke(); 1140 } 1141 for (var gy = 0; gy < ny; gy++) { 1142 ctx.beginPath(); 1143 ctx.moveTo(0, gy * dy); 1144 ctx.lineTo(canvas.width, gy * dy); 1145 ctx.stroke(); 1146 } 1147 1148 ctx.lineWidth = 1; 1149 1150 if (playing) 1151 drawPiece(ctx, current.type, current.x, current.y, current.dir); 1152 1153 var x, y, block; 1154 for (y = 0 ; y < ny ; y++) { 1155 for (x = 0 ; x < nx ; x++) { 1156 if (block = getBlock(x, y)) 1157 drawBlock(ctx, x, y, block.color); 1158 } 1159 } 1160 1161 ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; 1162 ctx.strokeRect(0, 0, nx * dx - 1, ny * dy - 1); 1163 invalid.court = false; 1164 } 1165 } 1166 1167 function drawNext() { 1168 if (invalid.next) { 1169 var padding = (nu - next.type.size) / 2; 1170 var udx = ucanvas.width / nu; 1171 var udy = ucanvas.height / nu; 1172 1173 uctx.save(); 1174 uctx.clearRect(0, 0, ucanvas.width, ucanvas.height); 1175 1176 eachblock(next.type, padding, padding, next.dir, function(x, y) { 1177 uctx.fillStyle = next.type.color; 1178 uctx.shadowColor = next.type.color; 1179 uctx.shadowBlur = 6; 1180 uctx.fillRect(x * udx + 1, y * udy + 1, udx - 2, udy - 2); 1181 uctx.shadowBlur = 0; 1182 uctx.strokeStyle = 'rgba(255,255,255,0.3)'; 1183 uctx.strokeRect(x * udx + 1, y * udy + 1, udx - 2, udy - 2); 1184 }); 1185 1186 uctx.restore(); 1187 invalid.next = false; 1188 } 1189 } 1190 1191 function drawScore() { 1192 if (invalid.score) { 1193 get('score-value').textContent = Math.floor(vscore); 1194 invalid.score = false; 1195 } 1196 } 1197 1198 function drawRows() { 1199 if (invalid.rows) { 1200 get('rows-value').textContent = rows; 1201 get('level-value').textContent = level; 1202 invalid.rows = false; 1203 } 1204 } 1205 1206 function drawPiece(ctx, type, x, y, dir) { 1207 eachblock(type, x, y, dir, function(x, y) { 1208 drawBlock(ctx, x, y, type.color); 1209 }); 1210 } 1211 1212 function drawBlock(ctx, x, y, color) { 1213 var bx = x * dx; 1214 var by = y * dy; 1215 1216 ctx.shadowColor = color; 1217 ctx.shadowBlur = 8; 1218 1219 ctx.fillStyle = color; 1220 ctx.fillRect(bx + 1, by + 1, dx - 2, dy - 2); 1221 1222 ctx.shadowBlur = 0; 1223 ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; 1224 ctx.fillRect(bx + 2, by + 2, dx - 6, 2); 1225 ctx.fillRect(bx + 2, by + 2, 2, dy - 6); 1226 1227 ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; 1228 ctx.strokeRect(bx + 1, by + 1, dx - 2, dy - 2); 1229 } 1230 1231 //========================================================================= 1232 // Initialize â setup events and sizing, but DON'T start game loop yet 1233 //========================================================================= 1234 1235 addEvents(); 1236 resize(); 1237 // Pre-initialize pieces for the first game 1238 next = randomPiece(); 1239 1240 </script>
1240 1241 1242 <!-- Play.fun Browser SDK â loaded ASYNC so it doesn't block game code --> 1243
1243<script src="https://sdk.play.fun" async onload="initPlayFunSDK()" onerror="console.warn('Play.fun SDK failed to load, running standalone.')"></script>
1243 1244 1245</body> 1246</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.