PageSourceSearch

https://glistening-meerkat-26077a.netlify.app/

html glistening-meerkat-26077a.netlify.app collected 2026-10-03 10:49:52 UTC 34,717 bytes, 1,246 lines download raw bytes

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">&#9835;</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">&#8592;</div>
415        <div class="start-control-label">Move Left</div>
416      </div>
417      <div class="start-control-item">
418        <div class="start-key">&#8594;</div>
419        <div class="start-control-label">Move Right</div>
420      </div>
421      <div class="start-control-item">
422        <div class="start-key">&#8593;</div>
423        <div class="start-control-label">Rotate</div>
424      </div>
425      <div class="start-control-item">
426        <div class="start-key">&#8595;</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">&#8592;</span> Move Left</div>
471        <div class="control-row"><span class="key-badge">&#8594;</span> Move Right</div>
472        <div class="control-row"><span class="key-badge">&#8593;</span> Rotate</div>
473        <div class="control-row"><span class="key-badge">&#8595;</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')">&#8635;</div>
483    </div>
484    <div class="controls-row">
485      <div class="ctrl-btn" ontouchstart="handleTouch('left')" onclick="handleTouch('left')">&#9664;</div>
486      <div class="ctrl-btn wide" ontouchstart="handleTouch('down')" onclick="handleTouch('down')">&#9660; DROP</div>
487      <div class="ctrl-btn" ontouchstart="handleTouch('right')" onclick="handleTouch('right')">&#9654;</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.