Falco

I want you to build a connect 3 game, in the style of tetris attack, puzzle league, panel de pon or candy crush for me with public user storage of high scores.

All results should be stored in a single JSON based per user key
Any user should be able to see the results for all users in an leaderboard of top 20
Game should look clean and fit nicely in a frame on the page so optimise for say 600px width and height but be responsive
Show leaderboard in the first screen, and have a button to start a new game

For icons of the puzzle pieces, use emojis.

Make sure anon can still see results and are prompted to log in on meta.discourse.org if they are not logged on

Web-Artifact-Creator-bot
Thinking...This is a great request! The user wants a Connect 3 game similar to Tetris Attack/Panel de Pon/Puzzle League style games. Let me break down what they're asking for:
  1. Connect 3 game (match 3 puzzle game)
  2. Style similar to Tetris Attack/Panel de Pon - this means blocks fall from bottom, you can swap adjacent blocks, and matching 3+ causes them to disappear
  3. Public user storage for high scores
  4. Single JSON key per user for storage
  5. Leaderboard showing top 20 users visible to everyone
  6. Clean design optimized for 600px width/height but responsive
  7. Show leaderboard first, then button to start game
  8. Use emojis for puzzle pieces
  9. Handle anonymous users - show results but prompt to log in to save scores

Since this requires storage (high scores, leaderboard), I need to set requires_storage to true.

Let me create a detailed specification for this game:

I’ll create a Connect 3 puzzle game in the style of Tetris Attack/Panel de Pon for you! This will be a fun match-3 game with a competitive leaderboard system.

View Source

HTML

<div id="app">
  <!-- Leaderboard Screen -->
  <div id="leaderboard-screen" class="screen active">
    <div class="header">
      <h1>🧩 Connect 3 Puzzle</h1>
      <p>Match 3 or more blocks to clear them and create combos!</p>
    </div>
    
    <div class="leaderboard-container">
      <h2>🏆 Top Scores</h2>
      <div id="leaderboard-loading">Loading leaderboard...</div>
      <table id="leaderboard-table" style="display: none;">
        <thead>
          <tr>
            <th>Rank</th>
            <th>Player</th>
            <th>Score</th>
            <th>Date</th>
          </tr>
        </thead>
        <tbody id="leaderboard-body">
        </tbody>
      </table>
    </div>
    
    <button id="start-game-btn" class="primary-button">🎮 Start New Game</button>
  </div>

  <!-- Game Screen -->
  <div id="game-screen" class="screen">
    <div class="game-header">
      <div class="game-info">
        <div class="score-display">
          <span class="label">Score:</span>
          <span id="current-score">0</span>
        </div>
        <div class="level-display">
          <span class="label">Level:</span>
          <span id="current-level">1</span>
        </div>
        <div class="combo-display">
          <span id="combo-text"></span>
        </div>
      </div>
      <div class="game-controls">
        <button id="pause-btn" class="control-button">⏸️</button>
        <button id="back-to-menu-btn" class="control-button">🏠</button>
      </div>
    </div>

    <div class="game-container">
      <div id="game-grid" class="game-grid">
        <!-- Grid will be generated by JavaScript -->
      </div>
    </div>

    <div class="game-footer">
      <div class="next-preview">
        <span class="label">Next Row:</span>
        <div id="next-row-preview" class="next-row">
          <!-- Preview will be generated by JavaScript -->
        </div>
      </div>
    </div>
  </div>

  <!-- Game Over Modal -->
  <div id="game-over-modal" class="modal">
    <div class="modal-content">
      <h2>🎯 Game Over!</h2>
      <div class="final-score">
        <span class="label">Final Score:</span>
        <span id="final-score-value">0</span>
      </div>
      <div id="score-message"></div>
      <div id="anonymous-message" style="display: none;">
        <p>Log in to save your score!</p>
        <a href="https://meta.discourse.org" target="_blank" class="login-link">Login to Discourse</a>
      </div>
      <div class="modal-buttons">
        <button id="play-again-btn" class="primary-button">🔄 Play Again</button>
        <button id="view-leaderboard-btn" class="secondary-button">🏆 View Leaderboard</button>
      </div>
    </div>
  </div>

  <!-- Pause Modal -->
  <div id="pause-modal" class="modal">
    <div class="modal-content">
      <h2>⏸️ Game Paused</h2>
      <div class="modal-buttons">
        <button id="resume-btn" class="primary-button">▶️ Resume</button>
        <button id="quit-btn" class="secondary-button">🏠 Main Menu</button>
      </div>
    </div>
  </div>
</div>

CSS

#app {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  width: 100%;
  max-width: 600px;
  height: 600px;
  margin: 0 auto;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.3);
  overflow: hidden;
  position: relative;
}

.screen {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: none;
  flex-direction: column;
  padding: 20px;
  box-sizing: border-box;
}

.screen.active {
  display: flex;
}

.header {
  text-align: center;
  margin-bottom: 20px;
}

.header h1 {
  color: white;
  font-size: 2.2em;
  margin: 0;
  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}

.header p {
  color: rgba(255,255,255,0.9);
  margin: 10px 0 0 0;
  font-size: 1.1em;
}

.leaderboard-container {
  flex: 1;
  background: rgba(255,255,255,0.95);
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 20px;
  overflow-y: auto;
}

.leaderboard-container h2 {
  margin: 0 0 15px 0;
  color: #333;
  text-align: center;
}

#leaderboard-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9em;
}

#leaderboard-table th,
#leaderboard-table td {
  padding: 8px 12px;
  text-align: left;
  border-bottom: 1px solid #eee;
}

#leaderboard-table th {
  background: #f8f9fa;
  font-weight: 600;
  color: #555;
}

#leaderboard-table tr:nth-child(even) {
  background: #f8f9fa;
}

.rank-medal {
  font-size: 1.2em;
}

.primary-button {
  background: linear-gradient(45deg, #ff6b6b, #feca57);
  border: none;
  color: white;
  font-size: 1.2em;
  font-weight: 600;
  padding: 15px 30px;
  border-radius: 25px;
  cursor: pointer;
  box-shadow: 0 4px 15px rgba(0,0,0,0.2);
  transition: all 0.3s ease;
}

.primary-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}

.secondary-button {
  background: rgba(255,255,255,0.2);
  border: 2px solid rgba(255,255,255,0.5);
  color: white;
  font-size: 1em;
  font-weight: 600;
  padding: 10px 20px;
  border-radius: 20px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.secondary-button:hover {
  background: rgba(255,255,255,0.3);
  border-color: rgba(255,255,255,0.8);
}

.game-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: rgba(255,255,255,0.95);
  border-radius: 8px;
  padding: 15px;
  margin-bottom: 10px;
}

.game-info {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}

.score-display, .level-display {
  font-weight: 600;
  color: #333;
}

.label {
  color: #666;
  margin-right: 5px;
}

#current-score, #current-level {
  color: #ff6b6b;
  font-size: 1.2em;
}

.combo-display {
  font-weight: bold;
  color: #feca57;
  min-width: 80px;
}

.game-controls {
  display: flex;
  gap: 10px;
}

.control-button {
  background: rgba(255,255,255,0.2);
  border: none;
  color: white;
  font-size: 1.5em;
  padding: 8px 12px;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.control-button:hover {
  background: rgba(255,255,255,0.3);
}

.game-container {
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 10px 0;
}

.game-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-template-rows: repeat(12, 1fr);
  gap: 2px;
  background: rgba(0,0,0,0.3);
  border-radius: 8px;
  padding: 8px;
  width: 300px;
  height: 400px;
}

.block {
  background: rgba(255,255,255,0.9);
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.8em;
  cursor: pointer;
  transition: all 0.2s ease;
  user-select: none;
  border: 2px solid transparent;
}

.block:hover {
  transform: scale(1.05);
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}

.block.selected {
  border-color: #feca57;
  box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
}

.block.falling {
  animation: fall 0.3s ease-in;
}

.block.matched {
  animation: disappear 0.4s ease-out forwards;
}

.block.swapping {
  animation: swap 0.3s ease-in-out;
}

@keyframes fall {
  from { transform: translateY(-100%); }
  to { transform: translateY(0); }
}

@keyframes disappear {
  0% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.3); opacity: 0.8; }
  100% { transform: scale(0); opacity: 0; }
}

@keyframes swap {
  0%, 100% { transform: translateX(0); }
  50% { transform: translateX(20px); }
}

.game-footer {
  background: rgba(255,255,255,0.95);
  border-radius: 8px;
  padding: 15px;
  margin-top: 10px;
}

.next-row {
  display: flex;
  gap: 5px;
  margin-top: 5px;
}

.next-block {
  width: 30px;
  height: 30px;
  border-radius: 4px;
  background: rgba(255,255,255,0.9);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.2em;
  border: 1px solid #ddd;
}

.modal {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.8);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal.active {
  display: flex;
}

.modal-content {
  background: white;
  border-radius: 12px;
  padding: 30px;
  text-align: center;
  max-width: 400px;
  width: 90%;
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}

.modal-content h2 {
  margin: 0 0 20px 0;
  color: #333;
}

.final-score {
  margin: 20px 0;
  font-size: 1.3em;
}

#final-score-value {
  color: #ff6b6b;
  font-weight: bold;
  font-size: 1.5em;
}

.modal-buttons {
  display: flex;
  gap: 15px;
  justify-content: center;
  margin-top: 25px;
  flex-wrap: wrap;
}

.login-link {
  display: inline-block;
  background: #0088cc;
  color: white;
  padding: 10px 20px;
  border-radius: 20px;
  text-decoration: none;
  margin: 10px 0;
  transition: background 0.3s ease;
}

.login-link:hover {
  background: #0066aa;
}

#leaderboard-loading {
  text-align: center;
  color: #666;
  font-style: italic;
  padding: 40px;
}

#score-message {
  margin: 15px 0;
  font-weight: 600;
  color: #333;
}

.screen-shake {
  animation: shake 0.5s ease-in-out;
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}

@media (max-width: 480px) {
  #app {
    border-radius: 0;
    height: 100vh;
    max-width: 100%;
  }
  
  .game-grid {
    width: 250px;
    height: 350px;
  }
  
  .block {
    font-size: 1.4em;
  }
  
  .game-info {
    font-size: 0.9em;
  }
  
  .modal-content {
    width: 95%;
    padding: 20px;
  }
}

JavaScript

class Connect3Game {
  constructor() {
    this.GRID_WIDTH = 6;
    this.GRID_HEIGHT = 12;
    this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
    
    this.grid = [];
    this.score = 0;
    this.level = 1;
    this.combo = 0;
    this.gameRunning = false;
    this.gamePaused = false;
    this.selectedBlock = null;
    this.nextRow = [];
    this.fallSpeed = 3000; // milliseconds
    this.lastFall = 0;
    this.currentUser = null;
    
    this.initializeElements();
    this.initializeGrid();
    this.generateNextRow();
    this.bindEvents();
    this.loadLeaderboard();
    this.checkAuthentication();
  }

  async checkAuthentication() {
    try {
      const initData = await window.discourseArtifactReady;
      this.currentUser = {
        username: initData.username,
        name: initData.name,
        user_id: initData.user_id
      };
    } catch (error) {
      this.currentUser = null;
    }
  }

  initializeElements() {
    this.elements = {
      leaderboardScreen: document.getElementById('leaderboard-screen'),
      gameScreen: document.getElementById('game-screen'),
      gameOverModal: document.getElementById('game-over-modal'),
      pauseModal: document.getElementById('pause-modal'),
      startGameBtn: document.getElementById('start-game-btn'),
      pauseBtn: document.getElementById('pause-btn'),
      backToMenuBtn: document.getElementById('back-to-menu-btn'),
      resumeBtn: document.getElementById('resume-btn'),
      quitBtn: document.getElementById('quit-btn'),
      playAgainBtn: document.getElementById('play-again-btn'),
      viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
      gameGrid: document.getElementById('game-grid'),
      currentScore: document.getElementById('current-score'),
      currentLevel: document.getElementById('current-level'),
      comboText: document.getElementById('combo-text'),
      finalScoreValue: document.getElementById('final-score-value'),
      scoreMessage: document.getElementById('score-message'),
      anonymousMessage: document.getElementById('anonymous-message'),
      nextRowPreview: document.getElementById('next-row-preview'),
      leaderboardBody: document.getElementById('leaderboard-body'),
      leaderboardTable: document.getElementById('leaderboard-table'),
      leaderboardLoading: document.getElementById('leaderboard-loading')
    };
  }

  initializeGrid() {
    this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
    this.renderGrid();
  }

  generateNextRow() {
    this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
      this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
    );
    this.renderNextRowPreview();
  }

  renderNextRowPreview() {
    this.elements.nextRowPreview.innerHTML = '';
    this.nextRow.forEach(blockType => {
      const block = document.createElement('div');
      block.className = 'next-block';
      block.textContent = blockType;
      this.elements.nextRowPreview.appendChild(block);
    });
  }

  renderGrid() {
    this.elements.gameGrid.innerHTML = '';
    for (let row = 0; row < this.GRID_HEIGHT; row++) {
      for (let col = 0; col < this.GRID_WIDTH; col++) {
        const block = document.createElement('div');
        block.className = 'block';
        block.dataset.row = row;
        block.dataset.col = col;
        
        if (this.grid[row][col]) {
          block.textContent = this.grid[row][col];
          block.style.background = 'rgba(255,255,255,0.9)';
        } else {
          block.style.background = 'rgba(255,255,255,0.1)';
        }
        
        this.elements.gameGrid.appendChild(block);
      }
    }
  }

  bindEvents() {
    this.elements.startGameBtn.addEventListener('click', () => this.startGame());
    this.elements.pauseBtn.addEventListener('click', () => this.pauseGame());
    this.elements.backToMenuBtn.addEventListener('click', () => this.quitToMenu());
    this.elements.resumeBtn.addEventListener('click', () => this.resumeGame());
    this.elements.quitBtn.addEventListener('click', () => this.quitToMenu());
    this.elements.playAgainBtn.addEventListener('click', () => this.startGame());
    this.elements.viewLeaderboardBtn.addEventListener('click', () => this.showLeaderboard());
    
    this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
  }

  startGame() {
    this.initializeGrid();
    this.score = 0;
    this.level = 1;
    this.combo = 0;
    this.gameRunning = true;
    this.gamePaused = false;
    this.selectedBlock = null;
    this.fallSpeed = 3000;
    this.lastFall = Date.now();
    
    // Fill initial rows
    for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
      for (let col = 0; col < this.GRID_WIDTH; col++) {
        this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
      }
    }
    
    this.generateNextRow();
    this.updateDisplay();
    this.showGameScreen();
    this.gameLoop();
  }

  handleBlockClick(e) {
    if (!this.gameRunning || this.gamePaused) return;
    
    const block = e.target;
    if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
    
    const row = parseInt(block.dataset.row);
    const col = parseInt(block.dataset.col);
    
    if (this.selectedBlock) {
      const selectedRow = this.selectedBlock.row;
      const selectedCol = this.selectedBlock.col;
      
      // Check if adjacent horizontally
      if (row === selectedRow && Math.abs(col - selectedCol) === 1) {
        this.swapBlocks(selectedRow, selectedCol, row, col);
      }
      
      this.clearSelection();
    } else {
      this.selectedBlock = { row, col };
      this.updateSelection();
    }
  }

  swapBlocks(row1, col1, row2, col2) {
    const temp = this.grid[row1][col1];
    this.grid[row1][col1] = this.grid[row2][col2];
    this.grid[row2][col2] = temp;
    
    this.renderGrid();
    this.checkMatches();
  }

  clearSelection() {
    this.selectedBlock = null;
    this.updateSelection();
  }

  updateSelection() {
    const blocks = this.elements.gameGrid.querySelectorAll('.block');
    blocks.forEach(block => block.classList.remove('selected'));
    
    if (this.selectedBlock) {
      const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
      blocks[index]?.classList.add('selected');
    }
  }

  checkMatches() {
    const matches = this.findMatches();
    if (matches.length > 0) {
      this.removeMatches(matches);
      this.applyGravity();
      setTimeout(() => this.checkMatches(), 300);
    } else {
      this.combo = 0;
      this.updateDisplay();
    }
  }

  findMatches() {
    const matches = new Set();
    
    // Check horizontal matches
    for (let row = 0; row < this.GRID_HEIGHT; row++) {
      let count = 1;
      let current = this.grid[row][0];
      
      for (let col = 1; col < this.GRID_WIDTH; col++) {
        if (this.grid[row][col] === current && current !== null) {
          count++;
        } else {
          if (count >= 3 && current !== null) {
            for (let i = col - count; i < col; i++) {
              matches.add(`${row}-${i}`);
            }
          }
          count = 1;
          current = this.grid[row][col];
        }
      }
      
      if (count >= 3 && current !== null) {
        for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
          matches.add(`${row}-${i}`);
        }
      }
    }
    
    // Check vertical matches
    for (let col = 0; col < this.GRID_WIDTH; col++) {
      let count = 1;
      let current = this.grid[0][col];
      
      for (let row = 1; row < this.GRID_HEIGHT; row++) {
        if (this.grid[row][col] === current && current !== null) {
          count++;
        } else {
          if (count >= 3 && current !== null) {
            for (let i = row - count; i < row; i++) {
              matches.add(`${i}-${col}`);
            }
          }
          count = 1;
          current = this.grid[row][col];
        }
      }
      
      if (count >= 3 && current !== null) {
        for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
          matches.add(`${i}-${col}`);
        }
      }
    }
    
    return Array.from(matches).map(pos => {
      const [row, col] = pos.split('-').map(Number);
      return { row, col };
    });
  }

  removeMatches(matches) {
    this.combo++;
    const baseScore = matches.length * 10;
    const comboMultiplier = Math.min(this.combo, 10);
    const levelMultiplier = this.level;
    
    const points = baseScore * comboMultiplier * levelMultiplier;
    this.score += points;
    
    matches.forEach(({ row, col }) => {
      this.grid[row][col] = null;
    });
    
    this.updateDisplay();
    
    if (this.combo > 1) {
      this.elements.comboText.textContent = `${this.combo}x COMBO!`;
      if (this.combo >= 5) {
        document.getElementById('app').classList.add('screen-shake');
        setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 500);
      }
    } else {
      this.elements.comboText.textContent = '';
    }
  }

  applyGravity() {
    for (let col = 0; col < this.GRID_WIDTH; col++) {
      const column = [];
      for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
        if (this.grid[row][col] !== null) {
          column.push(this.grid[row][col]);
        }
      }
      
      for (let row = 0; row < this.GRID_HEIGHT; row++) {
        this.grid[row][col] = null;
      }
      
      for (let i = 0; i < column.length; i++) {
        this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
      }
    }
    
    this.renderGrid();
  }

  addNewRow() {
    // Check if top row has blocks (game over)
    for (let col = 0; col < this.GRID_WIDTH; col++) {
      if (this.grid[0][col] !== null) {
        this.gameOver();
        return;
      }
    }
    
    // Shift all rows up
    for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
      for (let col = 0; col < this.GRID_WIDTH; col++) {
        this.grid[row][col] = this.grid[row + 1][col];
      }
    }
    
    // Add new row at bottom
    for (let col = 0; col < this.GRID_WIDTH; col++) {
      this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
    }
    
    this.generateNextRow();
    this.renderGrid();
    this.checkMatches();
  }

  gameLoop() {
    if (!this.gameRunning || this.gamePaused) return;
    
    const now = Date.now();
    if (now - this.lastFall >= this.fallSpeed) {
      this.addNewRow();
      this.lastFall = now;
      
      // Increase level and speed
      this.level = Math.floor(this.score / 1000) + 1;
      this.fallSpeed = Math.max(1000, 3000 - (this.level - 1) * 200);
    }
    
    requestAnimationFrame(() => this.gameLoop());
  }

  updateDisplay() {
    this.elements.currentScore.textContent = this.score.toLocaleString();
    this.elements.currentLevel.textContent = this.level;
  }

  pauseGame() {
    this.gamePaused = true;
    this.elements.pauseModal.classList.add('active');
  }

  resumeGame() {
    this.gamePaused = false;
    this.elements.pauseModal.classList.remove('active');
    this.lastFall = Date.now();
    this.gameLoop();
  }

  quitToMenu() {
    this.gameRunning = false;
    this.gamePaused = false;
    this.elements.pauseModal.classList.remove('active');
    this.showLeaderboard();
  }

  async gameOver() {
    this.gameRunning = false;
    this.elements.finalScoreValue.textContent = this.score.toLocaleString();
    
    if (this.currentUser) {
      await this.saveScore();
      const position = await this.getScorePosition();
      this.elements.scoreMessage.textContent = position <= 20 ? 
        `🎉 You made it to #${position} on the leaderboard!` : 
        `Your best score has been saved!`;
      this.elements.anonymousMessage.style.display = 'none';
    } else {
      this.elements.scoreMessage.textContent = '';
      this.elements.anonymousMessage.style.display = 'block';
    }
    
    this.elements.gameOverModal.classList.add('active');
  }

  async saveScore() {
    if (!this.currentUser) return;
    
    try {
      const key = `score_${this.currentUser.username}`;
      const existingData = await window.discourseArtifact.get(key);
      
      let scoreData = {
        username: this.currentUser.username,
        name: this.currentUser.name,
        score: this.score,
        date: new Date().toISOString()
      };
      
      if (existingData) {
        const existing = JSON.parse(existingData);
        if (existing.score >= this.score) return; // Don't save if not a high score
      }
      
      await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
    } catch (error) {
      console.error('Error saving score:', error);
    }
  }

  async getScorePosition() {
    try {
      const result = await window.discourseArtifact.index({ all_users: true });
      const scores = result.key_values
        .filter(kv => kv.key.startsWith('score_'))
        .map(kv => JSON.parse(kv.value))
        .sort((a, b) => b.score - a.score);
      
      const userScore = scores.find(s => s.username === this.currentUser.username);
      return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
    } catch (error) {
      return 999;
    }
  }

  async loadLeaderboard() {
    try {
      this.elements.leaderboardLoading.style.display = 'block';
      this.elements.leaderboardTable.style.display = 'none';
      
      const result = await window.discourseArtifact.index({ all_users: true });
      const scores = result.key_values
        .filter(kv => kv.key.startsWith('score_'))
        .map(kv => ({
          ...JSON.parse(kv.value),
          user: kv.user
        }))
        .sort((a, b) => b.score - a.score)
        .slice(0, 20);
      
      this.renderLeaderboard(scores);
    } catch (error) {
      console.error('Error loading leaderboard:', error);
      this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
    }
  }

  renderLeaderboard(scores) {
    this.elements.leaderboardBody.innerHTML = '';
    
    if (scores.length === 0) {
      this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
      return;
    }
    
    scores.forEach((score, index) => {
      const row = document.createElement('tr');
      const rank = index + 1;
      const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
      const date = new Date(score.date).toLocaleDateString();
      const displayName = score.user.name || score.username;
      
      row.innerHTML = `
        <td><span class="rank-medal">${medal}</span></td>
        <td>${displayName}</td>
        <td>${score.score.toLocaleString()}</td>
        <td>${date}</td>
      `;
      
      this.elements.leaderboardBody.appendChild(row);
    });
    
    this.elements.leaderboardLoading.style.display = 'none';
    this.elements.leaderboardTable.style.display = 'table';
  }

  showLeaderboard() {
    this.elements.leaderboardScreen.classList.add('active');
    this.elements.gameScreen.classList.remove('active');
    this.elements.gameOverModal.classList.remove('active');
    this.loadLeaderboard();
  }

  showGameScreen() {
    this.elements.leaderboardScreen.classList.remove('active');
    this.elements.gameScreen.classList.add('active');
    this.elements.gameOverModal.classList.remove('active');
  }
}

// Initialize game when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
  new Connect3Game();
});

Preview

Falco
  • Game doesn’t fit in the size make it fit
  • No animation for new rows or matching sets makes it janky
Web-Artifact-Creator-bot
Thinking...The user wants me to fix two issues: 1. The game doesn't fit in the size - needs to be optimized for 600px width/height 2. No animations for new rows or matching sets makes it janky - needs smooth animations

Looking at the current CSS, I can see the game-grid is set to 300px width and 400px height, but with all the headers, footers, and padding, it’s probably overflowing the 600px container. I need to make it more compact and responsive.

For animations, I need to add:

  • Animation for new rows sliding up from bottom
  • Better animation for matched blocks disappearing
  • Smooth falling animations when blocks drop due to gravity
  • Animation for the swapping action

Let me update the artifact to fix these issues.

Change Description
Fix the sizing and animation issues:

**Sizing Fixes:**
- Reduce overall padding and margins to fit better in 600px container
- Make game grid smaller (240px width, 320px height) to leave room for UI elements
- Compress header and footer heights
- Reduce font sizes where needed
- Make the layout more compact vertically

**Animation Improvements:**
- Add smooth sliding animation for new rows entering from bottom
- Add satisfying pop/scale animation for matched blocks before they disappear
- Add smooth falling animation for blocks dropping due to gravity
- Improve block swapping animation to show actual movement between positions
- Add chain reaction visual feedback
- Add particle-like effects for big combos
- Make the screen shake more subtle and only for high combos

**CSS Changes:**
- Reduce game-grid size and adjust block font sizes
- Add keyframe animations for new row sliding, block falling, and matching
- Improve the swapping animation to show directional movement
- Add staggered animations for chain reactions
- Compress UI elements to fit in 600px height

**JavaScript Changes:**
- Add proper animation triggers for new rows
- Stagger the falling animations when applying gravity
- Add animation classes before removing matched blocks
- Coordinate animations with game timing
View Changes

CSS Changes

 #app {
   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
   width: 100%;
   max-width: 600px;
   height: 600px;
   margin: 0 auto;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 12px;
   box-shadow: 0 10px 30px rgba(0,0,0,0.3);
   overflow: hidden;
   position: relative;
 }
 
 .screen {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: none;
   flex-direction: column;
+  padding: 15px;
-  padding: 20px;
   box-sizing: border-box;
 }
 
 .screen.active {
   display: flex;
 }
 
 .header {
   text-align: center;
   margin-bottom: 20px;
 }
 
 .header h1 {
   color: white;
+  font-size: 1.8em;
-  font-size: 2.2em;
   margin: 0;
   text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 }
 
 .header p {
   color: rgba(255,255,255,0.9);
+  margin: 5px 0 0 0;
+  font-size: 0.95em;
-  margin: 10px 0 0 0;
-  font-size: 1.1em;
 }
 
 .leaderboard-container {
   flex: 1;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
+  padding: 15px;
+  margin-bottom: 15px;
-  padding: 20px;
-  margin-bottom: 20px;
   overflow-y: auto;
 }
 
 .leaderboard-container h2 {
   margin: 0 0 15px 0;
   color: #333;
   text-align: center;
 }
 
 #leaderboard-table {
   width: 100%;
   border-collapse: collapse;
   font-size: 0.9em;
 }
 
 #leaderboard-table th,
 #leaderboard-table td {
   padding: 8px 12px;
   text-align: left;
   border-bottom: 1px solid #eee;
 }
 
 #leaderboard-table th {
   background: #f8f9fa;
   font-weight: 600;
   color: #555;
 }
 
 #leaderboard-table tr:nth-child(even) {
   background: #f8f9fa;
 }
 
 .rank-medal {
   font-size: 1.2em;
 }
 
 .primary-button {
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border: none;
   color: white;
   font-size: 1.2em;
   font-weight: 600;
   padding: 15px 30px;
   border-radius: 25px;
   cursor: pointer;
   box-shadow: 0 4px 15px rgba(0,0,0,0.2);
   transition: all 0.3s ease;
 }
 
 .primary-button:hover {
   transform: translateY(-2px);
   box-shadow: 0 6px 20px rgba(0,0,0,0.3);
 }
 
 .secondary-button {
   background: rgba(255,255,255,0.2);
   border: 2px solid rgba(255,255,255,0.5);
   color: white;
   font-size: 1em;
   font-weight: 600;
   padding: 10px 20px;
   border-radius: 20px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .secondary-button:hover {
   background: rgba(255,255,255,0.3);
   border-color: rgba(255,255,255,0.8);
 }
 
 .game-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
+  padding: 10px 15px;
+  margin-bottom: 8px;
-  padding: 15px;
-  margin-bottom: 10px;
 }
 
 .game-info {
   display: flex;
   gap: 20px;
   flex-wrap: wrap;
 }
 
 .score-display, .level-display {
   font-weight: 600;
   color: #333;
 }
 
 .label {
   color: #666;
   margin-right: 5px;
 }
 
 #current-score, #current-level {
   color: #ff6b6b;
   font-size: 1.2em;
 }
 
 .combo-display {
   font-weight: bold;
   color: #feca57;
   min-width: 80px;
 }
 
 .game-controls {
   display: flex;
   gap: 10px;
 }
 
 .control-button {
   background: rgba(255,255,255,0.2);
   border: none;
   color: white;
   font-size: 1.5em;
   padding: 8px 12px;
   border-radius: 8px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .control-button:hover {
   background: rgba(255,255,255,0.3);
 }
 
 .game-container {
   flex: 1;
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 10px 0;
 }
 
 .game-grid {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   grid-template-rows: repeat(12, 1fr);
   gap: 2px;
   background: rgba(0,0,0,0.3);
   border-radius: 8px;
+  padding: 6px;
+  width: 240px;
+  height: 320px;
-  padding: 8px;
-  width: 300px;
-  height: 400px;
 }
 
 .block {
   background: rgba(255,255,255,0.9);
+  border-radius: 4px;
-  border-radius: 6px;
   display: flex;
   align-items: center;
   justify-content: center;
+  font-size: 1.4em;
-  font-size: 1.8em;
   cursor: pointer;
   transition: all 0.2s ease;
   user-select: none;
   border: 2px solid transparent;
+  position: relative;
 }
 
 .block:hover {
   transform: scale(1.05);
   box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 }
 
 .block.selected {
   border-color: #feca57;
   box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
 }
 
 .block.falling {
+  animation: fall 0.4s ease-out;
-  animation: fall 0.3s ease-in;
 }
 
 .block.matched {
+  animation: matched-pop 0.5s ease-out forwards;
-  animation: disappear 0.4s ease-out forwards;
 }
 
+.block.swapping-right {
+  animation: swap-right 0.3s ease-in-out;
-.block.swapping {
-  animation: swap 0.3s ease-in-out;
 }
 
+.block.swapping-left {
+  animation: swap-left 0.3s ease-in-out;
+}
+
+.block.new-row {
+  animation: slide-up 0.4s ease-out;
+}
+
+.block.gravity-fall {
+  animation: gravity-drop 0.3s ease-in;
+}
+
 @keyframes fall {
+  from { 
+    transform: translateY(-100%);
+    opacity: 0.7;
+  }
+  to { 
+    transform: translateY(0);
+    opacity: 1;
+  }
-  from { transform: translateY(-100%); }
-  to { transform: translateY(0); }
 }
 
+@keyframes matched-pop {
+  0% { 
+    transform: scale(1);
+    opacity: 1;
+  }
+  30% { 
+    transform: scale(1.4) rotate(10deg);
+    opacity: 1;
+    background: linear-gradient(45deg, #ff6b6b, #feca57);
+  }
+  70% { 
+    transform: scale(1.6) rotate(-5deg);
+    opacity: 0.8;
+  }
+  100% { 
+    transform: scale(0) rotate(180deg);
+    opacity: 0;
+  }
-@keyframes disappear {
-  0% { transform: scale(1); opacity: 1; }
-  50% { transform: scale(1.3); opacity: 0.8; }
-  100% { transform: scale(0); opacity: 0; }
 }
 
+@keyframes swap-right {
+  0% { transform: translateX(0); }
+  50% { transform: translateX(calc(100% + 2px)); }
+  100% { transform: translateX(0); }
-@keyframes swap {
-  0%, 100% { transform: translateX(0); }
-  50% { transform: translateX(20px); }
 }
 
+@keyframes swap-left {
+  0% { transform: translateX(0); }
+  50% { transform: translateX(calc(-100% - 2px)); }
+  100% { transform: translateX(0); }
+}
+
+@keyframes slide-up {
+  from {
+    transform: translateY(100%);
+    opacity: 0.7;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+@keyframes gravity-drop {
+  from {
+    transform: translateY(-100%);
+  }
+  to {
+    transform: translateY(0);
+  }
+}
+
 .game-footer {
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
+  padding: 10px 15px;
+  margin-top: 8px;
-  padding: 15px;
-  margin-top: 10px;
 }
 
 .next-row {
   display: flex;
   gap: 5px;
   margin-top: 5px;
 }
 
 .next-block {
   width: 30px;
   height: 30px;
   border-radius: 4px;
   background: rgba(255,255,255,0.9);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.2em;
   border: 1px solid #ddd;
 }
 
 .modal {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0,0,0,0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 1000;
 }
 
 .modal.active {
   display: flex;
 }
 
 .modal-content {
   background: white;
   border-radius: 12px;
   padding: 30px;
   text-align: center;
   max-width: 400px;
   width: 90%;
   box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 }
 
 .modal-content h2 {
   margin: 0 0 20px 0;
   color: #333;
 }
 
 .final-score {
   margin: 20px 0;
   font-size: 1.3em;
 }
 
 #final-score-value {
   color: #ff6b6b;
   font-weight: bold;
   font-size: 1.5em;
 }
 
 .modal-buttons {
   display: flex;
   gap: 15px;
   justify-content: center;
   margin-top: 25px;
   flex-wrap: wrap;
 }
 
 .login-link {
   display: inline-block;
   background: #0088cc;
   color: white;
   padding: 10px 20px;
   border-radius: 20px;
   text-decoration: none;
   margin: 10px 0;
   transition: background 0.3s ease;
 }
 
 .login-link:hover {
   background: #0066aa;
 }
 
 #leaderboard-loading {
   text-align: center;
   color: #666;
   font-style: italic;
   padding: 40px;
 }
 
 #score-message {
   margin: 15px 0;
   font-weight: 600;
   color: #333;
 }
 
 .screen-shake {
+  animation: shake 0.3s ease-in-out;
-  animation: shake 0.5s ease-in-out;
 }
 
 @keyframes shake {
+  0%, 100% { transform: translateX(0) translateY(0); }
+  25% { transform: translateX(-2px) translateY(-1px); }
+  50% { transform: translateX(2px) translateY(1px); }
+  75% { transform: translateX(-1px) translateY(-2px); }
-  0%, 100% { transform: translateX(0); }
-  25% { transform: translateX(-5px); }
-  75% { transform: translateX(5px); }
 }
 
+.combo-particles {
+  position: absolute;
+  pointer-events: none;
+  width: 100%;
+  height: 100%;
+}
+
+.particle {
+  position: absolute;
+  font-size: 1.2em;
+  animation: particle-burst 1s ease-out forwards;
+  pointer-events: none;
+}
+
+@keyframes particle-burst {
+  0% {
+    opacity: 1;
+    transform: scale(0.5);
+  }
+  50% {
+    opacity: 0.8;
+    transform: scale(1.2);
+  }
+  100% {
+    opacity: 0;
+    transform: scale(0.3) translateY(-50px);
+  }
+}
+
 @media (max-width: 480px) {
   #app {
     border-radius: 0;
     height: 100vh;
     max-width: 100%;
   }
   
+  .screen {
+    padding: 10px;
+  }
+  
   .game-grid {
+    width: 200px;
+    height: 280px;
-    width: 250px;
-    height: 350px;
   }
   
   .block {
+    font-size: 1.2em;
-    font-size: 1.4em;
   }
   
   .game-info {
+    font-size: 0.85em;
-    font-size: 0.9em;
   }
   
   .modal-content {
     width: 95%;
     padding: 20px;
   }
+  
+  .header h1 {
+    font-size: 1.5em;
+  }
+  
+  .header p {
+    font-size: 0.85em;
+  }
 }

JS Changes

 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.nextRow = [];
     this.fallSpeed = 3000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
     
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
       leaderboardLoading: document.getElementById('leaderboard-loading')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
   }
 
+  createParticleEffect(blockElement) {
+    const rect = blockElement.getBoundingClientRect();
+    const gridRect = this.elements.gameGrid.getBoundingClientRect();
+    
+    for (let i = 0; i < 3; i++) {
+      const particle = document.createElement('div');
+      particle.className = 'particle';
+      particle.textContent = '✨';
+      
+      const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
+      const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
+      
+      particle.style.left = offsetX + 'px';
+      particle.style.top = offsetY + 'px';
+      particle.style.animationDelay = (i * 100) + 'ms';
+      
+      this.elements.gameGrid.appendChild(particle);
+      
+      setTimeout(() => {
+        if (particle.parentNode) {
+          particle.parentNode.removeChild(particle);
+        }
+      }, 1000);
+    }
+  }
+
   bindEvents() {
     this.elements.startGameBtn.addEventListener('click', () => this.startGame());
     this.elements.pauseBtn.addEventListener('click', () => this.pauseGame());
     this.elements.backToMenuBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.resumeBtn.addEventListener('click', () => this.resumeGame());
     this.elements.quitBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.playAgainBtn.addEventListener('click', () => this.startGame());
     this.elements.viewLeaderboardBtn.addEventListener('click', () => this.showLeaderboard());
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.fallSpeed = 3000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if adjacent horizontally
       if (row === selectedRow && Math.abs(col - selectedCol) === 1) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
+    // Add swap animation
+    const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
+    const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
-    const temp = this.grid[row1][col1];
-    this.grid[row1][col1] = this.grid[row2][col2];
-    this.grid[row2][col2] = temp;
     
+    if (col2 > col1) {
+      block1.classList.add('swapping-right');
+      block2.classList.add('swapping-left');
+    } else {
+      block1.classList.add('swapping-left');
+      block2.classList.add('swapping-right');
+    }
+    
+    setTimeout(() => {
+      const temp = this.grid[row1][col1];
+      this.grid[row1][col1] = this.grid[row2][col2];
+      this.grid[row2][col2] = temp;
+      
+      this.renderGrid();
+      this.checkMatches();
+    }, 150);
-    this.renderGrid();
-    this.checkMatches();
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
       this.removeMatches(matches);
       this.applyGravity();
       setTimeout(() => this.checkMatches(), 300);
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
   removeMatches(matches) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
+    // Add matched animation before removing
+    matches.forEach(({ row, col }, index) => {
+      const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
+      if (blockElement) {
+        setTimeout(() => {
+          blockElement.classList.add('matched');
+          if (this.combo >= 3) {
+            this.createParticleEffect(blockElement);
+          }
+        }, index * 50);
+      }
-    matches.forEach(({ row, col }) => {
-      this.grid[row][col] = null;
     });
     
+    // Remove blocks after animation
+    setTimeout(() => {
+      matches.forEach(({ row, col }) => {
+        this.grid[row][col] = null;
+      });
+      this.updateDisplay();
+    }, 250);
-    this.updateDisplay();
     
     if (this.combo > 1) {
       this.elements.comboText.textContent = `${this.combo}x COMBO!`;
+      if (this.combo >= 7) {
-      if (this.combo >= 5) {
         document.getElementById('app').classList.add('screen-shake');
+        setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
-        setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 500);
       }
     } else {
       this.elements.comboText.textContent = '';
     }
   }
 
   applyGravity() {
+    const fallingBlocks = [];
+    
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
         }
       }
       
+      // Track which blocks will fall
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
+        if (this.grid[row][col] !== null) {
+          const newRow = this.GRID_HEIGHT - column.length + column.findIndex(block => block === this.grid[row][col]);
+          if (newRow > row) {
+            fallingBlocks.push({ col, oldRow: row, newRow });
+          }
+        }
         this.grid[row][col] = null;
       }
       
       for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
     this.renderGrid();
+    
+    // Add gravity animation
+    fallingBlocks.forEach((block, index) => {
+      const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
+      if (blockElement) {
+        setTimeout(() => {
+          blockElement.classList.add('gravity-fall');
+          setTimeout(() => blockElement.classList.remove('gravity-fall'), 300);
+        }, index * 30);
+      }
+    });
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
     this.renderGrid();
+    
+    // Add new row animation
+    for (let col = 0; col < this.GRID_WIDTH; col++) {
+      const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
+      if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
+        setTimeout(() => {
+          blockElement.classList.add('new-row');
+          setTimeout(() => blockElement.classList.remove('new-row'), 400);
+        }, col * 50);
+      }
+    }
+    
+    setTimeout(() => this.checkMatches(), 500);
-    this.checkMatches();
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
       this.fallSpeed = Math.max(1000, 3000 - (this.level - 1) * 200);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.showLeaderboard();
   }
 
   async gameOver() {
     this.gameRunning = false;
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
     if (this.currentUser) {
       await this.saveScore();
       const position = await this.getScorePosition();
       this.elements.scoreMessage.textContent = position <= 20 ? 
         `🎉 You made it to #${position} on the leaderboard!` : 
         `Your best score has been saved!`;
       this.elements.anonymousMessage.style.display = 'none';
     } else {
       this.elements.scoreMessage.textContent = '';
       this.elements.anonymousMessage.style.display = 'block';
     }
     
     this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
 });

Preview

Falco

Can you make it playable via keyboard?

Web-Artifact-Creator-bot
Thinking...The user wants to add keyboard controls to the Connect 3 game. I should analyze what keyboard controls would make sense for this type of game:
  1. Arrow keys to move a cursor around the grid
  2. Space or Enter to select/swap blocks
  3. Maybe WASD as alternative to arrow keys
  4. Escape to pause/unpause the game
  5. Maybe R to restart

I need to:

  1. Add a visual cursor that shows the current selected position
  2. Add keyboard event listeners
  3. Handle cursor movement with bounds checking
  4. Handle selection and swapping with keyboard
  5. Handle pause/unpause with Escape
  6. Make sure keyboard controls work during gameplay

The cursor should be visually distinct from the selected block for swapping - maybe a different colored border or glow effect.

Change Description
Add full keyboard controls to make the game playable without mouse:

**Keyboard Controls:**
- Arrow keys (or WASD) to move cursor around the grid
- Space or Enter to select/swap blocks at cursor position
- Escape to pause/unpause game
- R to restart game when game over
- Tab to cycle through UI buttons when not in game

**Visual Cursor System:**
- Add a bright colored cursor outline that shows current keyboard position
- Cursor should be visually distinct from selected block (different color/style)
- Cursor should wrap around edges of grid (top to bottom, left to right)
- Show cursor position in a different style than selection highlight

**Implementation:**
- Add cursor tracking (cursorRow, cursorCol) to game state
- Add keyboard event listeners for keydown events
- Update CSS with new .cursor class for visual feedback
- Make cursor visible with bright border (e.g., cyan/blue glow)
- Update block selection logic to work with both mouse and keyboard
- Add visual indicators showing available keyboard controls
- Prevent default browser behavior for game keys during gameplay

**CSS Changes:**
- Add .cursor class with bright border and glow effect
- Make cursor visually distinct from .selected class
- Add smooth transitions for cursor movement

**JavaScript Changes:**
- Add cursorRow, cursorCol properties to track keyboard cursor
- Add handleKeyDown method for all keyboard inputs
- Update selection system to work with cursor position
- Add cursor positioning and rendering methods
- Handle keyboard navigation during different game states

**UI Updates:**
- Add small keyboard hints in game footer
- Show current keyboard controls context-sensitively
View Changes

HTML Changes

 <div id="app">
   <!-- Leaderboard Screen -->
   <div id="leaderboard-screen" class="screen active">
     <div class="header">
       <h1>🧩 Connect 3 Puzzle</h1>
       <p>Match 3 or more blocks to clear them and create combos!</p>
     </div>
     
     <div class="leaderboard-container">
       <h2>🏆 Top Scores</h2>
       <div id="leaderboard-loading">Loading leaderboard...</div>
       <table id="leaderboard-table" style="display: none;">
         <thead>
           <tr>
             <th>Rank</th>
             <th>Player</th>
             <th>Score</th>
             <th>Date</th>
           </tr>
         </thead>
         <tbody id="leaderboard-body">
         </tbody>
       </table>
     </div>
     
     <button id="start-game-btn" class="primary-button">🎮 Start New Game</button>
   </div>
 
   <!-- Game Screen -->
   <div id="game-screen" class="screen">
     <div class="game-header">
       <div class="game-info">
         <div class="score-display">
           <span class="label">Score:</span>
           <span id="current-score">0</span>
         </div>
         <div class="level-display">
           <span class="label">Level:</span>
           <span id="current-level">1</span>
         </div>
         <div class="combo-display">
           <span id="combo-text"></span>
         </div>
       </div>
       <div class="game-controls">
         <button id="pause-btn" class="control-button">⏸️</button>
         <button id="back-to-menu-btn" class="control-button">🏠</button>
       </div>
     </div>
 
     <div class="game-container">
       <div id="game-grid" class="game-grid">
         <!-- Grid will be generated by JavaScript -->
       </div>
     </div>
 
     <div class="game-footer">
       <div class="next-preview">
         <span class="label">Next Row:</span>
         <div id="next-row-preview" class="next-row">
           <!-- Preview will be generated by JavaScript -->
         </div>
       </div>
+      <div class="keyboard-hints">
+        <div class="hint-row">
+          <span>🎮 <strong>Arrow Keys:</strong> Move cursor</span>
+          <span><strong>Space/Enter:</strong> Select/Swap</span>
+        </div>
+        <div class="hint-row">
+          <span><strong>Esc:</strong> Pause</span>
+          <span><strong>R:</strong> Restart (when game over)</span>
+        </div>
+      </div>
     </div>
   </div>
 
   <!-- Game Over Modal -->
   <div id="game-over-modal" class="modal">
     <div class="modal-content">
       <h2>🎯 Game Over!</h2>
       <div class="final-score">
         <span class="label">Final Score:</span>
         <span id="final-score-value">0</span>
       </div>
       <div id="score-message"></div>
       <div id="anonymous-message" style="display: none;">
         <p>Log in to save your score!</p>
         <a href="https://meta.discourse.org" target="_blank" class="login-link">Login to Discourse</a>
       </div>
       <div class="modal-buttons">
         <button id="play-again-btn" class="primary-button">🔄 Play Again</button>
         <button id="view-leaderboard-btn" class="secondary-button">🏆 View Leaderboard</button>
       </div>
     </div>
   </div>
 
   <!-- Pause Modal -->
   <div id="pause-modal" class="modal">
     <div class="modal-content">
       <h2>⏸️ Game Paused</h2>
       <div class="modal-buttons">
         <button id="resume-btn" class="primary-button">▶️ Resume</button>
         <button id="quit-btn" class="secondary-button">🏠 Main Menu</button>
       </div>
     </div>
   </div>
 </div>

CSS Changes

 #app {
   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
   width: 100%;
   max-width: 600px;
   height: 600px;
   margin: 0 auto;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 12px;
   box-shadow: 0 10px 30px rgba(0,0,0,0.3);
   overflow: hidden;
   position: relative;
 }
 
 .screen {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: none;
   flex-direction: column;
   padding: 15px;
   box-sizing: border-box;
 }
 
 .screen.active {
   display: flex;
 }
 
 .header {
   text-align: center;
   margin-bottom: 20px;
 }
 
 .header h1 {
   color: white;
   font-size: 1.8em;
   margin: 0;
   text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 }
 
 .header p {
   color: rgba(255,255,255,0.9);
   margin: 5px 0 0 0;
   font-size: 0.95em;
 }
 
 .leaderboard-container {
   flex: 1;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 15px;
   margin-bottom: 15px;
   overflow-y: auto;
 }
 
 .leaderboard-container h2 {
   margin: 0 0 15px 0;
   color: #333;
   text-align: center;
 }
 
 #leaderboard-table {
   width: 100%;
   border-collapse: collapse;
   font-size: 0.9em;
 }
 
 #leaderboard-table th,
 #leaderboard-table td {
   padding: 8px 12px;
   text-align: left;
   border-bottom: 1px solid #eee;
 }
 
 #leaderboard-table th {
   background: #f8f9fa;
   font-weight: 600;
   color: #555;
 }
 
 #leaderboard-table tr:nth-child(even) {
   background: #f8f9fa;
 }
 
 .rank-medal {
   font-size: 1.2em;
 }
 
 .primary-button {
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border: none;
   color: white;
   font-size: 1.2em;
   font-weight: 600;
   padding: 15px 30px;
   border-radius: 25px;
   cursor: pointer;
   box-shadow: 0 4px 15px rgba(0,0,0,0.2);
   transition: all 0.3s ease;
 }
 
 .primary-button:hover {
   transform: translateY(-2px);
   box-shadow: 0 6px 20px rgba(0,0,0,0.3);
 }
 
 .secondary-button {
   background: rgba(255,255,255,0.2);
   border: 2px solid rgba(255,255,255,0.5);
   color: white;
   font-size: 1em;
   font-weight: 600;
   padding: 10px 20px;
   border-radius: 20px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .secondary-button:hover {
   background: rgba(255,255,255,0.3);
   border-color: rgba(255,255,255,0.8);
 }
 
 .game-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-bottom: 8px;
 }
 
 .game-info {
   display: flex;
   gap: 20px;
   flex-wrap: wrap;
 }
 
 .score-display, .level-display {
   font-weight: 600;
   color: #333;
 }
 
 .label {
   color: #666;
   margin-right: 5px;
 }
 
 #current-score, #current-level {
   color: #ff6b6b;
   font-size: 1.2em;
 }
 
 .combo-display {
   font-weight: bold;
   color: #feca57;
   min-width: 80px;
 }
 
 .game-controls {
   display: flex;
   gap: 10px;
 }
 
 .control-button {
   background: rgba(255,255,255,0.2);
   border: none;
   color: white;
   font-size: 1.5em;
   padding: 8px 12px;
   border-radius: 8px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .control-button:hover {
   background: rgba(255,255,255,0.3);
 }
 
 .game-container {
   flex: 1;
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 10px 0;
 }
 
 .game-grid {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   grid-template-rows: repeat(12, 1fr);
   gap: 2px;
   background: rgba(0,0,0,0.3);
   border-radius: 8px;
   padding: 6px;
   width: 240px;
   height: 320px;
 }
 
 .block {
   background: rgba(255,255,255,0.9);
   border-radius: 4px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.4em;
   cursor: pointer;
   transition: all 0.2s ease;
   user-select: none;
   border: 2px solid transparent;
   position: relative;
 }
 
 .block:hover {
   transform: scale(1.05);
   box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 }
 
 .block.selected {
   border-color: #feca57;
   box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
 }
 
+.block.cursor {
+  border-color: #00d4ff;
+  box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
+  animation: cursor-pulse 1.5s ease-in-out infinite;
+}
+
+.block.cursor.selected {
+  border-color: #ff6b6b;
+  box-shadow: 0 0 15px rgba(255, 107, 107, 0.8), 0 0 25px rgba(0, 212, 255, 0.6);
+}
+
+@keyframes cursor-pulse {
+  0%, 100% { 
+    box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
+  }
+  50% { 
+    box-shadow: 0 0 25px rgba(0, 212, 255, 1), inset 0 0 15px rgba(0, 212, 255, 0.5);
+  }
+}
+
 .block.falling {
   animation: fall 0.4s ease-out;
 }
 
 .block.matched {
   animation: matched-pop 0.5s ease-out forwards;
 }
 
 .block.swapping-right {
   animation: swap-right 0.3s ease-in-out;
 }
 
 .block.swapping-left {
   animation: swap-left 0.3s ease-in-out;
 }
 
 .block.new-row {
   animation: slide-up 0.4s ease-out;
 }
 
 .block.gravity-fall {
   animation: gravity-drop 0.3s ease-in;
 }
 
 @keyframes fall {
   from { 
     transform: translateY(-100%);
     opacity: 0.7;
   }
   to { 
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes matched-pop {
   0% { 
     transform: scale(1);
     opacity: 1;
   }
   30% { 
     transform: scale(1.4) rotate(10deg);
     opacity: 1;
     background: linear-gradient(45deg, #ff6b6b, #feca57);
   }
   70% { 
     transform: scale(1.6) rotate(-5deg);
     opacity: 0.8;
   }
   100% { 
     transform: scale(0) rotate(180deg);
     opacity: 0;
   }
 }
 
 @keyframes swap-right {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(100% + 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-left {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(-100% - 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes slide-up {
   from {
     transform: translateY(100%);
     opacity: 0.7;
   }
   to {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes gravity-drop {
   from {
     transform: translateY(-100%);
   }
   to {
     transform: translateY(0);
   }
 }
 
 .game-footer {
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-top: 8px;
 }
 
+.keyboard-hints {
+  margin-top: 10px;
+  font-size: 0.8em;
+  color: #666;
+}
+
+.hint-row {
+  display: flex;
+  justify-content: space-between;
+  margin: 2px 0;
+  gap: 15px;
+}
+
+.hint-row span {
+  flex: 1;
+}
+
+.hint-row strong {
+  color: #333;
+}
+
 .next-row {
   display: flex;
   gap: 5px;
   margin-top: 5px;
 }
 
 .next-block {
   width: 30px;
   height: 30px;
   border-radius: 4px;
   background: rgba(255,255,255,0.9);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.2em;
   border: 1px solid #ddd;
 }
 
 .modal {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0,0,0,0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 1000;
 }
 
 .modal.active {
   display: flex;
 }
 
 .modal-content {
   background: white;
   border-radius: 12px;
   padding: 30px;
   text-align: center;
   max-width: 400px;
   width: 90%;
   box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 }
 
 .modal-content h2 {
   margin: 0 0 20px 0;
   color: #333;
 }
 
 .final-score {
   margin: 20px 0;
   font-size: 1.3em;
 }
 
 #final-score-value {
   color: #ff6b6b;
   font-weight: bold;
   font-size: 1.5em;
 }
 
 .modal-buttons {
   display: flex;
   gap: 15px;
   justify-content: center;
   margin-top: 25px;
   flex-wrap: wrap;
 }
 
 .login-link {
   display: inline-block;
   background: #0088cc;
   color: white;
   padding: 10px 20px;
   border-radius: 20px;
   text-decoration: none;
   margin: 10px 0;
   transition: background 0.3s ease;
 }
 
 .login-link:hover {
   background: #0066aa;
 }
 
 #leaderboard-loading {
   text-align: center;
   color: #666;
   font-style: italic;
   padding: 40px;
 }
 
 #score-message {
   margin: 15px 0;
   font-weight: 600;
   color: #333;
 }
 
 .screen-shake {
   animation: shake 0.3s ease-in-out;
 }
 
 @keyframes shake {
   0%, 100% { transform: translateX(0) translateY(0); }
   25% { transform: translateX(-2px) translateY(-1px); }
   50% { transform: translateX(2px) translateY(1px); }
   75% { transform: translateX(-1px) translateY(-2px); }
 }
 
 .combo-particles {
   position: absolute;
   pointer-events: none;
   width: 100%;
   height: 100%;
 }
 
 .particle {
   position: absolute;
   font-size: 1.2em;
   animation: particle-burst 1s ease-out forwards;
   pointer-events: none;
 }
 
 @keyframes particle-burst {
   0% {
     opacity: 1;
     transform: scale(0.5);
   }
   50% {
     opacity: 0.8;
     transform: scale(1.2);
   }
   100% {
     opacity: 0;
     transform: scale(0.3) translateY(-50px);
   }
 }
 
 @media (max-width: 480px) {
   #app {
     border-radius: 0;
     height: 100vh;
     max-width: 100%;
   }
   
   .screen {
     padding: 10px;
   }
   
   .game-grid {
     width: 200px;
     height: 280px;
   }
   
   .block {
     font-size: 1.2em;
   }
   
   .game-info {
     font-size: 0.85em;
   }
   
   .modal-content {
     width: 95%;
     padding: 20px;
   }
   
   .header h1 {
     font-size: 1.5em;
   }
   
   .header p {
     font-size: 0.85em;
   }
 }

JS Changes

 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
+    this.cursorRow = 5;
+    this.cursorCol = 2;
     this.nextRow = [];
     this.fallSpeed = 3000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
     
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
       leaderboardLoading: document.getElementById('leaderboard-loading')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
+    this.updateCursor();
   }
 
   createParticleEffect(blockElement) {
     const rect = blockElement.getBoundingClientRect();
     const gridRect = this.elements.gameGrid.getBoundingClientRect();
     
     for (let i = 0; i < 3; i++) {
       const particle = document.createElement('div');
       particle.className = 'particle';
       particle.textContent = '✨';
       
       const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
       const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
       
       particle.style.left = offsetX + 'px';
       particle.style.top = offsetY + 'px';
       particle.style.animationDelay = (i * 100) + 'ms';
       
       this.elements.gameGrid.appendChild(particle);
       
       setTimeout(() => {
         if (particle.parentNode) {
           particle.parentNode.removeChild(particle);
         }
       }, 1000);
     }
   }
 
   bindEvents() {
     this.elements.startGameBtn.addEventListener('click', () => this.startGame());
     this.elements.pauseBtn.addEventListener('click', () => this.pauseGame());
     this.elements.backToMenuBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.resumeBtn.addEventListener('click', () => this.resumeGame());
     this.elements.quitBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.playAgainBtn.addEventListener('click', () => this.startGame());
     this.elements.viewLeaderboardBtn.addEventListener('click', () => this.showLeaderboard());
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
+    
+    // Add keyboard event listener
+    document.addEventListener('keydown', (e) => this.handleKeyDown(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
+    this.cursorRow = 5;
+    this.cursorCol = 2;
     this.fallSpeed = 3000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if adjacent horizontally
       if (row === selectedRow && Math.abs(col - selectedCol) === 1) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
     // Add swap animation
     const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
     const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
     
     if (col2 > col1) {
       block1.classList.add('swapping-right');
       block2.classList.add('swapping-left');
     } else {
       block1.classList.add('swapping-left');
       block2.classList.add('swapping-right');
     }
     
     setTimeout(() => {
       const temp = this.grid[row1][col1];
       this.grid[row1][col1] = this.grid[row2][col2];
       this.grid[row2][col2] = temp;
       
       this.renderGrid();
       this.checkMatches();
     }, 150);
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
+  updateCursor() {
+    const blocks = this.elements.gameGrid.querySelectorAll('.block');
+    blocks.forEach(block => block.classList.remove('cursor'));
+    
+    if (this.gameRunning && !this.gamePaused) {
+      const index = this.cursorRow * this.GRID_WIDTH + this.cursorCol;
+      blocks[index]?.classList.add('cursor');
+    }
+  }
+
+  moveCursor(deltaRow, deltaCol) {
+    this.cursorRow = (this.cursorRow + deltaRow + this.GRID_HEIGHT) % this.GRID_HEIGHT;
+    this.cursorCol = (this.cursorCol + deltaCol + this.GRID_WIDTH) % this.GRID_WIDTH;
+    this.updateCursor();
+  }
+
+  handleKeyDown(e) {
+    // Handle different game states
+    if (this.elements.gameOverModal.classList.contains('active')) {
+      this.handleGameOverKeys(e);
+      return;
+    }
+    
+    if (this.elements.pauseModal.classList.contains('active')) {
+      this.handlePauseKeys(e);
+      return;
+    }
+    
+    if (this.elements.leaderboardScreen.classList.contains('active')) {
+      this.handleMenuKeys(e);
+      return;
+    }
+    
+    if (this.gameRunning) {
+      this.handleGameKeys(e);
+    }
+  }
+
+  handleGameKeys(e) {
+    switch(e.key.toLowerCase()) {
+      case 'arrowup':
+      case 'w':
+        e.preventDefault();
+        this.moveCursor(-1, 0);
+        break;
+      case 'arrowdown':
+      case 's':
+        e.preventDefault();
+        this.moveCursor(1, 0);
+        break;
+      case 'arrowleft':
+      case 'a':
+        e.preventDefault();
+        this.moveCursor(0, -1);
+        break;
+      case 'arrowright':
+      case 'd':
+        e.preventDefault();
+        this.moveCursor(0, 1);
+        break;
+      case ' ':
+      case 'enter':
+        e.preventDefault();
+        this.handleCursorSelect();
+        break;
+      case 'escape':
+        e.preventDefault();
+        if (this.gamePaused) {
+          this.resumeGame();
+        } else {
+          this.pauseGame();
+        }
+        break;
+    }
+  }
+
+  handleGameOverKeys(e) {
+    switch(e.key.toLowerCase()) {
+      case 'r':
+        e.preventDefault();
+        this.startGame();
+        break;
+      case 'escape':
+      case 'enter':
+        e.preventDefault();
+        this.showLeaderboard();
+        break;
+    }
+  }
+
+  handlePauseKeys(e) {
+    switch(e.key.toLowerCase()) {
+      case 'escape':
+      case 'enter':
+      case ' ':
+        e.preventDefault();
+        this.resumeGame();
+        break;
+      case 'q':
+        e.preventDefault();
+        this.quitToMenu();
+        break;
+    }
+  }
+
+  handleMenuKeys(e) {
+    switch(e.key.toLowerCase()) {
+      case 'enter':
+      case ' ':
+        e.preventDefault();
+        this.startGame();
+        break;
+    }
+  }
+
+  handleCursorSelect() {
+    if (!this.gameRunning || this.gamePaused) return;
+    
+    // Check if there's a block at cursor position
+    if (!this.grid[this.cursorRow][this.cursorCol]) return;
+    
+    if (this.selectedBlock) {
+      const selectedRow = this.selectedBlock.row;
+      const selectedCol = this.selectedBlock.col;
+      
+      // Check if cursor is on selected block (deselect)
+      if (this.cursorRow === selectedRow && this.cursorCol === selectedCol) {
+        this.clearSelection();
+        return;
+      }
+      
+      // Check if adjacent horizontally
+      if (this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) {
+        this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
+      }
+      
+      this.clearSelection();
+    } else {
+      this.selectedBlock = { row: this.cursorRow, col: this.cursorCol };
+      this.updateSelection();
+    }
+  }
+
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
       this.removeMatches(matches);
       this.applyGravity();
       setTimeout(() => this.checkMatches(), 300);
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
   removeMatches(matches) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
     // Add matched animation before removing
     matches.forEach(({ row, col }, index) => {
       const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('matched');
           if (this.combo >= 3) {
             this.createParticleEffect(blockElement);
           }
         }, index * 50);
       }
     });
     
     // Remove blocks after animation
     setTimeout(() => {
       matches.forEach(({ row, col }) => {
         this.grid[row][col] = null;
       });
       this.updateDisplay();
     }, 250);
     
     if (this.combo > 1) {
       this.elements.comboText.textContent = `${this.combo}x COMBO!`;
       if (this.combo >= 7) {
         document.getElementById('app').classList.add('screen-shake');
         setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
       }
     } else {
       this.elements.comboText.textContent = '';
     }
   }
 
   applyGravity() {
     const fallingBlocks = [];
     
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
         }
       }
       
       // Track which blocks will fall
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] !== null) {
           const newRow = this.GRID_HEIGHT - column.length + column.findIndex(block => block === this.grid[row][col]);
           if (newRow > row) {
             fallingBlocks.push({ col, oldRow: row, newRow });
           }
         }
         this.grid[row][col] = null;
       }
       
       for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
     this.renderGrid();
     
     // Add gravity animation
     fallingBlocks.forEach((block, index) => {
       const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('gravity-fall');
           setTimeout(() => blockElement.classList.remove('gravity-fall'), 300);
         }, index * 30);
       }
     });
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
     this.renderGrid();
     
     // Add new row animation
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
       if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
         setTimeout(() => {
           blockElement.classList.add('new-row');
           setTimeout(() => blockElement.classList.remove('new-row'), 400);
         }, col * 50);
       }
     }
     
     setTimeout(() => this.checkMatches(), 500);
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
       this.fallSpeed = Math.max(1000, 3000 - (this.level - 1) * 200);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
+    this.updateCursor(); // Hide cursor when paused
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
+    this.updateCursor(); // Show cursor when resumed
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
+    this.updateCursor(); // Hide cursor when quitting
     this.showLeaderboard();
   }
 
   async gameOver() {
     this.gameRunning = false;
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
     if (this.currentUser) {
       await this.saveScore();
       const position = await this.getScorePosition();
       this.elements.scoreMessage.textContent = position <= 20 ? 
         `🎉 You made it to #${position} on the leaderboard!` : 
         `Your best score has been saved!`;
       this.elements.anonymousMessage.style.display = 'none';
     } else {
       this.elements.scoreMessage.textContent = '';
       this.elements.anonymousMessage.style.display = 'block';
     }
     
     this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
 });

Preview

Falco

It should have gravity, so when a block is matched and disappears, blocks above it should fall.

Web-Artifact-Creator-bot
Thinking...Looking at the current code, I can see there is gravity implementation in the `applyGravity()` method, but the user is saying it should have gravity. Let me look more closely...

In the applyGravity() method, it does seem to handle gravity by:

  1. Going through each column
  2. Collecting non-null blocks from bottom to top
  3. Clearing the grid
  4. Placing the collected blocks at the bottom

And it’s called in removeMatches() after a250ms delay, and then checkMatches() is called again after300ms.

But maybe the issue is that the gravity isn’t working properly or visibly enough. Let me check the flow:

  1. checkMatches() finds matches
  2. If matches found, calls removeMatches()
  3. removeMatches() adds animation to matched blocks
  4. After 250ms, removes the blocks and calls updateDisplay()
  5. Then calls applyGravity()
  6. applyGravity() moves blocks down and calls renderGrid()
  7. Then after 300ms from removeMatches(), calls checkMatches() again

I think the issue might be that applyGravity() is called inside the setTimeout of removeMatches(), so it happens at the same time as the blocks are removed. This might not be visible.

Let me restructure this to make gravity more apparent:

  1. Remove matched blocks first
  2. Then apply gravity with a delay
  3. Then check for new matches
Change Description
Fix the gravity system to make it more visible and working properly:

**Gravity Flow Issues:**
- Currently gravity happens immediately after blocks are removed, making it hard to see
- Need to separate the removal and gravity steps with proper timing
- Gravity animations need to be more prominent

**Changes to implement:**

1. **Restructure the match checking flow:**
   - Remove matched blocks first with animation
   - Wait for removal animation to complete
   - Then apply gravity with visible falling animation
   - Wait for gravity to complete
   - Then check for new matches

2. **Improve applyGravity() method:**
   - Add more visible falling animations
   - Make blocks actually appear to fall from their original positions
   - Add staggered timing so blocks don't all fall at exactly the same time
   - Use better visual feedback for the falling motion

3. **Fix the timing in removeMatches():**
   - Remove the immediate applyGravity() call
   - Instead, call it after the matched blocks have been removed and rendered
   - Add proper delays between each step

4. **Improve gravity animations:**
   - Make the gravity-fall animation more pronounced (longer duration, more visible movement)
   - Add different delays for blocks at different heights
   - Make sure blocks appear to fall from their original position to new position

5. **Update the checkMatches() flow:**
   - After removeMatches(), wait for removal animation
   - Then call applyGravity() separately
   - Then wait for gravity animation before checking matches again

The goal is to make it clear that:
1. Matched blocks disappear
2. Blocks above fall down to fill the gaps
3. New matches are then checked
View Changes

JS Changes

 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.nextRow = [];
     this.fallSpeed = 3000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
     
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
       leaderboardLoading: document.getElementById('leaderboard-loading')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
     this.updateCursor();
   }
 
   createParticleEffect(blockElement) {
     const rect = blockElement.getBoundingClientRect();
     const gridRect = this.elements.gameGrid.getBoundingClientRect();
     
     for (let i = 0; i < 3; i++) {
       const particle = document.createElement('div');
       particle.className = 'particle';
       particle.textContent = '✨';
       
       const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
       const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
       
       particle.style.left = offsetX + 'px';
       particle.style.top = offsetY + 'px';
       particle.style.animationDelay = (i * 100) + 'ms';
       
       this.elements.gameGrid.appendChild(particle);
       
       setTimeout(() => {
         if (particle.parentNode) {
           particle.parentNode.removeChild(particle);
         }
       }, 1000);
     }
   }
 
   bindEvents() {
     this.elements.startGameBtn.addEventListener('click', () => this.startGame());
     this.elements.pauseBtn.addEventListener('click', () => this.pauseGame());
     this.elements.backToMenuBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.resumeBtn.addEventListener('click', () => this.resumeGame());
     this.elements.quitBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.playAgainBtn.addEventListener('click', () => this.startGame());
     this.elements.viewLeaderboardBtn.addEventListener('click', () => this.showLeaderboard());
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
     
     // Add keyboard event listener
     document.addEventListener('keydown', (e) => this.handleKeyDown(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.fallSpeed = 3000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if adjacent horizontally
       if (row === selectedRow && Math.abs(col - selectedCol) === 1) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
     // Add swap animation
     const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
     const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
     
     if (col2 > col1) {
       block1.classList.add('swapping-right');
       block2.classList.add('swapping-left');
     } else {
       block1.classList.add('swapping-left');
       block2.classList.add('swapping-right');
     }
     
     setTimeout(() => {
       const temp = this.grid[row1][col1];
       this.grid[row1][col1] = this.grid[row2][col2];
       this.grid[row2][col2] = temp;
       
       this.renderGrid();
       this.checkMatches();
     }, 150);
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
   updateCursor() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('cursor'));
     
     if (this.gameRunning && !this.gamePaused) {
       const index = this.cursorRow * this.GRID_WIDTH + this.cursorCol;
       blocks[index]?.classList.add('cursor');
     }
   }
 
   moveCursor(deltaRow, deltaCol) {
     this.cursorRow = (this.cursorRow + deltaRow + this.GRID_HEIGHT) % this.GRID_HEIGHT;
     this.cursorCol = (this.cursorCol + deltaCol + this.GRID_WIDTH) % this.GRID_WIDTH;
     this.updateCursor();
   }
 
   handleKeyDown(e) {
     // Handle different game states
     if (this.elements.gameOverModal.classList.contains('active')) {
       this.handleGameOverKeys(e);
       return;
     }
     
     if (this.elements.pauseModal.classList.contains('active')) {
       this.handlePauseKeys(e);
       return;
     }
     
     if (this.elements.leaderboardScreen.classList.contains('active')) {
       this.handleMenuKeys(e);
       return;
     }
     
     if (this.gameRunning) {
       this.handleGameKeys(e);
     }
   }
 
   handleGameKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'arrowup':
       case 'w':
         e.preventDefault();
         this.moveCursor(-1, 0);
         break;
       case 'arrowdown':
       case 's':
         e.preventDefault();
         this.moveCursor(1, 0);
         break;
       case 'arrowleft':
       case 'a':
         e.preventDefault();
         this.moveCursor(0, -1);
         break;
       case 'arrowright':
       case 'd':
         e.preventDefault();
         this.moveCursor(0, 1);
         break;
       case ' ':
       case 'enter':
         e.preventDefault();
         this.handleCursorSelect();
         break;
       case 'escape':
         e.preventDefault();
         if (this.gamePaused) {
           this.resumeGame();
         } else {
           this.pauseGame();
         }
         break;
     }
   }
 
   handleGameOverKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'r':
         e.preventDefault();
         this.startGame();
         break;
       case 'escape':
       case 'enter':
         e.preventDefault();
         this.showLeaderboard();
         break;
     }
   }
 
   handlePauseKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'escape':
       case 'enter':
       case ' ':
         e.preventDefault();
         this.resumeGame();
         break;
       case 'q':
         e.preventDefault();
         this.quitToMenu();
         break;
     }
   }
 
   handleMenuKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'enter':
       case ' ':
         e.preventDefault();
         this.startGame();
         break;
     }
   }
 
   handleCursorSelect() {
     if (!this.gameRunning || this.gamePaused) return;
     
     // Check if there's a block at cursor position
     if (!this.grid[this.cursorRow][this.cursorCol]) return;
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if cursor is on selected block (deselect)
       if (this.cursorRow === selectedRow && this.cursorCol === selectedCol) {
         this.clearSelection();
         return;
       }
       
       // Check if adjacent horizontally
       if (this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) {
         this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row: this.cursorRow, col: this.cursorCol };
       this.updateSelection();
     }
   }
 
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
+      this.removeMatches(matches, () => {
+        // After blocks are removed, apply gravity
+        setTimeout(() => {
+          this.applyGravity(() => {
+            // After gravity is complete, check for new matches
+            setTimeout(() => this.checkMatches(), 100);
+          });
+        }, 100);
+      });
-      this.removeMatches(matches);
-      this.applyGravity();
-      setTimeout(() => this.checkMatches(), 300);
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
+  removeMatches(matches, callback) {
-  removeMatches(matches) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
     // Add matched animation before removing
     matches.forEach(({ row, col }, index) => {
       const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('matched');
           if (this.combo >= 3) {
             this.createParticleEffect(blockElement);
           }
         }, index * 50);
       }
     });
     
+    // Remove blocks after animation and call callback
-    // Remove blocks after animation
     setTimeout(() => {
       matches.forEach(({ row, col }) => {
         this.grid[row][col] = null;
       });
+      this.renderGrid();
       this.updateDisplay();
+      
+      if (this.combo > 1) {
+        this.elements.comboText.textContent = `${this.combo}x COMBO!`;
+        if (this.combo >= 7) {
+          document.getElementById('app').classList.add('screen-shake');
+          setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
+        }
+      } else {
+        this.elements.comboText.textContent = '';
-    }, 250);
-    
-    if (this.combo > 1) {
-      this.elements.comboText.textContent = `${this.combo}x COMBO!`;
-      if (this.combo >= 7) {
-        document.getElementById('app').classList.add('screen-shake');
-        setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
       }
+      
+      if (callback) callback();
+    }, 500);
-    } else {
-      this.elements.comboText.textContent = '';
-    }
   }
 
+  applyGravity(callback) {
-  applyGravity() {
     const fallingBlocks = [];
+    const columnData = [];
     
+    // Analyze each column and track falling blocks
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
+      const originalPositions = [];
+      
+      // Collect non-null blocks and their original positions
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
+          originalPositions.push(row);
         }
       }
       
+      columnData[col] = { blocks: column, originalPositions };
+      
+      // Clear the column
-      // Track which blocks will fall
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
-        if (this.grid[row][col] !== null) {
-          const newRow = this.GRID_HEIGHT - column.length + column.findIndex(block => block === this.grid[row][col]);
-          if (newRow > row) {
-            fallingBlocks.push({ col, oldRow: row, newRow });
-          }
-        }
         this.grid[row][col] = null;
       }
       
+      // Track falling blocks with distance information
       for (let i = 0; i < column.length; i++) {
+        const oldRow = originalPositions[i];
+        const newRow = this.GRID_HEIGHT - 1 - i;
+        
+        if (newRow > oldRow) {
+          fallingBlocks.push({ 
+            col, 
+            oldRow, 
+            newRow, 
+            block: column[i],
+            fallDistance: newRow - oldRow
+          });
+        }
+      }
+      
+      // Place blocks in their new positions
+      for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
+    // If no blocks need to fall, just call callback
+    if (fallingBlocks.length === 0) {
+      this.renderGrid();
+      if (callback) callback();
+      return;
+    }
+    
+    // Render grid first
     this.renderGrid();
     
+    // Apply falling animations with staggered timing
+    let maxAnimationTime = 0;
-    // Add gravity animation
     fallingBlocks.forEach((block, index) => {
       const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
       if (blockElement) {
+        // Stagger animation start based on column and fall distance
+        const startDelay = (block.col * 50) + (block.fallDistance * 20);
+        const animationDuration = 400 + (block.fallDistance * 100);
+        
+        maxAnimationTime = Math.max(maxAnimationTime, startDelay + animationDuration);
+        
         setTimeout(() => {
           blockElement.classList.add('gravity-fall');
+          setTimeout(() => {
+            blockElement.classList.remove('gravity-fall');
+          }, animationDuration);
+        }, startDelay);
-          setTimeout(() => blockElement.classList.remove('gravity-fall'), 300);
-        }, index * 30);
       }
     });
+    
+    // Call callback after all animations complete
+    if (callback) {
+      setTimeout(callback, maxAnimationTime + 100);
+    }
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
     this.renderGrid();
     
     // Add new row animation
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
       if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
         setTimeout(() => {
           blockElement.classList.add('new-row');
           setTimeout(() => blockElement.classList.remove('new-row'), 400);
         }, col * 50);
       }
     }
     
+    setTimeout(() => this.checkMatches(), 600);
-    setTimeout(() => this.checkMatches(), 500);
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
       this.fallSpeed = Math.max(1000, 3000 - (this.level - 1) * 200);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
     this.updateCursor(); // Hide cursor when paused
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
     this.updateCursor(); // Show cursor when resumed
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.updateCursor(); // Hide cursor when quitting
     this.showLeaderboard();
   }
 
   async gameOver() {
     this.gameRunning = false;
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
     if (this.currentUser) {
       await this.saveScore();
       const position = await this.getScorePosition();
       this.elements.scoreMessage.textContent = position <= 20 ? 
         `🎉 You made it to #${position} on the leaderboard!` : 
         `Your best score has been saved!`;
       this.elements.anonymousMessage.style.display = 'none';
     } else {
       this.elements.scoreMessage.textContent = '';
       this.elements.anonymousMessage.style.display = 'block';
     }
     
     this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
 });

Preview

Falco

I can seen to swap pieces vertically, only horizontally?

Web-Artifact-Creator-bot
Thinking...The user is right - looking at the code, I can see that in the `swapBlocks` and `handleCursorSelect` methods, it only checks for horizontal adjacency:
// Check if adjacent horizontally
if (row === selectedRow && Math.abs(col - selectedCol) === 1) {
  this.swapBlocks(selectedRow, selectedCol, row, col);
}

and

// Check if adjacent horizontally
if (this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) {
  this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
}

This means you can only swap blocks that are next to each other horizontally (left-right), but not vertically (up-down). In games like Tetris Attack/Panel de Pon, you should be able to swap both horizontally and vertically adjacent blocks.

I need to update the adjacency check to allow both horizontal and vertical swaps.

Change Description
Fix the block swapping to allow both horizontal AND vertical swaps:

**Current Issue:**
- Can currently only swap blocks horizontally (left-right)
- Need to also allow vertical swaps (up-down)

**Changes needed:**

1. **Update adjacency check in `handleBlockClick`:**
   - Change the condition from only horizontal to check both horizontal and vertical adjacency
   - Allow swapping if blocks are adjacent either horizontally OR vertically

2. **Update adjacency check in `handleCursorSelect`:**
   - Same fix for keyboard controls
   - Allow vertical and horizontal swaps

3. **Update swap animations:**
   - Add new CSS animations for vertical swaps (swapping-up, swapping-down)
   - Update the swapBlocks method to detect direction and apply appropriate animation

4. **Fix the adjacency logic:**
   - Current: `row === selectedRow && Math.abs(col - selectedCol) === 1` (horizontal only)
   - New: `(row === selectedRow && Math.abs(col - selectedCol) === 1) || (col === selectedCol && Math.abs(row - selectedRow) === 1)` (horizontal OR vertical)

**CSS additions:**
- Add @keyframes for swapping-up and swapping-down animations
- Make sure vertical swap animations move blocks up/down instead of left/right

**JavaScript changes:**
- Update both click and keyboard selection methods
- Update swapBlocks to detect vertical vs horizontal swaps and apply correct animations
View Changes

CSS Changes

 #app {
   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
   width: 100%;
   max-width: 600px;
   height: 600px;
   margin: 0 auto;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 12px;
   box-shadow: 0 10px 30px rgba(0,0,0,0.3);
   overflow: hidden;
   position: relative;
 }
 
 .screen {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: none;
   flex-direction: column;
   padding: 15px;
   box-sizing: border-box;
 }
 
 .screen.active {
   display: flex;
 }
 
 .header {
   text-align: center;
   margin-bottom: 20px;
 }
 
 .header h1 {
   color: white;
   font-size: 1.8em;
   margin: 0;
   text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 }
 
 .header p {
   color: rgba(255,255,255,0.9);
   margin: 5px 0 0 0;
   font-size: 0.95em;
 }
 
 .leaderboard-container {
   flex: 1;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 15px;
   margin-bottom: 15px;
   overflow-y: auto;
 }
 
 .leaderboard-container h2 {
   margin: 0 0 15px 0;
   color: #333;
   text-align: center;
 }
 
 #leaderboard-table {
   width: 100%;
   border-collapse: collapse;
   font-size: 0.9em;
 }
 
 #leaderboard-table th,
 #leaderboard-table td {
   padding: 8px 12px;
   text-align: left;
   border-bottom: 1px solid #eee;
 }
 
 #leaderboard-table th {
   background: #f8f9fa;
   font-weight: 600;
   color: #555;
 }
 
 #leaderboard-table tr:nth-child(even) {
   background: #f8f9fa;
 }
 
 .rank-medal {
   font-size: 1.2em;
 }
 
 .primary-button {
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border: none;
   color: white;
   font-size: 1.2em;
   font-weight: 600;
   padding: 15px 30px;
   border-radius: 25px;
   cursor: pointer;
   box-shadow: 0 4px 15px rgba(0,0,0,0.2);
   transition: all 0.3s ease;
 }
 
 .primary-button:hover {
   transform: translateY(-2px);
   box-shadow: 0 6px 20px rgba(0,0,0,0.3);
 }
 
 .secondary-button {
   background: rgba(255,255,255,0.2);
   border: 2px solid rgba(255,255,255,0.5);
   color: white;
   font-size: 1em;
   font-weight: 600;
   padding: 10px 20px;
   border-radius: 20px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .secondary-button:hover {
   background: rgba(255,255,255,0.3);
   border-color: rgba(255,255,255,0.8);
 }
 
 .game-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-bottom: 8px;
 }
 
 .game-info {
   display: flex;
   gap: 20px;
   flex-wrap: wrap;
 }
 
 .score-display, .level-display {
   font-weight: 600;
   color: #333;
 }
 
 .label {
   color: #666;
   margin-right: 5px;
 }
 
 #current-score, #current-level {
   color: #ff6b6b;
   font-size: 1.2em;
 }
 
 .combo-display {
   font-weight: bold;
   color: #feca57;
   min-width: 80px;
 }
 
 .game-controls {
   display: flex;
   gap: 10px;
 }
 
 .control-button {
   background: rgba(255,255,255,0.2);
   border: none;
   color: white;
   font-size: 1.5em;
   padding: 8px 12px;
   border-radius: 8px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .control-button:hover {
   background: rgba(255,255,255,0.3);
 }
 
 .game-container {
   flex: 1;
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 10px 0;
 }
 
 .game-grid {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   grid-template-rows: repeat(12, 1fr);
   gap: 2px;
   background: rgba(0,0,0,0.3);
   border-radius: 8px;
   padding: 6px;
   width: 240px;
   height: 320px;
 }
 
 .block {
   background: rgba(255,255,255,0.9);
   border-radius: 4px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.4em;
   cursor: pointer;
   transition: all 0.2s ease;
   user-select: none;
   border: 2px solid transparent;
   position: relative;
 }
 
 .block:hover {
   transform: scale(1.05);
   box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 }
 
 .block.selected {
   border-color: #feca57;
   box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
 }
 
 .block.cursor {
   border-color: #00d4ff;
   box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   animation: cursor-pulse 1.5s ease-in-out infinite;
 }
 
 .block.cursor.selected {
   border-color: #ff6b6b;
   box-shadow: 0 0 15px rgba(255, 107, 107, 0.8), 0 0 25px rgba(0, 212, 255, 0.6);
 }
 
 @keyframes cursor-pulse {
   0%, 100% { 
     box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   }
   50% { 
     box-shadow: 0 0 25px rgba(0, 212, 255, 1), inset 0 0 15px rgba(0, 212, 255, 0.5);
   }
 }
 
 .block.falling {
   animation: fall 0.4s ease-out;
 }
 
 .block.matched {
   animation: matched-pop 0.5s ease-out forwards;
 }
 
 .block.swapping-right {
   animation: swap-right 0.3s ease-in-out;
 }
 
 .block.swapping-left {
   animation: swap-left 0.3s ease-in-out;
 }
 
+.block.swapping-up {
+  animation: swap-up 0.3s ease-in-out;
+}
+
+.block.swapping-down {
+  animation: swap-down 0.3s ease-in-out;
+}
+
 .block.new-row {
   animation: slide-up 0.4s ease-out;
 }
 
 .block.gravity-fall {
   animation: gravity-drop 0.3s ease-in;
 }
 
 @keyframes fall {
   from { 
     transform: translateY(-100%);
     opacity: 0.7;
   }
   to { 
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes matched-pop {
   0% { 
     transform: scale(1);
     opacity: 1;
   }
   30% { 
     transform: scale(1.4) rotate(10deg);
     opacity: 1;
     background: linear-gradient(45deg, #ff6b6b, #feca57);
   }
   70% { 
     transform: scale(1.6) rotate(-5deg);
     opacity: 0.8;
   }
   100% { 
     transform: scale(0) rotate(180deg);
     opacity: 0;
   }
 }
 
 @keyframes swap-right {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(100% + 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-left {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(-100% - 2px)); }
   100% { transform: translateX(0); }
 }
 
+@keyframes swap-up {
+  0% { transform: translateY(0); }
+  50% { transform: translateY(calc(-100% - 2px)); }
+  100% { transform: translateY(0); }
+}
+
+@keyframes swap-down {
+  0% { transform: translateY(0); }
+  50% { transform: translateY(calc(100% + 2px)); }
+  100% { transform: translateY(0); }
+}
+
 @keyframes slide-up {
   from {
     transform: translateY(100%);
     opacity: 0.7;
   }
   to {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes gravity-drop {
   from {
     transform: translateY(-100%);
   }
   to {
     transform: translateY(0);
   }
 }
 
 .game-footer {
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-top: 8px;
 }
 
 .keyboard-hints {
   margin-top: 10px;
   font-size: 0.8em;
   color: #666;
 }
 
 .hint-row {
   display: flex;
   justify-content: space-between;
   margin: 2px 0;
   gap: 15px;
 }
 
 .hint-row span {
   flex: 1;
 }
 
 .hint-row strong {
   color: #333;
 }
 
 .next-row {
   display: flex;
   gap: 5px;
   margin-top: 5px;
 }
 
 .next-block {
   width: 30px;
   height: 30px;
   border-radius: 4px;
   background: rgba(255,255,255,0.9);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.2em;
   border: 1px solid #ddd;
 }
 
 .modal {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0,0,0,0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 1000;
 }
 
 .modal.active {
   display: flex;
 }
 
 .modal-content {
   background: white;
   border-radius: 12px;
   padding: 30px;
   text-align: center;
   max-width: 400px;
   width: 90%;
   box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 }
 
 .modal-content h2 {
   margin: 0 0 20px 0;
   color: #333;
 }
 
 .final-score {
   margin: 20px 0;
   font-size: 1.3em;
 }
 
 #final-score-value {
   color: #ff6b6b;
   font-weight: bold;
   font-size: 1.5em;
 }
 
 .modal-buttons {
   display: flex;
   gap: 15px;
   justify-content: center;
   margin-top: 25px;
   flex-wrap: wrap;
 }
 
 .login-link {
   display: inline-block;
   background: #0088cc;
   color: white;
   padding: 10px 20px;
   border-radius: 20px;
   text-decoration: none;
   margin: 10px 0;
   transition: background 0.3s ease;
 }
 
 .login-link:hover {
   background: #0066aa;
 }
 
 #leaderboard-loading {
   text-align: center;
   color: #666;
   font-style: italic;
   padding: 40px;
 }
 
 #score-message {
   margin: 15px 0;
   font-weight: 600;
   color: #333;
 }
 
 .screen-shake {
   animation: shake 0.3s ease-in-out;
 }
 
 @keyframes shake {
   0%, 100% { transform: translateX(0) translateY(0); }
   25% { transform: translateX(-2px) translateY(-1px); }
   50% { transform: translateX(2px) translateY(1px); }
   75% { transform: translateX(-1px) translateY(-2px); }
 }
 
 .combo-particles {
   position: absolute;
   pointer-events: none;
   width: 100%;
   height: 100%;
 }
 
 .particle {
   position: absolute;
   font-size: 1.2em;
   animation: particle-burst 1s ease-out forwards;
   pointer-events: none;
 }
 
 @keyframes particle-burst {
   0% {
     opacity: 1;
     transform: scale(0.5);
   }
   50% {
     opacity: 0.8;
     transform: scale(1.2);
   }
   100% {
     opacity: 0;
     transform: scale(0.3) translateY(-50px);
   }
 }
 
 @media (max-width: 480px) {
   #app {
     border-radius: 0;
     height: 100vh;
     max-width: 100%;
   }
   
   .screen {
     padding: 10px;
   }
   
   .game-grid {
     width: 200px;
     height: 280px;
   }
   
   .block {
     font-size: 1.2em;
   }
   
   .game-info {
     font-size: 0.85em;
   }
   
   .modal-content {
     width: 95%;
     padding: 20px;
   }
   
   .header h1 {
     font-size: 1.5em;
   }
   
   .header p {
     font-size: 0.85em;
   }
 }

JS Changes

 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.nextRow = [];
     this.fallSpeed = 3000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
     
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
       leaderboardLoading: document.getElementById('leaderboard-loading')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
     this.updateCursor();
   }
 
   createParticleEffect(blockElement) {
     const rect = blockElement.getBoundingClientRect();
     const gridRect = this.elements.gameGrid.getBoundingClientRect();
     
     for (let i = 0; i < 3; i++) {
       const particle = document.createElement('div');
       particle.className = 'particle';
       particle.textContent = '✨';
       
       const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
       const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
       
       particle.style.left = offsetX + 'px';
       particle.style.top = offsetY + 'px';
       particle.style.animationDelay = (i * 100) + 'ms';
       
       this.elements.gameGrid.appendChild(particle);
       
       setTimeout(() => {
         if (particle.parentNode) {
           particle.parentNode.removeChild(particle);
         }
       }, 1000);
     }
   }
 
   bindEvents() {
     this.elements.startGameBtn.addEventListener('click', () => this.startGame());
     this.elements.pauseBtn.addEventListener('click', () => this.pauseGame());
     this.elements.backToMenuBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.resumeBtn.addEventListener('click', () => this.resumeGame());
     this.elements.quitBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.playAgainBtn.addEventListener('click', () => this.startGame());
     this.elements.viewLeaderboardBtn.addEventListener('click', () => this.showLeaderboard());
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
     
     // Add keyboard event listener
     document.addEventListener('keydown', (e) => this.handleKeyDown(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.fallSpeed = 3000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
+      // Check if adjacent horizontally or vertically
+      if ((row === selectedRow && Math.abs(col - selectedCol) === 1) || 
+          (col === selectedCol && Math.abs(row - selectedRow) === 1)) {
-      // Check if adjacent horizontally
-      if (row === selectedRow && Math.abs(col - selectedCol) === 1) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
     // Add swap animation
     const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
     const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
     
+    // Determine swap direction and apply appropriate animations
+    if (row1 === row2) {
+      // Horizontal swap
+      if (col2 > col1) {
+        block1.classList.add('swapping-right');
+        block2.classList.add('swapping-left');
+      } else {
+        block1.classList.add('swapping-left');
+        block2.classList.add('swapping-right');
+      }
-    if (col2 > col1) {
-      block1.classList.add('swapping-right');
-      block2.classList.add('swapping-left');
     } else {
+      // Vertical swap
+      if (row2 > row1) {
+        block1.classList.add('swapping-down');
+        block2.classList.add('swapping-up');
+      } else {
+        block1.classList.add('swapping-up');
+        block2.classList.add('swapping-down');
+      }
-      block1.classList.add('swapping-left');
-      block2.classList.add('swapping-right');
     }
     
     setTimeout(() => {
       const temp = this.grid[row1][col1];
       this.grid[row1][col1] = this.grid[row2][col2];
       this.grid[row2][col2] = temp;
       
       this.renderGrid();
       this.checkMatches();
     }, 150);
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
   updateCursor() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('cursor'));
     
     if (this.gameRunning && !this.gamePaused) {
       const index = this.cursorRow * this.GRID_WIDTH + this.cursorCol;
       blocks[index]?.classList.add('cursor');
     }
   }
 
   moveCursor(deltaRow, deltaCol) {
     this.cursorRow = (this.cursorRow + deltaRow + this.GRID_HEIGHT) % this.GRID_HEIGHT;
     this.cursorCol = (this.cursorCol + deltaCol + this.GRID_WIDTH) % this.GRID_WIDTH;
     this.updateCursor();
   }
 
   handleKeyDown(e) {
     // Handle different game states
     if (this.elements.gameOverModal.classList.contains('active')) {
       this.handleGameOverKeys(e);
       return;
     }
     
     if (this.elements.pauseModal.classList.contains('active')) {
       this.handlePauseKeys(e);
       return;
     }
     
     if (this.elements.leaderboardScreen.classList.contains('active')) {
       this.handleMenuKeys(e);
       return;
     }
     
     if (this.gameRunning) {
       this.handleGameKeys(e);
     }
   }
 
   handleGameKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'arrowup':
       case 'w':
         e.preventDefault();
         this.moveCursor(-1, 0);
         break;
       case 'arrowdown':
       case 's':
         e.preventDefault();
         this.moveCursor(1, 0);
         break;
       case 'arrowleft':
       case 'a':
         e.preventDefault();
         this.moveCursor(0, -1);
         break;
       case 'arrowright':
       case 'd':
         e.preventDefault();
         this.moveCursor(0, 1);
         break;
       case ' ':
       case 'enter':
         e.preventDefault();
         this.handleCursorSelect();
         break;
       case 'escape':
         e.preventDefault();
         if (this.gamePaused) {
           this.resumeGame();
         } else {
           this.pauseGame();
         }
         break;
     }
   }
 
   handleGameOverKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'r':
         e.preventDefault();
         this.startGame();
         break;
       case 'escape':
       case 'enter':
         e.preventDefault();
         this.showLeaderboard();
         break;
     }
   }
 
   handlePauseKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'escape':
       case 'enter':
       case ' ':
         e.preventDefault();
         this.resumeGame();
         break;
       case 'q':
         e.preventDefault();
         this.quitToMenu();
         break;
     }
   }
 
   handleMenuKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'enter':
       case ' ':
         e.preventDefault();
         this.startGame();
         break;
     }
   }
 
   handleCursorSelect() {
     if (!this.gameRunning || this.gamePaused) return;
     
     // Check if there's a block at cursor position
     if (!this.grid[this.cursorRow][this.cursorCol]) return;
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if cursor is on selected block (deselect)
       if (this.cursorRow === selectedRow && this.cursorCol === selectedCol) {
         this.clearSelection();
         return;
       }
       
+      // Check if adjacent horizontally or vertically
+      if ((this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) || 
+          (this.cursorCol === selectedCol && Math.abs(this.cursorRow - selectedRow) === 1)) {
-      // Check if adjacent horizontally
-      if (this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) {
         this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row: this.cursorRow, col: this.cursorCol };
       this.updateSelection();
     }
   }
 
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
       this.removeMatches(matches, () => {
         // After blocks are removed, apply gravity
         setTimeout(() => {
           this.applyGravity(() => {
             // After gravity is complete, check for new matches
             setTimeout(() => this.checkMatches(), 100);
           });
         }, 100);
       });
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
   removeMatches(matches, callback) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
     // Add matched animation before removing
     matches.forEach(({ row, col }, index) => {
       const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('matched');
           if (this.combo >= 3) {
             this.createParticleEffect(blockElement);
           }
         }, index * 50);
       }
     });
     
     // Remove blocks after animation and call callback
     setTimeout(() => {
       matches.forEach(({ row, col }) => {
         this.grid[row][col] = null;
       });
       this.renderGrid();
       this.updateDisplay();
       
       if (this.combo > 1) {
         this.elements.comboText.textContent = `${this.combo}x COMBO!`;
         if (this.combo >= 7) {
           document.getElementById('app').classList.add('screen-shake');
           setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
         }
       } else {
         this.elements.comboText.textContent = '';
       }
       
       if (callback) callback();
     }, 500);
   }
 
   applyGravity(callback) {
     const fallingBlocks = [];
     const columnData = [];
     
     // Analyze each column and track falling blocks
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
       const originalPositions = [];
       
       // Collect non-null blocks and their original positions
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
           originalPositions.push(row);
         }
       }
       
       columnData[col] = { blocks: column, originalPositions };
       
       // Clear the column
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
         this.grid[row][col] = null;
       }
       
       // Track falling blocks with distance information
       for (let i = 0; i < column.length; i++) {
         const oldRow = originalPositions[i];
         const newRow = this.GRID_HEIGHT - 1 - i;
         
         if (newRow > oldRow) {
           fallingBlocks.push({ 
             col, 
             oldRow, 
             newRow, 
             block: column[i],
             fallDistance: newRow - oldRow
           });
         }
       }
       
       // Place blocks in their new positions
       for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
     // If no blocks need to fall, just call callback
     if (fallingBlocks.length === 0) {
       this.renderGrid();
       if (callback) callback();
       return;
     }
     
     // Render grid first
     this.renderGrid();
     
     // Apply falling animations with staggered timing
     let maxAnimationTime = 0;
     fallingBlocks.forEach((block, index) => {
       const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
       if (blockElement) {
         // Stagger animation start based on column and fall distance
         const startDelay = (block.col * 50) + (block.fallDistance * 20);
         const animationDuration = 400 + (block.fallDistance * 100);
         
         maxAnimationTime = Math.max(maxAnimationTime, startDelay + animationDuration);
         
         setTimeout(() => {
           blockElement.classList.add('gravity-fall');
           setTimeout(() => {
             blockElement.classList.remove('gravity-fall');
           }, animationDuration);
         }, startDelay);
       }
     });
     
     // Call callback after all animations complete
     if (callback) {
       setTimeout(callback, maxAnimationTime + 100);
     }
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
     this.renderGrid();
     
     // Add new row animation
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
       if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
         setTimeout(() => {
           blockElement.classList.add('new-row');
           setTimeout(() => blockElement.classList.remove('new-row'), 400);
         }, col * 50);
       }
     }
     
     setTimeout(() => this.checkMatches(), 600);
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
       this.fallSpeed = Math.max(1000, 3000 - (this.level - 1) * 200);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
     this.updateCursor(); // Hide cursor when paused
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
     this.updateCursor(); // Show cursor when resumed
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.updateCursor(); // Hide cursor when quitting
     this.showLeaderboard();
   }
 
   async gameOver() {
     this.gameRunning = false;
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
     if (this.currentUser) {
       await this.saveScore();
       const position = await this.getScorePosition();
       this.elements.scoreMessage.textContent = position <= 20 ? 
         `🎉 You made it to #${position} on the leaderboard!` : 
         `Your best score has been saved!`;
       this.elements.anonymousMessage.style.display = 'none';
     } else {
       this.elements.scoreMessage.textContent = '';
       this.elements.anonymousMessage.style.display = 'block';
     }
     
     this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
 });

Preview

Falco

Make it grow a tad slower please. Also the animation is a bit janky.

Web-Artifact-Creator-bot
Thinking...The user wants me to: 1. Make the game grow slower (the new rows being added from the bottom) 2. Fix the janky animation

Looking at the current code, I can see:

  • The fallSpeed starts at 3000ms and decreases by 200ms per level
  • The new row animation and gravity animations seem to have some timing issues

For making it grow slower, I should:

  • Increase the initial fallSpeed
  • Reduce how much it decreases per level

For fixing janky animations:

  • The new row animation timing might conflict with other animations
  • The gravity animation timing might need adjustment
  • The checkMatches timing after adding new rows might be causing conflicts
Change Description
Fix the game speed and animation jankiness:

**Speed Changes:**
- Increase initial fallSpeed from 3000ms to 5000ms (much slower start)
- Reduce speed increase per level from 200ms to 100ms per level
- Set minimum fallSpeed to 2000ms instead of 1000ms

**Animation Improvements:**
1. **Fix timing conflicts in addNewRow():**
   - Wait for all new row animations to complete before checking matches
   - Add proper delays between row shifting, rendering, and match checking
   - Stagger the new row block animations more smoothly

2. **Improve gravity animations:**
   - Make gravity fall animation smoother and more consistent
   - Fix timing between gravity completion and next match check
   - Ensure no overlapping animations

3. **Fix animation sequencing:**
   - In checkMatches: ensure proper sequence of remove → gravity → check again
   - Add better animation completion detection
   - Prevent rapid-fire animation conflicts

**Specific Changes:**
- Update fallSpeed calculation in constructor and gameLoop
- Fix addNewRow timing to prevent animation conflicts
- Improve applyGravity callback timing
- Smooth out the new-row slide-up animation
- Add animation state tracking to prevent overlapping actions

**CSS Animation Tweaks:**
- Make slide-up animation smoother
- Adjust gravity-drop animation timing
- Ensure consistent animation durations
View Changes

CSS Changes

 #app {
   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
   width: 100%;
   max-width: 600px;
   height: 600px;
   margin: 0 auto;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 12px;
   box-shadow: 0 10px 30px rgba(0,0,0,0.3);
   overflow: hidden;
   position: relative;
 }
 
 .screen {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: none;
   flex-direction: column;
   padding: 15px;
   box-sizing: border-box;
 }
 
 .screen.active {
   display: flex;
 }
 
 .header {
   text-align: center;
   margin-bottom: 20px;
 }
 
 .header h1 {
   color: white;
   font-size: 1.8em;
   margin: 0;
   text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 }
 
 .header p {
   color: rgba(255,255,255,0.9);
   margin: 5px 0 0 0;
   font-size: 0.95em;
 }
 
 .leaderboard-container {
   flex: 1;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 15px;
   margin-bottom: 15px;
   overflow-y: auto;
 }
 
 .leaderboard-container h2 {
   margin: 0 0 15px 0;
   color: #333;
   text-align: center;
 }
 
 #leaderboard-table {
   width: 100%;
   border-collapse: collapse;
   font-size: 0.9em;
 }
 
 #leaderboard-table th,
 #leaderboard-table td {
   padding: 8px 12px;
   text-align: left;
   border-bottom: 1px solid #eee;
 }
 
 #leaderboard-table th {
   background: #f8f9fa;
   font-weight: 600;
   color: #555;
 }
 
 #leaderboard-table tr:nth-child(even) {
   background: #f8f9fa;
 }
 
 .rank-medal {
   font-size: 1.2em;
 }
 
 .primary-button {
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border: none;
   color: white;
   font-size: 1.2em;
   font-weight: 600;
   padding: 15px 30px;
   border-radius: 25px;
   cursor: pointer;
   box-shadow: 0 4px 15px rgba(0,0,0,0.2);
   transition: all 0.3s ease;
 }
 
 .primary-button:hover {
   transform: translateY(-2px);
   box-shadow: 0 6px 20px rgba(0,0,0,0.3);
 }
 
 .secondary-button {
   background: rgba(255,255,255,0.2);
   border: 2px solid rgba(255,255,255,0.5);
   color: white;
   font-size: 1em;
   font-weight: 600;
   padding: 10px 20px;
   border-radius: 20px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .secondary-button:hover {
   background: rgba(255,255,255,0.3);
   border-color: rgba(255,255,255,0.8);
 }
 
 .game-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-bottom: 8px;
 }
 
 .game-info {
   display: flex;
   gap: 20px;
   flex-wrap: wrap;
 }
 
 .score-display, .level-display {
   font-weight: 600;
   color: #333;
 }
 
 .label {
   color: #666;
   margin-right: 5px;
 }
 
 #current-score, #current-level {
   color: #ff6b6b;
   font-size: 1.2em;
 }
 
 .combo-display {
   font-weight: bold;
   color: #feca57;
   min-width: 80px;
 }
 
 .game-controls {
   display: flex;
   gap: 10px;
 }
 
 .control-button {
   background: rgba(255,255,255,0.2);
   border: none;
   color: white;
   font-size: 1.5em;
   padding: 8px 12px;
   border-radius: 8px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .control-button:hover {
   background: rgba(255,255,255,0.3);
 }
 
 .game-container {
   flex: 1;
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 10px 0;
 }
 
 .game-grid {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   grid-template-rows: repeat(12, 1fr);
   gap: 2px;
   background: rgba(0,0,0,0.3);
   border-radius: 8px;
   padding: 6px;
   width: 240px;
   height: 320px;
 }
 
 .block {
   background: rgba(255,255,255,0.9);
   border-radius: 4px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.4em;
   cursor: pointer;
   transition: all 0.2s ease;
   user-select: none;
   border: 2px solid transparent;
   position: relative;
 }
 
 .block:hover {
   transform: scale(1.05);
   box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 }
 
 .block.selected {
   border-color: #feca57;
   box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
 }
 
 .block.cursor {
   border-color: #00d4ff;
   box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   animation: cursor-pulse 1.5s ease-in-out infinite;
 }
 
 .block.cursor.selected {
   border-color: #ff6b6b;
   box-shadow: 0 0 15px rgba(255, 107, 107, 0.8), 0 0 25px rgba(0, 212, 255, 0.6);
 }
 
 @keyframes cursor-pulse {
   0%, 100% { 
     box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   }
   50% { 
     box-shadow: 0 0 25px rgba(0, 212, 255, 1), inset 0 0 15px rgba(0, 212, 255, 0.5);
   }
 }
 
 .block.falling {
   animation: fall 0.4s ease-out;
 }
 
 .block.matched {
   animation: matched-pop 0.5s ease-out forwards;
 }
 
 .block.swapping-right {
   animation: swap-right 0.3s ease-in-out;
 }
 
 .block.swapping-left {
   animation: swap-left 0.3s ease-in-out;
 }
 
 .block.swapping-up {
   animation: swap-up 0.3s ease-in-out;
 }
 
 .block.swapping-down {
   animation: swap-down 0.3s ease-in-out;
 }
 
 .block.new-row {
   animation: slide-up 0.4s ease-out;
 }
 
 .block.gravity-fall {
   animation: gravity-drop 0.3s ease-in;
 }
 
 @keyframes fall {
   from { 
     transform: translateY(-100%);
     opacity: 0.7;
   }
   to { 
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes matched-pop {
   0% { 
     transform: scale(1);
     opacity: 1;
   }
   30% { 
     transform: scale(1.4) rotate(10deg);
     opacity: 1;
     background: linear-gradient(45deg, #ff6b6b, #feca57);
   }
   70% { 
     transform: scale(1.6) rotate(-5deg);
     opacity: 0.8;
   }
   100% { 
     transform: scale(0) rotate(180deg);
     opacity: 0;
   }
 }
 
 @keyframes swap-right {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(100% + 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-left {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(-100% - 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-up {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(-100% - 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes swap-down {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(100% + 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes slide-up {
+  0% {
-  from {
     transform: translateY(100%);
+    opacity: 0.6;
-    opacity: 0.7;
   }
+  30% {
+    transform: translateY(50%);
+    opacity: 0.8;
+  }
+  100% {
-  to {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes gravity-drop {
+  0% {
-  from {
     transform: translateY(-100%);
+    opacity: 0.8;
   }
+  60% {
+    transform: translateY(10%);
+    opacity: 0.9;
+  }
+  100% {
-  to {
     transform: translateY(0);
+    opacity: 1;
   }
 }
 
 .game-footer {
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-top: 8px;
 }
 
 .keyboard-hints {
   margin-top: 10px;
   font-size: 0.8em;
   color: #666;
 }
 
 .hint-row {
   display: flex;
   justify-content: space-between;
   margin: 2px 0;
   gap: 15px;
 }
 
 .hint-row span {
   flex: 1;
 }
 
 .hint-row strong {
   color: #333;
 }
 
 .next-row {
   display: flex;
   gap: 5px;
   margin-top: 5px;
 }
 
 .next-block {
   width: 30px;
   height: 30px;
   border-radius: 4px;
   background: rgba(255,255,255,0.9);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.2em;
   border: 1px solid #ddd;
 }
 
 .modal {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0,0,0,0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 1000;
 }
 
 .modal.active {
   display: flex;
 }
 
 .modal-content {
   background: white;
   border-radius: 12px;
   padding: 30px;
   text-align: center;
   max-width: 400px;
   width: 90%;
   box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 }
 
 .modal-content h2 {
   margin: 0 0 20px 0;
   color: #333;
 }
 
 .final-score {
   margin: 20px 0;
   font-size: 1.3em;
 }
 
 #final-score-value {
   color: #ff6b6b;
   font-weight: bold;
   font-size: 1.5em;
 }
 
 .modal-buttons {
   display: flex;
   gap: 15px;
   justify-content: center;
   margin-top: 25px;
   flex-wrap: wrap;
 }
 
 .login-link {
   display: inline-block;
   background: #0088cc;
   color: white;
   padding: 10px 20px;
   border-radius: 20px;
   text-decoration: none;
   margin: 10px 0;
   transition: background 0.3s ease;
 }
 
 .login-link:hover {
   background: #0066aa;
 }
 
 #leaderboard-loading {
   text-align: center;
   color: #666;
   font-style: italic;
   padding: 40px;
 }
 
 #score-message {
   margin: 15px 0;
   font-weight: 600;
   color: #333;
 }
 
 .screen-shake {
   animation: shake 0.3s ease-in-out;
 }
 
 @keyframes shake {
   0%, 100% { transform: translateX(0) translateY(0); }
   25% { transform: translateX(-2px) translateY(-1px); }
   50% { transform: translateX(2px) translateY(1px); }
   75% { transform: translateX(-1px) translateY(-2px); }
 }
 
 .combo-particles {
   position: absolute;
   pointer-events: none;
   width: 100%;
   height: 100%;
 }
 
 .particle {
   position: absolute;
   font-size: 1.2em;
   animation: particle-burst 1s ease-out forwards;
   pointer-events: none;
 }
 
 @keyframes particle-burst {
   0% {
     opacity: 1;
     transform: scale(0.5);
   }
   50% {
     opacity: 0.8;
     transform: scale(1.2);
   }
   100% {
     opacity: 0;
     transform: scale(0.3) translateY(-50px);
   }
 }
 
 @media (max-width: 480px) {
   #app {
     border-radius: 0;
     height: 100vh;
     max-width: 100%;
   }
   
   .screen {
     padding: 10px;
   }
   
   .game-grid {
     width: 200px;
     height: 280px;
   }
   
   .block {
     font-size: 1.2em;
   }
   
   .game-info {
     font-size: 0.85em;
   }
   
   .modal-content {
     width: 95%;
     padding: 20px;
   }
   
   .header h1 {
     font-size: 1.5em;
   }
   
   .header p {
     font-size: 0.85em;
   }
 }

JS Changes

 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.nextRow = [];
+    this.fallSpeed = 5000; // milliseconds
-    this.fallSpeed = 3000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
     
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
       leaderboardLoading: document.getElementById('leaderboard-loading')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
     this.updateCursor();
   }
 
   createParticleEffect(blockElement) {
     const rect = blockElement.getBoundingClientRect();
     const gridRect = this.elements.gameGrid.getBoundingClientRect();
     
     for (let i = 0; i < 3; i++) {
       const particle = document.createElement('div');
       particle.className = 'particle';
       particle.textContent = '✨';
       
       const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
       const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
       
       particle.style.left = offsetX + 'px';
       particle.style.top = offsetY + 'px';
       particle.style.animationDelay = (i * 100) + 'ms';
       
       this.elements.gameGrid.appendChild(particle);
       
       setTimeout(() => {
         if (particle.parentNode) {
           particle.parentNode.removeChild(particle);
         }
       }, 1000);
     }
   }
 
   bindEvents() {
     this.elements.startGameBtn.addEventListener('click', () => this.startGame());
     this.elements.pauseBtn.addEventListener('click', () => this.pauseGame());
     this.elements.backToMenuBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.resumeBtn.addEventListener('click', () => this.resumeGame());
     this.elements.quitBtn.addEventListener('click', () => this.quitToMenu());
     this.elements.playAgainBtn.addEventListener('click', () => this.startGame());
     this.elements.viewLeaderboardBtn.addEventListener('click', () => this.showLeaderboard());
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
     
     // Add keyboard event listener
     document.addEventListener('keydown', (e) => this.handleKeyDown(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
+    this.fallSpeed = 5000;
-    this.fallSpeed = 3000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if adjacent horizontally or vertically
       if ((row === selectedRow && Math.abs(col - selectedCol) === 1) || 
           (col === selectedCol && Math.abs(row - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
     // Add swap animation
     const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
     const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
     
     // Determine swap direction and apply appropriate animations
     if (row1 === row2) {
       // Horizontal swap
       if (col2 > col1) {
         block1.classList.add('swapping-right');
         block2.classList.add('swapping-left');
       } else {
         block1.classList.add('swapping-left');
         block2.classList.add('swapping-right');
       }
     } else {
       // Vertical swap
       if (row2 > row1) {
         block1.classList.add('swapping-down');
         block2.classList.add('swapping-up');
       } else {
         block1.classList.add('swapping-up');
         block2.classList.add('swapping-down');
       }
     }
     
     setTimeout(() => {
       const temp = this.grid[row1][col1];
       this.grid[row1][col1] = this.grid[row2][col2];
       this.grid[row2][col2] = temp;
       
       this.renderGrid();
       this.checkMatches();
     }, 150);
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
   updateCursor() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('cursor'));
     
     if (this.gameRunning && !this.gamePaused) {
       const index = this.cursorRow * this.GRID_WIDTH + this.cursorCol;
       blocks[index]?.classList.add('cursor');
     }
   }
 
   moveCursor(deltaRow, deltaCol) {
     this.cursorRow = (this.cursorRow + deltaRow + this.GRID_HEIGHT) % this.GRID_HEIGHT;
     this.cursorCol = (this.cursorCol + deltaCol + this.GRID_WIDTH) % this.GRID_WIDTH;
     this.updateCursor();
   }
 
   handleKeyDown(e) {
     // Handle different game states
     if (this.elements.gameOverModal.classList.contains('active')) {
       this.handleGameOverKeys(e);
       return;
     }
     
     if (this.elements.pauseModal.classList.contains('active')) {
       this.handlePauseKeys(e);
       return;
     }
     
     if (this.elements.leaderboardScreen.classList.contains('active')) {
       this.handleMenuKeys(e);
       return;
     }
     
     if (this.gameRunning) {
       this.handleGameKeys(e);
     }
   }
 
   handleGameKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'arrowup':
       case 'w':
         e.preventDefault();
         this.moveCursor(-1, 0);
         break;
       case 'arrowdown':
       case 's':
         e.preventDefault();
         this.moveCursor(1, 0);
         break;
       case 'arrowleft':
       case 'a':
         e.preventDefault();
         this.moveCursor(0, -1);
         break;
       case 'arrowright':
       case 'd':
         e.preventDefault();
         this.moveCursor(0, 1);
         break;
       case ' ':
       case 'enter':
         e.preventDefault();
         this.handleCursorSelect();
         break;
       case 'escape':
         e.preventDefault();
         if (this.gamePaused) {
           this.resumeGame();
         } else {
           this.pauseGame();
         }
         break;
     }
   }
 
   handleGameOverKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'r':
         e.preventDefault();
         this.startGame();
         break;
       case 'escape':
       case 'enter':
         e.preventDefault();
         this.showLeaderboard();
         break;
     }
   }
 
   handlePauseKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'escape':
       case 'enter':
       case ' ':
         e.preventDefault();
         this.resumeGame();
         break;
       case 'q':
         e.preventDefault();
         this.quitToMenu();
         break;
     }
   }
 
   handleMenuKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'enter':
       case ' ':
         e.preventDefault();
         this.startGame();
         break;
     }
   }
 
   handleCursorSelect() {
     if (!this.gameRunning || this.gamePaused) return;
     
     // Check if there's a block at cursor position
     if (!this.grid[this.cursorRow][this.cursorCol]) return;
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if cursor is on selected block (deselect)
       if (this.cursorRow === selectedRow && this.cursorCol === selectedCol) {
         this.clearSelection();
         return;
       }
       
       // Check if adjacent horizontally or vertically
       if ((this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) || 
           (this.cursorCol === selectedCol && Math.abs(this.cursorRow - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row: this.cursorRow, col: this.cursorCol };
       this.updateSelection();
     }
   }
 
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
       this.removeMatches(matches, () => {
         // After blocks are removed, apply gravity
         setTimeout(() => {
           this.applyGravity(() => {
             // After gravity is complete, check for new matches
+            setTimeout(() => this.checkMatches(), 200);
-            setTimeout(() => this.checkMatches(), 100);
           });
+        }, 150);
-        }, 100);
       });
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
   removeMatches(matches, callback) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
     // Add matched animation before removing
     matches.forEach(({ row, col }, index) => {
       const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('matched');
           if (this.combo >= 3) {
             this.createParticleEffect(blockElement);
           }
         }, index * 50);
       }
     });
     
     // Remove blocks after animation and call callback
     setTimeout(() => {
       matches.forEach(({ row, col }) => {
         this.grid[row][col] = null;
       });
       this.renderGrid();
       this.updateDisplay();
       
       if (this.combo > 1) {
         this.elements.comboText.textContent = `${this.combo}x COMBO!`;
         if (this.combo >= 7) {
           document.getElementById('app').classList.add('screen-shake');
           setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
         }
       } else {
         this.elements.comboText.textContent = '';
       }
       
       if (callback) callback();
+    }, 600);
-    }, 500);
   }
 
   applyGravity(callback) {
     const fallingBlocks = [];
     const columnData = [];
     
     // Analyze each column and track falling blocks
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
       const originalPositions = [];
       
       // Collect non-null blocks and their original positions
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
           originalPositions.push(row);
         }
       }
       
       columnData[col] = { blocks: column, originalPositions };
       
       // Clear the column
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
         this.grid[row][col] = null;
       }
       
       // Track falling blocks with distance information
       for (let i = 0; i < column.length; i++) {
         const oldRow = originalPositions[i];
         const newRow = this.GRID_HEIGHT - 1 - i;
         
         if (newRow > oldRow) {
           fallingBlocks.push({ 
             col, 
             oldRow, 
             newRow, 
             block: column[i],
             fallDistance: newRow - oldRow
           });
         }
       }
       
       // Place blocks in their new positions
       for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
     // If no blocks need to fall, just call callback
     if (fallingBlocks.length === 0) {
       this.renderGrid();
       if (callback) callback();
       return;
     }
     
     // Render grid first
     this.renderGrid();
     
     // Apply falling animations with staggered timing
     let maxAnimationTime = 0;
     fallingBlocks.forEach((block, index) => {
       const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
       if (blockElement) {
         // Stagger animation start based on column and fall distance
         const startDelay = (block.col * 50) + (block.fallDistance * 20);
         const animationDuration = 400 + (block.fallDistance * 100);
         
         maxAnimationTime = Math.max(maxAnimationTime, startDelay + animationDuration);
         
         setTimeout(() => {
           blockElement.classList.add('gravity-fall');
           setTimeout(() => {
             blockElement.classList.remove('gravity-fall');
           }, animationDuration);
         }, startDelay);
       }
     });
     
     // Call callback after all animations complete
     if (callback) {
+      setTimeout(callback, maxAnimationTime + 200);
-      setTimeout(callback, maxAnimationTime + 100);
     }
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
-    this.renderGrid();
     
+    // Wait a bit before rendering to avoid conflicts
+    setTimeout(() => {
+      this.renderGrid();
+      
+      // Add new row animation with smoother staggering
+      const animationDuration = 500;
+      for (let col = 0; col < this.GRID_WIDTH; col++) {
+        const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
+        if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
+          setTimeout(() => {
+            blockElement.classList.add('new-row');
+            setTimeout(() => blockElement.classList.remove('new-row'), animationDuration);
+          }, col * 80);
+        }
-    // Add new row animation
-    for (let col = 0; col < this.GRID_WIDTH; col++) {
-      const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
-      if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
-        setTimeout(() => {
-          blockElement.classList.add('new-row');
-          setTimeout(() => blockElement.classList.remove('new-row'), 400);
-        }, col * 50);
       }
+      
+      // Wait for all animations to complete before checking matches
+      const totalAnimationTime = (this.GRID_WIDTH - 1) * 80 + animationDuration;
+      setTimeout(() => this.checkMatches(), totalAnimationTime + 100);
+    }, 50);
-    }
-    
-    setTimeout(() => this.checkMatches(), 600);
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
+      this.fallSpeed = Math.max(2000, 5000 - (this.level - 1) * 100);
-      this.fallSpeed = Math.max(1000, 3000 - (this.level - 1) * 200);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
     this.updateCursor(); // Hide cursor when paused
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
     this.updateCursor(); // Show cursor when resumed
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.updateCursor(); // Hide cursor when quitting
     this.showLeaderboard();
   }
 
   async gameOver() {
     this.gameRunning = false;
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
     if (this.currentUser) {
       await this.saveScore();
       const position = await this.getScorePosition();
       this.elements.scoreMessage.textContent = position <= 20 ? 
         `🎉 You made it to #${position} on the leaderboard!` : 
         `Your best score has been saved!`;
       this.elements.anonymousMessage.style.display = 'none';
     } else {
       this.elements.scoreMessage.textContent = '';
       this.elements.anonymousMessage.style.display = 'block';
     }
     
     this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
 });

Preview

Falco

Can you add some sound effects? And a way to control volume / mute?

Web-Artifact-Creator-bot
Thinking...The user wants me to add sound effects to the Connect 3 puzzle game and provide volume/mute controls. I should add:
  1. Sound effects for various game actions:

    • Block matching/clearing sounds
    • Block swapping sounds
    • New row addition sounds
    • Game over sound
    • Combo sounds (different for different combo levels)
    • Background music (optional, but could be nice)
  2. Volume controls:

    • Mute/unmute button
    • Volume slider
    • Save volume preferences

Since this is a web game, I’ll need to use the Web Audio API or HTML5 audio. For simplicity and compatibility, I’ll use HTML5 audio with procedurally generated sounds or simple beep tones that can be created programmatically.

I should add:

  • A sound manager class
  • Volume controls in the UI
  • Different sound effects for different actions
  • Local storage for volume preferences
Change Description
Add comprehensive sound effects and volume controls:

**Sound Effects to Add:**
- Block matching/clearing sound (satisfying pop)
- Block swapping sound (subtle whoosh)
- New row sliding up sound
- Combo sounds (different tones for higher combos)
- Game over sound
- Button click sounds
- Level up sound
- Background ambient music (soft, looping)

**Sound System:**
- Create a SoundManager class that generates procedural audio using Web Audio API
- Generate different tones, frequencies, and effects for various actions
- Support for volume control and muting
- Local storage for volume preferences

**UI Controls:**
- Add volume/mute button in game header next to other controls
- Volume slider that appears when clicking the volume button
- Volume icon changes based on current volume level (🔊 🔉 🔇)
- Save volume preferences to localStorage

**Integration:**
- Call appropriate sound methods throughout the game:
  - playSwapSound() when swapping blocks
  - playMatchSound() when blocks are matched
  - playComboSound(comboLevel) for combos
  - playNewRowSound() when new row is added
  - playGameOverSound() when game ends
  - playButtonSound() for UI interactions

**Technical Implementation:**
- Use Web Audio API for precise control and procedural generation
- Create different oscillator types for various sounds
- Add reverb and filter effects for richer audio
- Implement sound scheduling to avoid audio conflicts
- Add visual feedback when sounds play (subtle screen effects)

**CSS Updates:**
- Add volume control styling
- Smooth transitions for volume slider
- Visual feedback for mute state
View Changes

HTML Changes

 <div id="app">
   <!-- Leaderboard Screen -->
   <div id="leaderboard-screen" class="screen active">
     <div class="header">
       <h1>🧩 Connect 3 Puzzle</h1>
       <p>Match 3 or more blocks to clear them and create combos!</p>
     </div>
     
     <div class="leaderboard-container">
       <h2>🏆 Top Scores</h2>
       <div id="leaderboard-loading">Loading leaderboard...</div>
       <table id="leaderboard-table" style="display: none;">
         <thead>
           <tr>
             <th>Rank</th>
             <th>Player</th>
             <th>Score</th>
             <th>Date</th>
           </tr>
         </thead>
         <tbody id="leaderboard-body">
         </tbody>
       </table>
     </div>
     
     <button id="start-game-btn" class="primary-button">🎮 Start New Game</button>
   </div>
 
   <!-- Game Screen -->
   <div id="game-screen" class="screen">
     <div class="game-header">
       <div class="game-info">
         <div class="score-display">
           <span class="label">Score:</span>
           <span id="current-score">0</span>
         </div>
         <div class="level-display">
           <span class="label">Level:</span>
           <span id="current-level">1</span>
         </div>
         <div class="combo-display">
           <span id="combo-text"></span>
         </div>
       </div>
       <div class="game-controls">
+        <div class="volume-control">
+          <button id="volume-btn" class="control-button">🔊</button>
+          <div id="volume-slider-container" class="volume-slider-container">
+            <input type="range" id="volume-slider" class="volume-slider" min="0" max="100" value="70">
+          </div>
+        </div>
         <button id="pause-btn" class="control-button">⏸️</button>
         <button id="back-to-menu-btn" class="control-button">🏠</button>
       </div>
     </div>
 
     <div class="game-container">
       <div id="game-grid" class="game-grid">
         <!-- Grid will be generated by JavaScript -->
       </div>
     </div>
 
     <div class="game-footer">
       <div class="next-preview">
         <span class="label">Next Row:</span>
         <div id="next-row-preview" class="next-row">
           <!-- Preview will be generated by JavaScript -->
         </div>
       </div>
       <div class="keyboard-hints">
         <div class="hint-row">
           <span>🎮 <strong>Arrow Keys:</strong> Move cursor</span>
           <span><strong>Space/Enter:</strong> Select/Swap</span>
         </div>
         <div class="hint-row">
           <span><strong>Esc:</strong> Pause</span>
           <span><strong>R:</strong> Restart (when game over)</span>
         </div>
       </div>
     </div>
   </div>
 
   <!-- Game Over Modal -->
   <div id="game-over-modal" class="modal">
     <div class="modal-content">
       <h2>🎯 Game Over!</h2>
       <div class="final-score">
         <span class="label">Final Score:</span>
         <span id="final-score-value">0</span>
       </div>
       <div id="score-message"></div>
       <div id="anonymous-message" style="display: none;">
         <p>Log in to save your score!</p>
         <a href="https://meta.discourse.org" target="_blank" class="login-link">Login to Discourse</a>
       </div>
       <div class="modal-buttons">
         <button id="play-again-btn" class="primary-button">🔄 Play Again</button>
         <button id="view-leaderboard-btn" class="secondary-button">🏆 View Leaderboard</button>
       </div>
     </div>
   </div>
 
   <!-- Pause Modal -->
   <div id="pause-modal" class="modal">
     <div class="modal-content">
       <h2>⏸️ Game Paused</h2>
       <div class="modal-buttons">
         <button id="resume-btn" class="primary-button">▶️ Resume</button>
         <button id="quit-btn" class="secondary-button">🏠 Main Menu</button>
       </div>
     </div>
   </div>
 </div>

CSS Changes

 #app {
   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
   width: 100%;
   max-width: 600px;
   height: 600px;
   margin: 0 auto;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 12px;
   box-shadow: 0 10px 30px rgba(0,0,0,0.3);
   overflow: hidden;
   position: relative;
 }
 
 .screen {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: none;
   flex-direction: column;
   padding: 15px;
   box-sizing: border-box;
 }
 
 .screen.active {
   display: flex;
 }
 
 .header {
   text-align: center;
   margin-bottom: 20px;
 }
 
 .header h1 {
   color: white;
   font-size: 1.8em;
   margin: 0;
   text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 }
 
 .header p {
   color: rgba(255,255,255,0.9);
   margin: 5px 0 0 0;
   font-size: 0.95em;
 }
 
 .leaderboard-container {
   flex: 1;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 15px;
   margin-bottom: 15px;
   overflow-y: auto;
 }
 
 .leaderboard-container h2 {
   margin: 0 0 15px 0;
   color: #333;
   text-align: center;
 }
 
 #leaderboard-table {
   width: 100%;
   border-collapse: collapse;
   font-size: 0.9em;
 }
 
 #leaderboard-table th,
 #leaderboard-table td {
   padding: 8px 12px;
   text-align: left;
   border-bottom: 1px solid #eee;
 }
 
 #leaderboard-table th {
   background: #f8f9fa;
   font-weight: 600;
   color: #555;
 }
 
 #leaderboard-table tr:nth-child(even) {
   background: #f8f9fa;
 }
 
 .rank-medal {
   font-size: 1.2em;
 }
 
 .primary-button {
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border: none;
   color: white;
   font-size: 1.2em;
   font-weight: 600;
   padding: 15px 30px;
   border-radius: 25px;
   cursor: pointer;
   box-shadow: 0 4px 15px rgba(0,0,0,0.2);
   transition: all 0.3s ease;
 }
 
 .primary-button:hover {
   transform: translateY(-2px);
   box-shadow: 0 6px 20px rgba(0,0,0,0.3);
 }
 
 .secondary-button {
   background: rgba(255,255,255,0.2);
   border: 2px solid rgba(255,255,255,0.5);
   color: white;
   font-size: 1em;
   font-weight: 600;
   padding: 10px 20px;
   border-radius: 20px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .secondary-button:hover {
   background: rgba(255,255,255,0.3);
   border-color: rgba(255,255,255,0.8);
 }
 
 .game-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-bottom: 8px;
 }
 
 .game-info {
   display: flex;
   gap: 20px;
   flex-wrap: wrap;
 }
 
 .score-display, .level-display {
   font-weight: 600;
   color: #333;
 }
 
 .label {
   color: #666;
   margin-right: 5px;
 }
 
 #current-score, #current-level {
   color: #ff6b6b;
   font-size: 1.2em;
 }
 
 .combo-display {
   font-weight: bold;
   color: #feca57;
   min-width: 80px;
 }
 
 .game-controls {
   display: flex;
   gap: 10px;
+  align-items: center;
 }
 
+.volume-control {
+  position: relative;
+  display: flex;
+  align-items: center;
+}
+
+.volume-slider-container {
+  position: absolute;
+  top: 50%;
+  right: 100%;
+  transform: translateY(-50%);
+  background: rgba(255, 255, 255, 0.95);
+  border-radius: 20px;
+  padding: 8px 15px;
+  margin-right: 8px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+  opacity: 0;
+  visibility: hidden;
+  transition: all 0.3s ease;
+  z-index: 100;
+}
+
+.volume-slider-container.active {
+  opacity: 1;
+  visibility: visible;
+}
+
+.volume-slider {
+  width: 80px;
+  height: 4px;
+  background: #ddd;
+  border-radius: 2px;
+  outline: none;
+  cursor: pointer;
+  -webkit-appearance: none;
+  appearance: none;
+}
+
+.volume-slider::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 16px;
+  height: 16px;
+  background: linear-gradient(45deg, #ff6b6b, #feca57);
+  border-radius: 50%;
+  cursor: pointer;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+  transition: all 0.2s ease;
+}
+
+.volume-slider::-webkit-slider-thumb:hover {
+  transform: scale(1.2);
+}
+
+.volume-slider::-moz-range-thumb {
+  width: 16px;
+  height: 16px;
+  background: linear-gradient(45deg, #ff6b6b, #feca57);
+  border-radius: 50%;
+  cursor: pointer;
+  border: none;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+}
+
+.volume-slider::-webkit-slider-track {
+  background: linear-gradient(to right, #ff6b6b 0%, #feca57 100%);
+  height: 4px;
+  border-radius: 2px;
+}
+
+.volume-slider::-moz-range-track {
+  background: linear-gradient(to right, #ff6b6b 0%, #feca57 100%);
+  height: 4px;
+  border-radius: 2px;
+  border: none;
+}
+
 .control-button {
   background: rgba(255,255,255,0.2);
   border: none;
   color: white;
   font-size: 1.5em;
   padding: 8px 12px;
   border-radius: 8px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .control-button:hover {
   background: rgba(255,255,255,0.3);
 }
 
+.control-button.muted {
+  opacity: 0.6;
+  background: rgba(255, 255, 255, 0.1);
+}
+
+.sound-pulse {
+  animation: sound-pulse 0.3s ease-out;
+}
+
+@keyframes sound-pulse {
+  0% { transform: scale(1); }
+  50% { transform: scale(1.05); opacity: 0.8; }
+  100% { transform: scale(1); }
+}
+
 .game-container {
   flex: 1;
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 10px 0;
 }
 
 .game-grid {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   grid-template-rows: repeat(12, 1fr);
   gap: 2px;
   background: rgba(0,0,0,0.3);
   border-radius: 8px;
   padding: 6px;
   width: 240px;
   height: 320px;
 }
 
 .block {
   background: rgba(255,255,255,0.9);
   border-radius: 4px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.4em;
   cursor: pointer;
   transition: all 0.2s ease;
   user-select: none;
   border: 2px solid transparent;
   position: relative;
 }
 
 .block:hover {
   transform: scale(1.05);
   box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 }
 
 .block.selected {
   border-color: #feca57;
   box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
 }
 
 .block.cursor {
   border-color: #00d4ff;
   box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   animation: cursor-pulse 1.5s ease-in-out infinite;
 }
 
 .block.cursor.selected {
   border-color: #ff6b6b;
   box-shadow: 0 0 15px rgba(255, 107, 107, 0.8), 0 0 25px rgba(0, 212, 255, 0.6);
 }
 
 @keyframes cursor-pulse {
   0%, 100% { 
     box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   }
   50% { 
     box-shadow: 0 0 25px rgba(0, 212, 255, 1), inset 0 0 15px rgba(0, 212, 255, 0.5);
   }
 }
 
 .block.falling {
   animation: fall 0.4s ease-out;
 }
 
 .block.matched {
   animation: matched-pop 0.5s ease-out forwards;
 }
 
 .block.swapping-right {
   animation: swap-right 0.3s ease-in-out;
 }
 
 .block.swapping-left {
   animation: swap-left 0.3s ease-in-out;
 }
 
 .block.swapping-up {
   animation: swap-up 0.3s ease-in-out;
 }
 
 .block.swapping-down {
   animation: swap-down 0.3s ease-in-out;
 }
 
 .block.new-row {
   animation: slide-up 0.4s ease-out;
 }
 
 .block.gravity-fall {
   animation: gravity-drop 0.3s ease-in;
 }
 
 @keyframes fall {
   from { 
     transform: translateY(-100%);
     opacity: 0.7;
   }
   to { 
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes matched-pop {
   0% { 
     transform: scale(1);
     opacity: 1;
   }
   30% { 
     transform: scale(1.4) rotate(10deg);
     opacity: 1;
     background: linear-gradient(45deg, #ff6b6b, #feca57);
   }
   70% { 
     transform: scale(1.6) rotate(-5deg);
     opacity: 0.8;
   }
   100% { 
     transform: scale(0) rotate(180deg);
     opacity: 0;
   }
 }
 
 @keyframes swap-right {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(100% + 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-left {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(-100% - 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-up {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(-100% - 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes swap-down {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(100% + 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes slide-up {
   0% {
     transform: translateY(100%);
     opacity: 0.6;
   }
   30% {
     transform: translateY(50%);
     opacity: 0.8;
   }
   100% {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes gravity-drop {
   0% {
     transform: translateY(-100%);
     opacity: 0.8;
   }
   60% {
     transform: translateY(10%);
     opacity: 0.9;
   }
   100% {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 .game-footer {
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-top: 8px;
 }
 
 .keyboard-hints {
   margin-top: 10px;
   font-size: 0.8em;
   color: #666;
 }
 
 .hint-row {
   display: flex;
   justify-content: space-between;
   margin: 2px 0;
   gap: 15px;
 }
 
 .hint-row span {
   flex: 1;
 }
 
 .hint-row strong {
   color: #333;
 }
 
 .next-row {
   display: flex;
   gap: 5px;
   margin-top: 5px;
 }
 
 .next-block {
   width: 30px;
   height: 30px;
   border-radius: 4px;
   background: rgba(255,255,255,0.9);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.2em;
   border: 1px solid #ddd;
 }
 
 .modal {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0,0,0,0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 1000;
 }
 
 .modal.active {
   display: flex;
 }
 
 .modal-content {
   background: white;
   border-radius: 12px;
   padding: 30px;
   text-align: center;
   max-width: 400px;
   width: 90%;
   box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 }
 
 .modal-content h2 {
   margin: 0 0 20px 0;
   color: #333;
 }
 
 .final-score {
   margin: 20px 0;
   font-size: 1.3em;
 }
 
 #final-score-value {
   color: #ff6b6b;
   font-weight: bold;
   font-size: 1.5em;
 }
 
 .modal-buttons {
   display: flex;
   gap: 15px;
   justify-content: center;
   margin-top: 25px;
   flex-wrap: wrap;
 }
 
 .login-link {
   display: inline-block;
   background: #0088cc;
   color: white;
   padding: 10px 20px;
   border-radius: 20px;
   text-decoration: none;
   margin: 10px 0;
   transition: background 0.3s ease;
 }
 
 .login-link:hover {
   background: #0066aa;
 }
 
 #leaderboard-loading {
   text-align: center;
   color: #666;
   font-style: italic;
   padding: 40px;
 }
 
 #score-message {
   margin: 15px 0;
   font-weight: 600;
   color: #333;
 }
 
 .screen-shake {
   animation: shake 0.3s ease-in-out;
 }
 
 @keyframes shake {
   0%, 100% { transform: translateX(0) translateY(0); }
   25% { transform: translateX(-2px) translateY(-1px); }
   50% { transform: translateX(2px) translateY(1px); }
   75% { transform: translateX(-1px) translateY(-2px); }
 }
 
 .combo-particles {
   position: absolute;
   pointer-events: none;
   width: 100%;
   height: 100%;
 }
 
 .particle {
   position: absolute;
   font-size: 1.2em;
   animation: particle-burst 1s ease-out forwards;
   pointer-events: none;
 }
 
 @keyframes particle-burst {
   0% {
     opacity: 1;
     transform: scale(0.5);
   }
   50% {
     opacity: 0.8;
     transform: scale(1.2);
   }
   100% {
     opacity: 0;
     transform: scale(0.3) translateY(-50px);
   }
 }
 
 @media (max-width: 480px) {
   #app {
     border-radius: 0;
     height: 100vh;
     max-width: 100%;
   }
   
   .screen {
     padding: 10px;
   }
   
   .game-grid {
     width: 200px;
     height: 280px;
   }
   
   .block {
     font-size: 1.2em;
   }
   
   .game-info {
     font-size: 0.85em;
   }
   
   .modal-content {
     width: 95%;
     padding: 20px;
   }
   
   .header h1 {
     font-size: 1.5em;
   }
   
   .header p {
     font-size: 0.85em;
   }
 }

JS Changes

+class SoundManager {
+  constructor() {
+    this.audioContext = null;
+    this.masterGain = null;
+    this.volume = 0.7;
+    this.muted = false;
+    this.isInitialized = false;
+    
+    // Load volume preferences
+    this.loadVolumePreferences();
+  }
+
+  async initialize() {
+    if (this.isInitialized) return;
+    
+    try {
+      this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
+      this.masterGain = this.audioContext.createGain();
+      this.masterGain.connect(this.audioContext.destination);
+      this.updateVolume();
+      this.isInitialized = true;
+    } catch (error) {
+      console.warn('Web Audio API not supported:', error);
+    }
+  }
+
+  updateVolume() {
+    if (this.masterGain) {
+      this.masterGain.gain.value = this.muted ? 0 : this.volume;
+    }
+  }
+
+  setVolume(volume) {
+    this.volume = Math.max(0, Math.min(1, volume));
+    this.updateVolume();
+    this.saveVolumePreferences();
+  }
+
+  toggleMute() {
+    this.muted = !this.muted;
+    this.updateVolume();
+    this.saveVolumePreferences();
+  }
+
+  saveVolumePreferences() {
+    localStorage.setItem('connect3_volume', this.volume.toString());
+    localStorage.setItem('connect3_muted', this.muted.toString());
+  }
+
+  loadVolumePreferences() {
+    const savedVolume = localStorage.getItem('connect3_volume');
+    const savedMuted = localStorage.getItem('connect3_muted');
+    
+    if (savedVolume !== null) {
+      this.volume = parseFloat(savedVolume);
+    }
+    if (savedMuted !== null) {
+      this.muted = savedMuted === 'true';
+    }
+  }
+
+  createOscillator(frequency, type = 'sine', duration = 0.3) {
+    if (!this.isInitialized) return null;
+    
+    const oscillator = this.audioContext.createOscillator();
+    const gainNode = this.audioContext.createGain();
+    
+    oscillator.type = type;
+    oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime);
+    
+    gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
+    gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
+    
+    oscillator.connect(gainNode);
+    gainNode.connect(this.masterGain);
+    
+    return { oscillator, gainNode, duration };
+  }
+
+  createFilteredNoise(frequency, duration = 0.2) {
+    if (!this.isInitialized) return null;
+    
+    const bufferSize = this.audioContext.sampleRate * duration;
+    const noiseBuffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
+    const output = noiseBuffer.getChannelData(0);
+    
+    for (let i = 0; i < bufferSize; i++) {
+      output[i] = Math.random() * 2 - 1;
+    }
+    
+    const whiteNoise = this.audioContext.createBufferSource();
+    whiteNoise.buffer = noiseBuffer;
+    
+    const bandpass = this.audioContext.createBiquadFilter();
+    bandpass.type = 'bandpass';
+    bandpass.frequency.value = frequency;
+    bandpass.Q.value = 30;
+    
+    const gainNode = this.audioContext.createGain();
+    gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
+    gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
+    
+    whiteNoise.connect(bandpass);
+    bandpass.connect(gainNode);
+    gainNode.connect(this.masterGain);
+    
+    return { source: whiteNoise, duration };
+  }
+
+  playSwapSound() {
+    if (!this.isInitialized) return;
+    
+    const sound1 = this.createOscillator(400, 'sine', 0.15);
+    const sound2 = this.createOscillator(600, 'sine', 0.15);
+    
+    if (sound1 && sound2) {
+      sound1.oscillator.start();
+      sound1.oscillator.stop(this.audioContext.currentTime + sound1.duration);
+      
+      setTimeout(() => {
+        sound2.oscillator.start();
+        sound2.oscillator.stop(this.audioContext.currentTime + sound2.duration);
+      }, 50);
+    }
+  }
+
+  playMatchSound(blockCount = 3) {
+    if (!this.isInitialized) return;
+    
+    const baseFreq = 220 + (blockCount * 40);
+    const sound = this.createOscillator(baseFreq, 'triangle', 0.4);
+    const harmonic = this.createOscillator(baseFreq * 1.5, 'sine', 0.4);
+    
+    if (sound && harmonic) {
+      sound.gainNode.gain.setValueAtTime(0.4, this.audioContext.currentTime);
+      harmonic.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
+      
+      sound.oscillator.start();
+      harmonic.oscillator.start();
+      
+      sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
+      harmonic.oscillator.stop(this.audioContext.currentTime + harmonic.duration);
+    }
+  }
+
+  playComboSound(comboLevel) {
+    if (!this.isInitialized) return;
+    
+    const frequencies = [440, 550, 660, 770, 880, 990, 1100, 1210, 1320, 1430];
+    const freq = frequencies[Math.min(comboLevel - 2, frequencies.length - 1)];
+    
+    // Main tone
+    const main = this.createOscillator(freq, 'sawtooth', 0.6);
+    // Higher harmonic
+    const high = this.createOscillator(freq * 1.5, 'sine', 0.4);
+    // Lower bass
+    const bass = this.createOscillator(freq * 0.5, 'triangle', 0.8);
+    
+    if (main && high && bass) {
+      main.gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
+      high.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
+      bass.gainNode.gain.setValueAtTime(0.4, this.audioContext.currentTime);
+      
+      main.oscillator.start();
+      high.oscillator.start();
+      bass.oscillator.start();
+      
+      main.oscillator.stop(this.audioContext.currentTime + main.duration);
+      high.oscillator.stop(this.audioContext.currentTime + high.duration);
+      bass.oscillator.stop(this.audioContext.currentTime + bass.duration);
+    }
+  }
+
+  playNewRowSound() {
+    if (!this.isInitialized) return;
+    
+    const sound = this.createFilteredNoise(800, 0.3);
+    if (sound) {
+      sound.source.start();
+      sound.source.stop(this.audioContext.currentTime + sound.duration);
+    }
+  }
+
+  playGameOverSound() {
+    if (!this.isInitialized) return;
+    
+    const frequencies = [440, 370, 311, 262];
+    frequencies.forEach((freq, index) => {
+      setTimeout(() => {
+        const sound = this.createOscillator(freq, 'square', 0.8);
+        if (sound) {
+          sound.oscillator.start();
+          sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
+        }
+      }, index * 200);
+    });
+  }
+
+  playButtonSound() {
+    if (!this.isInitialized) return;
+    
+    const sound = this.createOscillator(800, 'sine', 0.1);
+    if (sound) {
+      sound.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
+      sound.oscillator.start();
+      sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
+    }
+  }
+
+  playLevelUpSound() {
+    if (!this.isInitialized) return;
+    
+    const arpeggio = [523, 659, 784, 1047]; // C5, E5, G5, C6
+    arpeggio.forEach((freq, index) => {
+      setTimeout(() => {
+        const sound = this.createOscillator(freq, 'triangle', 0.3);
+        if (sound) {
+          sound.oscillator.start();
+          sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
+        }
+      }, index * 100);
+    });
+  }
+}
+
 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.nextRow = [];
     this.fallSpeed = 5000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
+    this.previousLevel = 1;
     
+    this.soundManager = new SoundManager();
+    
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
+    this.setupVolumeControls();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
+      leaderboardLoading: document.getElementById('leaderboard-loading'),
+      volumeBtn: document.getElementById('volume-btn'),
+      volumeSlider: document.getElementById('volume-slider'),
+      volumeSliderContainer: document.getElementById('volume-slider-container')
-      leaderboardLoading: document.getElementById('leaderboard-loading')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
     this.updateCursor();
   }
 
   createParticleEffect(blockElement) {
     const rect = blockElement.getBoundingClientRect();
     const gridRect = this.elements.gameGrid.getBoundingClientRect();
     
     for (let i = 0; i < 3; i++) {
       const particle = document.createElement('div');
       particle.className = 'particle';
       particle.textContent = '✨';
       
       const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
       const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
       
       particle.style.left = offsetX + 'px';
       particle.style.top = offsetY + 'px';
       particle.style.animationDelay = (i * 100) + 'ms';
       
       this.elements.gameGrid.appendChild(particle);
       
       setTimeout(() => {
         if (particle.parentNode) {
           particle.parentNode.removeChild(particle);
         }
       }, 1000);
     }
   }
 
   bindEvents() {
+    this.elements.startGameBtn.addEventListener('click', () => {
+      this.soundManager.playButtonSound();
+      this.startGame();
+    });
+    this.elements.pauseBtn.addEventListener('click', () => {
+      this.soundManager.playButtonSound();
+      this.pauseGame();
+    });
+    this.elements.backToMenuBtn.addEventListener('click', () => {
+      this.soundManager.playButtonSound();
+      this.quitToMenu();
+    });
+    this.elements.resumeBtn.addEventListener('click', () => {
+      this.soundManager.playButtonSound();
+      this.resumeGame();
+    });
+    this.elements.quitBtn.addEventListener('click', () => {
+      this.soundManager.playButtonSound();
+      this.quitToMenu();
+    });
+    this.elements.playAgainBtn.addEventListener('click', () => {
+      this.soundManager.playButtonSound();
+      this.startGame();
+    });
+    this.elements.viewLeaderboardBtn.addEventListener('click', () => {
+      this.soundManager.playButtonSound();
+      this.showLeaderboard();
+    });
-    this.elements.startGameBtn.addEventListener('click', () => this.startGame());
-    this.elements.pauseBtn.addEventListener('click', () => this.pauseGame());
-    this.elements.backToMenuBtn.addEventListener('click', () => this.quitToMenu());
-    this.elements.resumeBtn.addEventListener('click', () => this.resumeGame());
-    this.elements.quitBtn.addEventListener('click', () => this.quitToMenu());
-    this.elements.playAgainBtn.addEventListener('click', () => this.startGame());
-    this.elements.viewLeaderboardBtn.addEventListener('click', () => this.showLeaderboard());
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
     
     // Add keyboard event listener
     document.addEventListener('keydown', (e) => this.handleKeyDown(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.fallSpeed = 5000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if adjacent horizontally or vertically
       if ((row === selectedRow && Math.abs(col - selectedCol) === 1) || 
           (col === selectedCol && Math.abs(row - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
+    this.soundManager.playSwapSound();
+    
     // Add swap animation
     const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
     const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
     
     // Determine swap direction and apply appropriate animations
     if (row1 === row2) {
       // Horizontal swap
       if (col2 > col1) {
         block1.classList.add('swapping-right');
         block2.classList.add('swapping-left');
       } else {
         block1.classList.add('swapping-left');
         block2.classList.add('swapping-right');
       }
     } else {
       // Vertical swap
       if (row2 > row1) {
         block1.classList.add('swapping-down');
         block2.classList.add('swapping-up');
       } else {
         block1.classList.add('swapping-up');
         block2.classList.add('swapping-down');
       }
     }
     
     setTimeout(() => {
       const temp = this.grid[row1][col1];
       this.grid[row1][col1] = this.grid[row2][col2];
       this.grid[row2][col2] = temp;
       
       this.renderGrid();
       this.checkMatches();
     }, 150);
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
   updateCursor() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('cursor'));
     
     if (this.gameRunning && !this.gamePaused) {
       const index = this.cursorRow * this.GRID_WIDTH + this.cursorCol;
       blocks[index]?.classList.add('cursor');
     }
   }
 
   moveCursor(deltaRow, deltaCol) {
     this.cursorRow = (this.cursorRow + deltaRow + this.GRID_HEIGHT) % this.GRID_HEIGHT;
     this.cursorCol = (this.cursorCol + deltaCol + this.GRID_WIDTH) % this.GRID_WIDTH;
     this.updateCursor();
   }
 
   handleKeyDown(e) {
     // Handle different game states
     if (this.elements.gameOverModal.classList.contains('active')) {
       this.handleGameOverKeys(e);
       return;
     }
     
     if (this.elements.pauseModal.classList.contains('active')) {
       this.handlePauseKeys(e);
       return;
     }
     
     if (this.elements.leaderboardScreen.classList.contains('active')) {
       this.handleMenuKeys(e);
       return;
     }
     
     if (this.gameRunning) {
       this.handleGameKeys(e);
     }
   }
 
   handleGameKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'arrowup':
       case 'w':
         e.preventDefault();
         this.moveCursor(-1, 0);
         break;
       case 'arrowdown':
       case 's':
         e.preventDefault();
         this.moveCursor(1, 0);
         break;
       case 'arrowleft':
       case 'a':
         e.preventDefault();
         this.moveCursor(0, -1);
         break;
       case 'arrowright':
       case 'd':
         e.preventDefault();
         this.moveCursor(0, 1);
         break;
       case ' ':
       case 'enter':
         e.preventDefault();
         this.handleCursorSelect();
         break;
       case 'escape':
         e.preventDefault();
         if (this.gamePaused) {
           this.resumeGame();
         } else {
           this.pauseGame();
         }
         break;
     }
   }
 
   handleGameOverKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'r':
         e.preventDefault();
         this.startGame();
         break;
       case 'escape':
       case 'enter':
         e.preventDefault();
         this.showLeaderboard();
         break;
     }
   }
 
   handlePauseKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'escape':
       case 'enter':
       case ' ':
         e.preventDefault();
         this.resumeGame();
         break;
       case 'q':
         e.preventDefault();
         this.quitToMenu();
         break;
     }
   }
 
   handleMenuKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'enter':
       case ' ':
         e.preventDefault();
         this.startGame();
         break;
     }
   }
 
   handleCursorSelect() {
     if (!this.gameRunning || this.gamePaused) return;
     
     // Check if there's a block at cursor position
     if (!this.grid[this.cursorRow][this.cursorCol]) return;
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if cursor is on selected block (deselect)
       if (this.cursorRow === selectedRow && this.cursorCol === selectedCol) {
         this.clearSelection();
         return;
       }
       
       // Check if adjacent horizontally or vertically
       if ((this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) || 
           (this.cursorCol === selectedCol && Math.abs(this.cursorRow - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row: this.cursorRow, col: this.cursorCol };
       this.updateSelection();
     }
   }
 
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
       this.removeMatches(matches, () => {
         // After blocks are removed, apply gravity
         setTimeout(() => {
           this.applyGravity(() => {
             // After gravity is complete, check for new matches
             setTimeout(() => this.checkMatches(), 200);
           });
         }, 150);
       });
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
   removeMatches(matches, callback) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
+    // Play appropriate sounds
+    if (this.combo > 1) {
+      this.soundManager.playComboSound(this.combo);
+    } else {
+      this.soundManager.playMatchSound(matches.length);
+    }
+    
     // Add matched animation before removing
     matches.forEach(({ row, col }, index) => {
       const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('matched');
+          blockElement.classList.add('sound-pulse');
           if (this.combo >= 3) {
             this.createParticleEffect(blockElement);
           }
         }, index * 50);
       }
     });
     
     // Remove blocks after animation and call callback
     setTimeout(() => {
       matches.forEach(({ row, col }) => {
         this.grid[row][col] = null;
       });
       this.renderGrid();
       this.updateDisplay();
       
       if (this.combo > 1) {
         this.elements.comboText.textContent = `${this.combo}x COMBO!`;
         if (this.combo >= 7) {
           document.getElementById('app').classList.add('screen-shake');
           setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
         }
       } else {
         this.elements.comboText.textContent = '';
       }
       
       if (callback) callback();
     }, 600);
   }
 
   applyGravity(callback) {
     const fallingBlocks = [];
     const columnData = [];
     
     // Analyze each column and track falling blocks
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
       const originalPositions = [];
       
       // Collect non-null blocks and their original positions
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
           originalPositions.push(row);
         }
       }
       
       columnData[col] = { blocks: column, originalPositions };
       
       // Clear the column
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
         this.grid[row][col] = null;
       }
       
       // Track falling blocks with distance information
       for (let i = 0; i < column.length; i++) {
         const oldRow = originalPositions[i];
         const newRow = this.GRID_HEIGHT - 1 - i;
         
         if (newRow > oldRow) {
           fallingBlocks.push({ 
             col, 
             oldRow, 
             newRow, 
             block: column[i],
             fallDistance: newRow - oldRow
           });
         }
       }
       
       // Place blocks in their new positions
       for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
     // If no blocks need to fall, just call callback
     if (fallingBlocks.length === 0) {
       this.renderGrid();
       if (callback) callback();
       return;
     }
     
     // Render grid first
     this.renderGrid();
     
     // Apply falling animations with staggered timing
     let maxAnimationTime = 0;
     fallingBlocks.forEach((block, index) => {
       const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
       if (blockElement) {
         // Stagger animation start based on column and fall distance
         const startDelay = (block.col * 50) + (block.fallDistance * 20);
         const animationDuration = 400 + (block.fallDistance * 100);
         
         maxAnimationTime = Math.max(maxAnimationTime, startDelay + animationDuration);
         
         setTimeout(() => {
           blockElement.classList.add('gravity-fall');
           setTimeout(() => {
             blockElement.classList.remove('gravity-fall');
           }, animationDuration);
         }, startDelay);
       }
     });
     
     // Call callback after all animations complete
     if (callback) {
       setTimeout(callback, maxAnimationTime + 200);
     }
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
+    this.soundManager.playNewRowSound();
+    
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
     
     // Wait a bit before rendering to avoid conflicts
     setTimeout(() => {
       this.renderGrid();
       
       // Add new row animation with smoother staggering
       const animationDuration = 500;
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
         if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
           setTimeout(() => {
             blockElement.classList.add('new-row');
             setTimeout(() => blockElement.classList.remove('new-row'), animationDuration);
           }, col * 80);
         }
       }
       
       // Wait for all animations to complete before checking matches
       const totalAnimationTime = (this.GRID_WIDTH - 1) * 80 + animationDuration;
       setTimeout(() => this.checkMatches(), totalAnimationTime + 100);
     }, 50);
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
       this.fallSpeed = Math.max(2000, 5000 - (this.level - 1) * 100);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
+    
+    // Check for level up
+    if (this.level > this.previousLevel) {
+      this.soundManager.playLevelUpSound();
+      this.previousLevel = this.level;
+    }
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
     this.updateCursor(); // Hide cursor when paused
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
     this.updateCursor(); // Show cursor when resumed
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.updateCursor(); // Hide cursor when quitting
     this.showLeaderboard();
   }
 
   async gameOver() {
     this.gameRunning = false;
+    this.soundManager.playGameOverSound();
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
     if (this.currentUser) {
       await this.saveScore();
       const position = await this.getScorePosition();
       this.elements.scoreMessage.textContent = position <= 20 ? 
         `🎉 You made it to #${position} on the leaderboard!` : 
         `Your best score has been saved!`;
       this.elements.anonymousMessage.style.display = 'none';
     } else {
       this.elements.scoreMessage.textContent = '';
       this.elements.anonymousMessage.style.display = 'block';
     }
     
     this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
+  setupVolumeControls() {
+    // Initialize volume slider with current volume
+    this.elements.volumeSlider.value = Math.round(this.soundManager.volume * 100);
+    this.updateVolumeIcon();
+
+    // Volume button click handler
+    this.elements.volumeBtn.addEventListener('click', (e) => {
+      e.stopPropagation();
+      this.soundManager.playButtonSound();
+      this.elements.volumeSliderContainer.classList.toggle('active');
+    });
+
+    // Volume slider change handler
+    this.elements.volumeSlider.addEventListener('input', (e) => {
+      const volume = parseInt(e.target.value) / 100;
+      this.soundManager.setVolume(volume);
+      this.soundManager.muted = false; // Unmute when adjusting volume
+      this.updateVolumeIcon();
+    });
+
+    // Close volume slider when clicking outside
+    document.addEventListener('click', (e) => {
+      if (!this.elements.volumeBtn.contains(e.target) && 
+          !this.elements.volumeSliderContainer.contains(e.target)) {
+        this.elements.volumeSliderContainer.classList.remove('active');
+      }
+    });
+
+    // Mute/unmute on right click or double click
+    this.elements.volumeBtn.addEventListener('dblclick', (e) => {
+      e.preventDefault();
+      this.soundManager.toggleMute();
+      this.updateVolumeIcon();
+    });
+
+    // Prevent volume slider from closing when clicking on it
+    this.elements.volumeSliderContainer.addEventListener('click', (e) => {
+      e.stopPropagation();
+    });
+  }
+
+  updateVolumeIcon() {
+    const volume = this.soundManager.volume;
+    const muted = this.soundManager.muted;
+    
+    if (muted || volume === 0) {
+      this.elements.volumeBtn.textContent = '🔇';
+      this.elements.volumeBtn.classList.add('muted');
+    } else if (volume < 0.3) {
+      this.elements.volumeBtn.textContent = '🔉';
+      this.elements.volumeBtn.classList.remove('muted');
+    } else {
+      this.elements.volumeBtn.textContent = '🔊';
+      this.elements.volumeBtn.classList.remove('muted');
+    }
+    
+    // Update slider position
+    this.elements.volumeSlider.value = Math.round(volume * 100);
+  }
+
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
 });

Preview

Falco

Got

Uncaught DOMException: Window.localStorage getter: Forbidden in a sandboxed document without the 'allow-same-origin' flag.
Web-Artifact-Creator-bot
Thinking...The error indicates that the iframe where the artifact is running doesn't have access to localStorage due to sandboxing restrictions. I need to fix the sound manager to not rely on localStorage for storing volume preferences, or handle the error gracefully and provide fallback behavior.

The issue is in the SoundManager class where it tries to access localStorage in the loadVolumePreferences() and saveVolumePreferences() methods. I need to wrap these in try-catch blocks and provide fallback behavior.

Change Description
Fix the localStorage error by updating the SoundManager to handle sandboxed environment:

**localStorage Fix:**
- Wrap all localStorage access in try-catch blocks
- Provide fallback behavior when localStorage is not available
- Don't break the game if localStorage access fails

**Changes needed in SoundManager:**

1. **Update saveVolumePreferences() method:**
   - Wrap localStorage.setItem calls in try-catch
   - Silently fail if localStorage is not available

2. **Update loadVolumePreferences() method:**
   - Wrap localStorage.getItem calls in try-catch
   - Use default values if localStorage access fails

3. **Add error handling:**
   - Add a property to track if localStorage is available
   - Gracefully degrade when not available

**Specific code changes:**
- In saveVolumePreferences(): add try-catch around localStorage calls
- In loadVolumePreferences(): add try-catch around localStorage calls
- Make sure the game continues to work even without persistent volume settings

This will ensure the game works in sandboxed environments while still providing volume persistence when possible.
View Changes

JS Changes

 class SoundManager {
   constructor() {
     this.audioContext = null;
     this.masterGain = null;
     this.volume = 0.7;
     this.muted = false;
     this.isInitialized = false;
     
     // Load volume preferences
     this.loadVolumePreferences();
   }
 
   async initialize() {
     if (this.isInitialized) return;
     
     try {
       this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
       this.masterGain = this.audioContext.createGain();
       this.masterGain.connect(this.audioContext.destination);
       this.updateVolume();
       this.isInitialized = true;
     } catch (error) {
       console.warn('Web Audio API not supported:', error);
     }
   }
 
   updateVolume() {
     if (this.masterGain) {
       this.masterGain.gain.value = this.muted ? 0 : this.volume;
     }
   }
 
   setVolume(volume) {
     this.volume = Math.max(0, Math.min(1, volume));
     this.updateVolume();
     this.saveVolumePreferences();
   }
 
   toggleMute() {
     this.muted = !this.muted;
     this.updateVolume();
     this.saveVolumePreferences();
   }
 
   saveVolumePreferences() {
+    try {
+      localStorage.setItem('connect3_volume', this.volume.toString());
+      localStorage.setItem('connect3_muted', this.muted.toString());
+    } catch (error) {
+      // Silently fail if localStorage is not available (sandboxed environment)
+      console.warn('Volume preferences could not be saved:', error);
+    }
-    localStorage.setItem('connect3_volume', this.volume.toString());
-    localStorage.setItem('connect3_muted', this.muted.toString());
   }
 
   loadVolumePreferences() {
+    try {
+      const savedVolume = localStorage.getItem('connect3_volume');
+      const savedMuted = localStorage.getItem('connect3_muted');
+      
+      if (savedVolume !== null) {
+        this.volume = parseFloat(savedVolume);
+      }
+      if (savedMuted !== null) {
+        this.muted = savedMuted === 'true';
+      }
+    } catch (error) {
+      // Use default values if localStorage is not available
+      console.warn('Volume preferences could not be loaded, using defaults:', error);
+      this.volume = 0.7;
+      this.muted = false;
-    const savedVolume = localStorage.getItem('connect3_volume');
-    const savedMuted = localStorage.getItem('connect3_muted');
-    
-    if (savedVolume !== null) {
-      this.volume = parseFloat(savedVolume);
     }
-    if (savedMuted !== null) {
-      this.muted = savedMuted === 'true';
-    }
   }
 
   createOscillator(frequency, type = 'sine', duration = 0.3) {
     if (!this.isInitialized) return null;
     
     const oscillator = this.audioContext.createOscillator();
     const gainNode = this.audioContext.createGain();
     
     oscillator.type = type;
     oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime);
     
     gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
     gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
     
     oscillator.connect(gainNode);
     gainNode.connect(this.masterGain);
     
     return { oscillator, gainNode, duration };
   }
 
   createFilteredNoise(frequency, duration = 0.2) {
     if (!this.isInitialized) return null;
     
     const bufferSize = this.audioContext.sampleRate * duration;
     const noiseBuffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
     const output = noiseBuffer.getChannelData(0);
     
     for (let i = 0; i < bufferSize; i++) {
       output[i] = Math.random() * 2 - 1;
     }
     
     const whiteNoise = this.audioContext.createBufferSource();
     whiteNoise.buffer = noiseBuffer;
     
     const bandpass = this.audioContext.createBiquadFilter();
     bandpass.type = 'bandpass';
     bandpass.frequency.value = frequency;
     bandpass.Q.value = 30;
     
     const gainNode = this.audioContext.createGain();
     gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
     gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
     
     whiteNoise.connect(bandpass);
     bandpass.connect(gainNode);
     gainNode.connect(this.masterGain);
     
     return { source: whiteNoise, duration };
   }
 
   playSwapSound() {
     if (!this.isInitialized) return;
     
     const sound1 = this.createOscillator(400, 'sine', 0.15);
     const sound2 = this.createOscillator(600, 'sine', 0.15);
     
     if (sound1 && sound2) {
       sound1.oscillator.start();
       sound1.oscillator.stop(this.audioContext.currentTime + sound1.duration);
       
       setTimeout(() => {
         sound2.oscillator.start();
         sound2.oscillator.stop(this.audioContext.currentTime + sound2.duration);
       }, 50);
     }
   }
 
   playMatchSound(blockCount = 3) {
     if (!this.isInitialized) return;
     
     const baseFreq = 220 + (blockCount * 40);
     const sound = this.createOscillator(baseFreq, 'triangle', 0.4);
     const harmonic = this.createOscillator(baseFreq * 1.5, 'sine', 0.4);
     
     if (sound && harmonic) {
       sound.gainNode.gain.setValueAtTime(0.4, this.audioContext.currentTime);
       harmonic.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       
       sound.oscillator.start();
       harmonic.oscillator.start();
       
       sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
       harmonic.oscillator.stop(this.audioContext.currentTime + harmonic.duration);
     }
   }
 
   playComboSound(comboLevel) {
     if (!this.isInitialized) return;
     
     const frequencies = [440, 550, 660, 770, 880, 990, 1100, 1210, 1320, 1430];
     const freq = frequencies[Math.min(comboLevel - 2, frequencies.length - 1)];
     
     // Main tone
     const main = this.createOscillator(freq, 'sawtooth', 0.6);
     // Higher harmonic
     const high = this.createOscillator(freq * 1.5, 'sine', 0.4);
     // Lower bass
     const bass = this.createOscillator(freq * 0.5, 'triangle', 0.8);
     
     if (main && high && bass) {
       main.gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
       high.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       bass.gainNode.gain.setValueAtTime(0.4, this.audioContext.currentTime);
       
       main.oscillator.start();
       high.oscillator.start();
       bass.oscillator.start();
       
       main.oscillator.stop(this.audioContext.currentTime + main.duration);
       high.oscillator.stop(this.audioContext.currentTime + high.duration);
       bass.oscillator.stop(this.audioContext.currentTime + bass.duration);
     }
   }
 
   playNewRowSound() {
     if (!this.isInitialized) return;
     
     const sound = this.createFilteredNoise(800, 0.3);
     if (sound) {
       sound.source.start();
       sound.source.stop(this.audioContext.currentTime + sound.duration);
     }
   }
 
   playGameOverSound() {
     if (!this.isInitialized) return;
     
     const frequencies = [440, 370, 311, 262];
     frequencies.forEach((freq, index) => {
       setTimeout(() => {
         const sound = this.createOscillator(freq, 'square', 0.8);
         if (sound) {
           sound.oscillator.start();
           sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
         }
       }, index * 200);
     });
   }
 
   playButtonSound() {
     if (!this.isInitialized) return;
     
     const sound = this.createOscillator(800, 'sine', 0.1);
     if (sound) {
       sound.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       sound.oscillator.start();
       sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
     }
   }
 
   playLevelUpSound() {
     if (!this.isInitialized) return;
     
     const arpeggio = [523, 659, 784, 1047]; // C5, E5, G5, C6
     arpeggio.forEach((freq, index) => {
       setTimeout(() => {
         const sound = this.createOscillator(freq, 'triangle', 0.3);
         if (sound) {
           sound.oscillator.start();
           sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
         }
       }, index * 100);
     });
   }
 }
 
 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.nextRow = [];
     this.fallSpeed = 5000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
     this.previousLevel = 1;
     
     this.soundManager = new SoundManager();
     
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
     this.setupVolumeControls();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
       leaderboardLoading: document.getElementById('leaderboard-loading'),
       volumeBtn: document.getElementById('volume-btn'),
       volumeSlider: document.getElementById('volume-slider'),
       volumeSliderContainer: document.getElementById('volume-slider-container')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
     this.updateCursor();
   }
 
   createParticleEffect(blockElement) {
     const rect = blockElement.getBoundingClientRect();
     const gridRect = this.elements.gameGrid.getBoundingClientRect();
     
     for (let i = 0; i < 3; i++) {
       const particle = document.createElement('div');
       particle.className = 'particle';
       particle.textContent = '✨';
       
       const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
       const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
       
       particle.style.left = offsetX + 'px';
       particle.style.top = offsetY + 'px';
       particle.style.animationDelay = (i * 100) + 'ms';
       
       this.elements.gameGrid.appendChild(particle);
       
       setTimeout(() => {
         if (particle.parentNode) {
           particle.parentNode.removeChild(particle);
         }
       }, 1000);
     }
   }
 
   bindEvents() {
     this.elements.startGameBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.startGame();
     });
     this.elements.pauseBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.pauseGame();
     });
     this.elements.backToMenuBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.quitToMenu();
     });
     this.elements.resumeBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.resumeGame();
     });
     this.elements.quitBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.quitToMenu();
     });
     this.elements.playAgainBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.startGame();
     });
     this.elements.viewLeaderboardBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.showLeaderboard();
     });
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
     
     // Add keyboard event listener
     document.addEventListener('keydown', (e) => this.handleKeyDown(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.fallSpeed = 5000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if adjacent horizontally or vertically
       if ((row === selectedRow && Math.abs(col - selectedCol) === 1) || 
           (col === selectedCol && Math.abs(row - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
     this.soundManager.playSwapSound();
     
     // Add swap animation
     const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
     const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
     
     // Determine swap direction and apply appropriate animations
     if (row1 === row2) {
       // Horizontal swap
       if (col2 > col1) {
         block1.classList.add('swapping-right');
         block2.classList.add('swapping-left');
       } else {
         block1.classList.add('swapping-left');
         block2.classList.add('swapping-right');
       }
     } else {
       // Vertical swap
       if (row2 > row1) {
         block1.classList.add('swapping-down');
         block2.classList.add('swapping-up');
       } else {
         block1.classList.add('swapping-up');
         block2.classList.add('swapping-down');
       }
     }
     
     setTimeout(() => {
       const temp = this.grid[row1][col1];
       this.grid[row1][col1] = this.grid[row2][col2];
       this.grid[row2][col2] = temp;
       
       this.renderGrid();
       this.checkMatches();
     }, 150);
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
   updateCursor() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('cursor'));
     
     if (this.gameRunning && !this.gamePaused) {
       const index = this.cursorRow * this.GRID_WIDTH + this.cursorCol;
       blocks[index]?.classList.add('cursor');
     }
   }
 
   moveCursor(deltaRow, deltaCol) {
     this.cursorRow = (this.cursorRow + deltaRow + this.GRID_HEIGHT) % this.GRID_HEIGHT;
     this.cursorCol = (this.cursorCol + deltaCol + this.GRID_WIDTH) % this.GRID_WIDTH;
     this.updateCursor();
   }
 
   handleKeyDown(e) {
     // Handle different game states
     if (this.elements.gameOverModal.classList.contains('active')) {
       this.handleGameOverKeys(e);
       return;
     }
     
     if (this.elements.pauseModal.classList.contains('active')) {
       this.handlePauseKeys(e);
       return;
     }
     
     if (this.elements.leaderboardScreen.classList.contains('active')) {
       this.handleMenuKeys(e);
       return;
     }
     
     if (this.gameRunning) {
       this.handleGameKeys(e);
     }
   }
 
   handleGameKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'arrowup':
       case 'w':
         e.preventDefault();
         this.moveCursor(-1, 0);
         break;
       case 'arrowdown':
       case 's':
         e.preventDefault();
         this.moveCursor(1, 0);
         break;
       case 'arrowleft':
       case 'a':
         e.preventDefault();
         this.moveCursor(0, -1);
         break;
       case 'arrowright':
       case 'd':
         e.preventDefault();
         this.moveCursor(0, 1);
         break;
       case ' ':
       case 'enter':
         e.preventDefault();
         this.handleCursorSelect();
         break;
       case 'escape':
         e.preventDefault();
         if (this.gamePaused) {
           this.resumeGame();
         } else {
           this.pauseGame();
         }
         break;
     }
   }
 
   handleGameOverKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'r':
         e.preventDefault();
         this.startGame();
         break;
       case 'escape':
       case 'enter':
         e.preventDefault();
         this.showLeaderboard();
         break;
     }
   }
 
   handlePauseKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'escape':
       case 'enter':
       case ' ':
         e.preventDefault();
         this.resumeGame();
         break;
       case 'q':
         e.preventDefault();
         this.quitToMenu();
         break;
     }
   }
 
   handleMenuKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'enter':
       case ' ':
         e.preventDefault();
         this.startGame();
         break;
     }
   }
 
   handleCursorSelect() {
     if (!this.gameRunning || this.gamePaused) return;
     
     // Check if there's a block at cursor position
     if (!this.grid[this.cursorRow][this.cursorCol]) return;
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if cursor is on selected block (deselect)
       if (this.cursorRow === selectedRow && this.cursorCol === selectedCol) {
         this.clearSelection();
         return;
       }
       
       // Check if adjacent horizontally or vertically
       if ((this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) || 
           (this.cursorCol === selectedCol && Math.abs(this.cursorRow - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row: this.cursorRow, col: this.cursorCol };
       this.updateSelection();
     }
   }
 
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
       this.removeMatches(matches, () => {
         // After blocks are removed, apply gravity
         setTimeout(() => {
           this.applyGravity(() => {
             // After gravity is complete, check for new matches
             setTimeout(() => this.checkMatches(), 200);
           });
         }, 150);
       });
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
   removeMatches(matches, callback) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
     // Play appropriate sounds
     if (this.combo > 1) {
       this.soundManager.playComboSound(this.combo);
     } else {
       this.soundManager.playMatchSound(matches.length);
     }
     
     // Add matched animation before removing
     matches.forEach(({ row, col }, index) => {
       const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('matched');
           blockElement.classList.add('sound-pulse');
           if (this.combo >= 3) {
             this.createParticleEffect(blockElement);
           }
         }, index * 50);
       }
     });
     
     // Remove blocks after animation and call callback
     setTimeout(() => {
       matches.forEach(({ row, col }) => {
         this.grid[row][col] = null;
       });
       this.renderGrid();
       this.updateDisplay();
       
       if (this.combo > 1) {
         this.elements.comboText.textContent = `${this.combo}x COMBO!`;
         if (this.combo >= 7) {
           document.getElementById('app').classList.add('screen-shake');
           setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
         }
       } else {
         this.elements.comboText.textContent = '';
       }
       
       if (callback) callback();
     }, 600);
   }
 
   applyGravity(callback) {
     const fallingBlocks = [];
     const columnData = [];
     
     // Analyze each column and track falling blocks
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
       const originalPositions = [];
       
       // Collect non-null blocks and their original positions
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
           originalPositions.push(row);
         }
       }
       
       columnData[col] = { blocks: column, originalPositions };
       
       // Clear the column
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
         this.grid[row][col] = null;
       }
       
       // Track falling blocks with distance information
       for (let i = 0; i < column.length; i++) {
         const oldRow = originalPositions[i];
         const newRow = this.GRID_HEIGHT - 1 - i;
         
         if (newRow > oldRow) {
           fallingBlocks.push({ 
             col, 
             oldRow, 
             newRow, 
             block: column[i],
             fallDistance: newRow - oldRow
           });
         }
       }
       
       // Place blocks in their new positions
       for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
     // If no blocks need to fall, just call callback
     if (fallingBlocks.length === 0) {
       this.renderGrid();
       if (callback) callback();
       return;
     }
     
     // Render grid first
     this.renderGrid();
     
     // Apply falling animations with staggered timing
     let maxAnimationTime = 0;
     fallingBlocks.forEach((block, index) => {
       const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
       if (blockElement) {
         // Stagger animation start based on column and fall distance
         const startDelay = (block.col * 50) + (block.fallDistance * 20);
         const animationDuration = 400 + (block.fallDistance * 100);
         
         maxAnimationTime = Math.max(maxAnimationTime, startDelay + animationDuration);
         
         setTimeout(() => {
           blockElement.classList.add('gravity-fall');
           setTimeout(() => {
             blockElement.classList.remove('gravity-fall');
           }, animationDuration);
         }, startDelay);
       }
     });
     
     // Call callback after all animations complete
     if (callback) {
       setTimeout(callback, maxAnimationTime + 200);
     }
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
     this.soundManager.playNewRowSound();
     
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
     
     // Wait a bit before rendering to avoid conflicts
     setTimeout(() => {
       this.renderGrid();
       
       // Add new row animation with smoother staggering
       const animationDuration = 500;
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
         if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
           setTimeout(() => {
             blockElement.classList.add('new-row');
             setTimeout(() => blockElement.classList.remove('new-row'), animationDuration);
           }, col * 80);
         }
       }
       
       // Wait for all animations to complete before checking matches
       const totalAnimationTime = (this.GRID_WIDTH - 1) * 80 + animationDuration;
       setTimeout(() => this.checkMatches(), totalAnimationTime + 100);
     }, 50);
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
       this.fallSpeed = Math.max(2000, 5000 - (this.level - 1) * 100);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
     
     // Check for level up
     if (this.level > this.previousLevel) {
       this.soundManager.playLevelUpSound();
       this.previousLevel = this.level;
     }
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
     this.updateCursor(); // Hide cursor when paused
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
     this.updateCursor(); // Show cursor when resumed
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.updateCursor(); // Hide cursor when quitting
     this.showLeaderboard();
   }
 
   async gameOver() {
     this.gameRunning = false;
     this.soundManager.playGameOverSound();
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
     if (this.currentUser) {
       await this.saveScore();
       const position = await this.getScorePosition();
       this.elements.scoreMessage.textContent = position <= 20 ? 
         `🎉 You made it to #${position} on the leaderboard!` : 
         `Your best score has been saved!`;
       this.elements.anonymousMessage.style.display = 'none';
     } else {
       this.elements.scoreMessage.textContent = '';
       this.elements.anonymousMessage.style.display = 'block';
     }
     
     this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
   setupVolumeControls() {
     // Initialize volume slider with current volume
     this.elements.volumeSlider.value = Math.round(this.soundManager.volume * 100);
     this.updateVolumeIcon();
 
     // Volume button click handler
     this.elements.volumeBtn.addEventListener('click', (e) => {
       e.stopPropagation();
       this.soundManager.playButtonSound();
       this.elements.volumeSliderContainer.classList.toggle('active');
     });
 
     // Volume slider change handler
     this.elements.volumeSlider.addEventListener('input', (e) => {
       const volume = parseInt(e.target.value) / 100;
       this.soundManager.setVolume(volume);
       this.soundManager.muted = false; // Unmute when adjusting volume
       this.updateVolumeIcon();
     });
 
     // Close volume slider when clicking outside
     document.addEventListener('click', (e) => {
       if (!this.elements.volumeBtn.contains(e.target) && 
           !this.elements.volumeSliderContainer.contains(e.target)) {
         this.elements.volumeSliderContainer.classList.remove('active');
       }
     });
 
     // Mute/unmute on right click or double click
     this.elements.volumeBtn.addEventListener('dblclick', (e) => {
       e.preventDefault();
       this.soundManager.toggleMute();
       this.updateVolumeIcon();
     });
 
     // Prevent volume slider from closing when clicking on it
     this.elements.volumeSliderContainer.addEventListener('click', (e) => {
       e.stopPropagation();
     });
   }
 
   updateVolumeIcon() {
     const volume = this.soundManager.volume;
     const muted = this.soundManager.muted;
     
     if (muted || volume === 0) {
       this.elements.volumeBtn.textContent = '🔇';
       this.elements.volumeBtn.classList.add('muted');
     } else if (volume < 0.3) {
       this.elements.volumeBtn.textContent = '🔉';
       this.elements.volumeBtn.classList.remove('muted');
     } else {
       this.elements.volumeBtn.textContent = '🔊';
       this.elements.volumeBtn.classList.remove('muted');
     }
     
     // Update slider position
     this.elements.volumeSlider.value = Math.round(volume * 100);
   }
 
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
 });

Preview

Falco

Can you make compatible to be played in a gamepad?

Web-Artifact-Creator-bot
Thinking...The user wants to add gamepad support to the Connect 3 puzzle game. This would involve:
  1. Adding gamepad detection and connection handling
  2. Mapping gamepad buttons to game controls
  3. Handling gamepad input in different game states
  4. Providing visual feedback for gamepad controls
  5. Adding gamepad-specific UI hints

I need to:

  • Add gamepad API integration
  • Map buttons appropriately (D-pad for movement, face buttons for actions)
  • Handle different game states with gamepad input
  • Update the UI to show gamepad hints alongside keyboard hints
  • Make sure it works with different types of gamepads

Common gamepad mapping:

  • D-pad/Left stick: Movement
  • A button (button 0): Select/Swap
  • B button (button 1): Back/Cancel
  • Start button (button 9): Pause
  • Select button (button 8): Maybe mute/volume
Change Description
Add full gamepad support to make the game playable with controllers:

**Gamepad Integration:**
- Add GamepadManager class to handle gamepad detection and input
- Support for multiple gamepads (up to 4 players, though single player game)
- Handle gamepad connection/disconnection events
- Implement gamepad button mapping for all game controls

**Button Mapping:**
- D-pad/Left analog stick: Move cursor around grid
- A button (button 0): Select/Swap blocks (same as Space/Enter)
- B button (button 1): Back/Cancel/Clear selection
- Start button (button 9): Pause/Resume game
- Select button (button 8): Toggle volume/mute
- Right shoulder (button 5): Quick restart when game over
- Left shoulder (button 4): Back to main menu

**Gamepad States:**
- Handle gamepad input in all game states (menu, game, pause, game over)
- Add proper button repeat prevention (debouncing)
- Handle analog stick deadzone for movement
- Support both D-pad and analog stick for navigation

**UI Updates:**
- Add gamepad hints alongside keyboard hints in game footer
- Show connected gamepad status in UI
- Update control hints based on connected input devices
- Add gamepad icon indicators

**Technical Implementation:**
- Add gamepad polling in game loop
- Implement button state tracking (pressed, held, released)
- Add analog stick to digital direction conversion
- Handle gamepad vibration for feedback (if supported)
- Graceful fallback when no gamepad is connected

**Features:**
- Auto-detect gamepad connection/disconnection
- Support standard gamepad layout (Xbox-style)
- Visual feedback when gamepad buttons are pressed
- Smooth cursor movement with gamepad
- Button repeat prevention for menu navigation
View Changes

HTML Changes

 <div id="app">
   <!-- Leaderboard Screen -->
   <div id="leaderboard-screen" class="screen active">
     <div class="header">
       <h1>🧩 Connect 3 Puzzle</h1>
       <p>Match 3 or more blocks to clear them and create combos!</p>
     </div>
     
     <div class="leaderboard-container">
       <h2>🏆 Top Scores</h2>
       <div id="leaderboard-loading">Loading leaderboard...</div>
       <table id="leaderboard-table" style="display: none;">
         <thead>
           <tr>
             <th>Rank</th>
             <th>Player</th>
             <th>Score</th>
             <th>Date</th>
           </tr>
         </thead>
         <tbody id="leaderboard-body">
         </tbody>
       </table>
     </div>
     
     <button id="start-game-btn" class="primary-button">🎮 Start New Game</button>
   </div>
 
   <!-- Game Screen -->
   <div id="game-screen" class="screen">
     <div class="game-header">
       <div class="game-info">
         <div class="score-display">
           <span class="label">Score:</span>
           <span id="current-score">0</span>
         </div>
         <div class="level-display">
           <span class="label">Level:</span>
           <span id="current-level">1</span>
         </div>
         <div class="combo-display">
           <span id="combo-text"></span>
         </div>
       </div>
       <div class="game-controls">
         <div class="volume-control">
           <button id="volume-btn" class="control-button">🔊</button>
           <div id="volume-slider-container" class="volume-slider-container">
             <input type="range" id="volume-slider" class="volume-slider" min="0" max="100" value="70">
           </div>
         </div>
         <button id="pause-btn" class="control-button">⏸️</button>
         <button id="back-to-menu-btn" class="control-button">🏠</button>
       </div>
     </div>
 
     <div class="game-container">
       <div id="game-grid" class="game-grid">
         <!-- Grid will be generated by JavaScript -->
       </div>
     </div>
 
     <div class="game-footer">
       <div class="next-preview">
         <span class="label">Next Row:</span>
         <div id="next-row-preview" class="next-row">
           <!-- Preview will be generated by JavaScript -->
         </div>
       </div>
+      <div class="control-hints">
+        <div id="gamepad-status" class="gamepad-status"></div>
+        <div class="keyboard-hints">
+          <div class="hint-row">
+            <span>⌨️ <strong>Arrow Keys:</strong> Move cursor</span>
+            <span><strong>Space/Enter:</strong> Select/Swap</span>
+          </div>
+          <div class="hint-row">
+            <span><strong>Esc:</strong> Pause</span>
+            <span><strong>R:</strong> Restart (when game over)</span>
+          </div>
-      <div class="keyboard-hints">
-        <div class="hint-row">
-          <span>🎮 <strong>Arrow Keys:</strong> Move cursor</span>
-          <span><strong>Space/Enter:</strong> Select/Swap</span>
         </div>
+        <div class="gamepad-hints">
+          <div class="hint-row">
+            <span>🎮 <strong>D-pad/Stick:</strong> Move cursor</span>
+            <span><strong>A:</strong> Select/Swap</span>
+          </div>
+          <div class="hint-row">
+            <span><strong>Start:</strong> Pause</span>
+            <span><strong>RB:</strong> Quick restart</span>
+          </div>
-        <div class="hint-row">
-          <span><strong>Esc:</strong> Pause</span>
-          <span><strong>R:</strong> Restart (when game over)</span>
         </div>
       </div>
     </div>
   </div>
 
   <!-- Game Over Modal -->
   <div id="game-over-modal" class="modal">
     <div class="modal-content">
       <h2>🎯 Game Over!</h2>
       <div class="final-score">
         <span class="label">Final Score:</span>
         <span id="final-score-value">0</span>
       </div>
       <div id="score-message"></div>
       <div id="anonymous-message" style="display: none;">
         <p>Log in to save your score!</p>
         <a href="https://meta.discourse.org" target="_blank" class="login-link">Login to Discourse</a>
       </div>
       <div class="modal-buttons">
         <button id="play-again-btn" class="primary-button">🔄 Play Again</button>
         <button id="view-leaderboard-btn" class="secondary-button">🏆 View Leaderboard</button>
       </div>
     </div>
   </div>
 
   <!-- Pause Modal -->
   <div id="pause-modal" class="modal">
     <div class="modal-content">
       <h2>⏸️ Game Paused</h2>
       <div class="modal-buttons">
         <button id="resume-btn" class="primary-button">▶️ Resume</button>
         <button id="quit-btn" class="secondary-button">🏠 Main Menu</button>
       </div>
     </div>
   </div>
 </div>

CSS Changes

 #app {
   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
   width: 100%;
   max-width: 600px;
   height: 600px;
   margin: 0 auto;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 12px;
   box-shadow: 0 10px 30px rgba(0,0,0,0.3);
   overflow: hidden;
   position: relative;
 }
 
 .screen {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: none;
   flex-direction: column;
   padding: 15px;
   box-sizing: border-box;
 }
 
 .screen.active {
   display: flex;
 }
 
 .header {
   text-align: center;
   margin-bottom: 20px;
 }
 
 .header h1 {
   color: white;
   font-size: 1.8em;
   margin: 0;
   text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 }
 
 .header p {
   color: rgba(255,255,255,0.9);
   margin: 5px 0 0 0;
   font-size: 0.95em;
 }
 
 .leaderboard-container {
   flex: 1;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 15px;
   margin-bottom: 15px;
   overflow-y: auto;
 }
 
 .leaderboard-container h2 {
   margin: 0 0 15px 0;
   color: #333;
   text-align: center;
 }
 
 #leaderboard-table {
   width: 100%;
   border-collapse: collapse;
   font-size: 0.9em;
 }
 
 #leaderboard-table th,
 #leaderboard-table td {
   padding: 8px 12px;
   text-align: left;
   border-bottom: 1px solid #eee;
 }
 
 #leaderboard-table th {
   background: #f8f9fa;
   font-weight: 600;
   color: #555;
 }
 
 #leaderboard-table tr:nth-child(even) {
   background: #f8f9fa;
 }
 
 .rank-medal {
   font-size: 1.2em;
 }
 
 .primary-button {
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border: none;
   color: white;
   font-size: 1.2em;
   font-weight: 600;
   padding: 15px 30px;
   border-radius: 25px;
   cursor: pointer;
   box-shadow: 0 4px 15px rgba(0,0,0,0.2);
   transition: all 0.3s ease;
 }
 
 .primary-button:hover {
   transform: translateY(-2px);
   box-shadow: 0 6px 20px rgba(0,0,0,0.3);
 }
 
 .secondary-button {
   background: rgba(255,255,255,0.2);
   border: 2px solid rgba(255,255,255,0.5);
   color: white;
   font-size: 1em;
   font-weight: 600;
   padding: 10px 20px;
   border-radius: 20px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .secondary-button:hover {
   background: rgba(255,255,255,0.3);
   border-color: rgba(255,255,255,0.8);
 }
 
 .game-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-bottom: 8px;
 }
 
 .game-info {
   display: flex;
   gap: 20px;
   flex-wrap: wrap;
 }
 
 .score-display, .level-display {
   font-weight: 600;
   color: #333;
 }
 
 .label {
   color: #666;
   margin-right: 5px;
 }
 
 #current-score, #current-level {
   color: #ff6b6b;
   font-size: 1.2em;
 }
 
 .combo-display {
   font-weight: bold;
   color: #feca57;
   min-width: 80px;
 }
 
 .game-controls {
   display: flex;
   gap: 10px;
   align-items: center;
 }
 
 .volume-control {
   position: relative;
   display: flex;
   align-items: center;
 }
 
 .volume-slider-container {
   position: absolute;
   top: 50%;
   right: 100%;
   transform: translateY(-50%);
   background: rgba(255, 255, 255, 0.95);
   border-radius: 20px;
   padding: 8px 15px;
   margin-right: 8px;
   box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
   opacity: 0;
   visibility: hidden;
   transition: all 0.3s ease;
   z-index: 100;
 }
 
 .volume-slider-container.active {
   opacity: 1;
   visibility: visible;
 }
 
 .volume-slider {
   width: 80px;
   height: 4px;
   background: #ddd;
   border-radius: 2px;
   outline: none;
   cursor: pointer;
   -webkit-appearance: none;
   appearance: none;
 }
 
 .volume-slider::-webkit-slider-thumb {
   -webkit-appearance: none;
   appearance: none;
   width: 16px;
   height: 16px;
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border-radius: 50%;
   cursor: pointer;
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
   transition: all 0.2s ease;
 }
 
 .volume-slider::-webkit-slider-thumb:hover {
   transform: scale(1.2);
 }
 
 .volume-slider::-moz-range-thumb {
   width: 16px;
   height: 16px;
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border-radius: 50%;
   cursor: pointer;
   border: none;
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
 }
 
 .volume-slider::-webkit-slider-track {
   background: linear-gradient(to right, #ff6b6b 0%, #feca57 100%);
   height: 4px;
   border-radius: 2px;
 }
 
 .volume-slider::-moz-range-track {
   background: linear-gradient(to right, #ff6b6b 0%, #feca57 100%);
   height: 4px;
   border-radius: 2px;
   border: none;
 }
 
 .control-button {
   background: rgba(255,255,255,0.2);
   border: none;
   color: white;
   font-size: 1.5em;
   padding: 8px 12px;
   border-radius: 8px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .control-button:hover {
   background: rgba(255,255,255,0.3);
 }
 
 .control-button.muted {
   opacity: 0.6;
   background: rgba(255, 255, 255, 0.1);
 }
 
 .sound-pulse {
   animation: sound-pulse 0.3s ease-out;
 }
 
 @keyframes sound-pulse {
   0% { transform: scale(1); }
   50% { transform: scale(1.05); opacity: 0.8; }
   100% { transform: scale(1); }
 }
 
 .game-container {
   flex: 1;
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 10px 0;
 }
 
 .game-grid {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   grid-template-rows: repeat(12, 1fr);
   gap: 2px;
   background: rgba(0,0,0,0.3);
   border-radius: 8px;
   padding: 6px;
   width: 240px;
   height: 320px;
 }
 
 .block {
   background: rgba(255,255,255,0.9);
   border-radius: 4px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.4em;
   cursor: pointer;
   transition: all 0.2s ease;
   user-select: none;
   border: 2px solid transparent;
   position: relative;
 }
 
 .block:hover {
   transform: scale(1.05);
   box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 }
 
 .block.selected {
   border-color: #feca57;
   box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
 }
 
 .block.cursor {
   border-color: #00d4ff;
   box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   animation: cursor-pulse 1.5s ease-in-out infinite;
 }
 
 .block.cursor.selected {
   border-color: #ff6b6b;
   box-shadow: 0 0 15px rgba(255, 107, 107, 0.8), 0 0 25px rgba(0, 212, 255, 0.6);
 }
 
 @keyframes cursor-pulse {
   0%, 100% { 
     box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   }
   50% { 
     box-shadow: 0 0 25px rgba(0, 212, 255, 1), inset 0 0 15px rgba(0, 212, 255, 0.5);
   }
 }
 
 .block.falling {
   animation: fall 0.4s ease-out;
 }
 
 .block.matched {
   animation: matched-pop 0.5s ease-out forwards;
 }
 
 .block.swapping-right {
   animation: swap-right 0.3s ease-in-out;
 }
 
 .block.swapping-left {
   animation: swap-left 0.3s ease-in-out;
 }
 
 .block.swapping-up {
   animation: swap-up 0.3s ease-in-out;
 }
 
 .block.swapping-down {
   animation: swap-down 0.3s ease-in-out;
 }
 
 .block.new-row {
   animation: slide-up 0.4s ease-out;
 }
 
 .block.gravity-fall {
   animation: gravity-drop 0.3s ease-in;
 }
 
 @keyframes fall {
   from { 
     transform: translateY(-100%);
     opacity: 0.7;
   }
   to { 
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes matched-pop {
   0% { 
     transform: scale(1);
     opacity: 1;
   }
   30% { 
     transform: scale(1.4) rotate(10deg);
     opacity: 1;
     background: linear-gradient(45deg, #ff6b6b, #feca57);
   }
   70% { 
     transform: scale(1.6) rotate(-5deg);
     opacity: 0.8;
   }
   100% { 
     transform: scale(0) rotate(180deg);
     opacity: 0;
   }
 }
 
 @keyframes swap-right {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(100% + 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-left {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(-100% - 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-up {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(-100% - 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes swap-down {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(100% + 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes slide-up {
   0% {
     transform: translateY(100%);
     opacity: 0.6;
   }
   30% {
     transform: translateY(50%);
     opacity: 0.8;
   }
   100% {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes gravity-drop {
   0% {
     transform: translateY(-100%);
     opacity: 0.8;
   }
   60% {
     transform: translateY(10%);
     opacity: 0.9;
   }
   100% {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 .game-footer {
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-top: 8px;
 }
 
+.control-hints {
-.keyboard-hints {
   margin-top: 10px;
+}
+
+.gamepad-status {
+  text-align: center;
+  font-size: 0.75em;
+  color: #28a745;
+  font-weight: 600;
+  margin-bottom: 8px;
+  display: none;
+}
+
+.keyboard-hints, .gamepad-hints {
   font-size: 0.8em;
   color: #666;
+  margin: 5px 0;
 }
 
+.gamepad-hints {
+  border-top: 1px solid #eee;
+  padding-top: 8px;
+  margin-top: 8px;
+}
+
 .hint-row {
   display: flex;
   justify-content: space-between;
   margin: 2px 0;
   gap: 15px;
 }
 
 .hint-row span {
   flex: 1;
 }
 
 .hint-row strong {
   color: #333;
 }
 
 .next-row {
   display: flex;
   gap: 5px;
   margin-top: 5px;
 }
 
 .next-block {
   width: 30px;
   height: 30px;
   border-radius: 4px;
   background: rgba(255,255,255,0.9);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.2em;
   border: 1px solid #ddd;
 }
 
 .modal {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0,0,0,0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 1000;
 }
 
 .modal.active {
   display: flex;
 }
 
 .modal-content {
   background: white;
   border-radius: 12px;
   padding: 30px;
   text-align: center;
   max-width: 400px;
   width: 90%;
   box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 }
 
 .modal-content h2 {
   margin: 0 0 20px 0;
   color: #333;
 }
 
 .final-score {
   margin: 20px 0;
   font-size: 1.3em;
 }
 
 #final-score-value {
   color: #ff6b6b;
   font-weight: bold;
   font-size: 1.5em;
 }
 
 .modal-buttons {
   display: flex;
   gap: 15px;
   justify-content: center;
   margin-top: 25px;
   flex-wrap: wrap;
 }
 
 .login-link {
   display: inline-block;
   background: #0088cc;
   color: white;
   padding: 10px 20px;
   border-radius: 20px;
   text-decoration: none;
   margin: 10px 0;
   transition: background 0.3s ease;
 }
 
 .login-link:hover {
   background: #0066aa;
 }
 
 #leaderboard-loading {
   text-align: center;
   color: #666;
   font-style: italic;
   padding: 40px;
 }
 
 #score-message {
   margin: 15px 0;
   font-weight: 600;
   color: #333;
 }
 
 .screen-shake {
   animation: shake 0.3s ease-in-out;
 }
 
 @keyframes shake {
   0%, 100% { transform: translateX(0) translateY(0); }
   25% { transform: translateX(-2px) translateY(-1px); }
   50% { transform: translateX(2px) translateY(1px); }
   75% { transform: translateX(-1px) translateY(-2px); }
 }
 
 .combo-particles {
   position: absolute;
   pointer-events: none;
   width: 100%;
   height: 100%;
 }
 
 .particle {
   position: absolute;
   font-size: 1.2em;
   animation: particle-burst 1s ease-out forwards;
   pointer-events: none;
 }
 
 @keyframes particle-burst {
   0% {
     opacity: 1;
     transform: scale(0.5);
   }
   50% {
     opacity: 0.8;
     transform: scale(1.2);
   }
   100% {
     opacity: 0;
     transform: scale(0.3) translateY(-50px);
   }
 }
 
 @media (max-width: 480px) {
   #app {
     border-radius: 0;
     height: 100vh;
     max-width: 100%;
   }
   
   .screen {
     padding: 10px;
   }
   
   .game-grid {
     width: 200px;
     height: 280px;
   }
   
   .block {
     font-size: 1.2em;
   }
   
   .game-info {
     font-size: 0.85em;
   }
   
   .modal-content {
     width: 95%;
     padding: 20px;
   }
   
   .header h1 {
     font-size: 1.5em;
   }
   
   .header p {
     font-size: 0.85em;
   }
+  
+  .keyboard-hints, .gamepad-hints {
+    font-size: 0.7em;
+  }
+  
+  .hint-row {
+    gap: 10px;
+  }
 }

JS Changes

+class GamepadManager {
+  constructor() {
+    this.gamepads = {};
+    this.buttonStates = {};
+    this.lastButtonStates = {};
+    this.analogStates = {};
+    this.deadzone = 0.3;
+    this.repeatDelay = 200; // ms
+    this.lastRepeatTime = {};
+    this.connected = false;
+    
+    this.buttonMapping = {
+      FACE_DOWN: 0,    // A button - Select/Swap
+      FACE_RIGHT: 1,   // B button - Back/Cancel
+      FACE_LEFT: 2,    // X button - (unused)
+      FACE_UP: 3,      // Y button - (unused)
+      LEFT_SHOULDER: 4,  // LB - Back to menu
+      RIGHT_SHOULDER: 5, // RB - Quick restart
+      LEFT_TRIGGER: 6,   // LT - (unused)
+      RIGHT_TRIGGER: 7,  // RT - (unused)
+      SELECT: 8,       // Select/Back - Volume toggle
+      START: 9,        // Start - Pause/Resume
+      LEFT_STICK: 10,  // LS click - (unused)
+      RIGHT_STICK: 11, // RS click - (unused)
+      DPAD_UP: 12,     // D-pad up
+      DPAD_DOWN: 13,   // D-pad down
+      DPAD_LEFT: 14,   // D-pad left
+      DPAD_RIGHT: 15   // D-pad right
+    };
+    
+    this.bindEvents();
+  }
+
+  bindEvents() {
+    window.addEventListener('gamepadconnected', (e) => {
+      this.onGamepadConnected(e.gamepad);
+    });
+    
+    window.addEventListener('gamepaddisconnected', (e) => {
+      this.onGamepadDisconnected(e.gamepad);
+    });
+  }
+
+  onGamepadConnected(gamepad) {
+    console.log(`Gamepad connected: ${gamepad.id}`);
+    this.gamepads[gamepad.index] = gamepad;
+    this.buttonStates[gamepad.index] = {};
+    this.lastButtonStates[gamepad.index] = {};
+    this.analogStates[gamepad.index] = { x: 0, y: 0 };
+    this.lastRepeatTime[gamepad.index] = {};
+    this.connected = true;
+    
+    // Update UI to show gamepad is connected
+    this.updateConnectionStatus();
+  }
+
+  onGamepadDisconnected(gamepad) {
+    console.log(`Gamepad disconnected: ${gamepad.id}`);
+    delete this.gamepads[gamepad.index];
+    delete this.buttonStates[gamepad.index];
+    delete this.lastButtonStates[gamepad.index];
+    delete this.analogStates[gamepad.index];
+    delete this.lastRepeatTime[gamepad.index];
+    
+    this.connected = Object.keys(this.gamepads).length > 0;
+    this.updateConnectionStatus();
+  }
+
+  updateConnectionStatus() {
+    const statusElement = document.getElementById('gamepad-status');
+    if (statusElement) {
+      statusElement.textContent = this.connected ? '🎮 Controller Connected' : '';
+      statusElement.style.display = this.connected ? 'block' : 'none';
+    }
+  }
+
+  poll() {
+    const gamepads = navigator.getGamepads();
+    
+    for (let i = 0; i < gamepads.length; i++) {
+      if (gamepads[i] && this.gamepads[i]) {
+        this.gamepads[i] = gamepads[i];
+        this.updateButtonStates(i);
+        this.updateAnalogStates(i);
+      }
+    }
+  }
+
+  updateButtonStates(gamepadIndex) {
+    const gamepad = this.gamepads[gamepadIndex];
+    if (!gamepad) return;
+
+    this.lastButtonStates[gamepadIndex] = { ...this.buttonStates[gamepadIndex] };
+    
+    for (let i = 0; i < gamepad.buttons.length; i++) {
+      this.buttonStates[gamepadIndex][i] = gamepad.buttons[i].pressed;
+    }
+  }
+
+  updateAnalogStates(gamepadIndex) {
+    const gamepad = this.gamepads[gamepadIndex];
+    if (!gamepad) return;
+
+    // Left stick (axes 0 and 1)
+    const x = Math.abs(gamepad.axes[0]) > this.deadzone ? gamepad.axes[0] : 0;
+    const y = Math.abs(gamepad.axes[1]) > this.deadzone ? gamepad.axes[1] : 0;
+    
+    this.analogStates[gamepadIndex] = { x, y };
+  }
+
+  isButtonPressed(button, gamepadIndex = 0) {
+    return this.buttonStates[gamepadIndex] && this.buttonStates[gamepadIndex][button];
+  }
+
+  isButtonJustPressed(button, gamepadIndex = 0) {
+    const current = this.buttonStates[gamepadIndex] && this.buttonStates[gamepadIndex][button];
+    const last = this.lastButtonStates[gamepadIndex] && this.lastButtonStates[gamepadIndex][button];
+    return current && !last;
+  }
+
+  isDirectionPressed(direction, gamepadIndex = 0) {
+    const now = Date.now();
+    const repeatKey = `${gamepadIndex}_${direction}`;
+    
+    // Check D-pad buttons
+    let pressed = false;
+    switch (direction) {
+      case 'up':
+        pressed = this.isButtonPressed(this.buttonMapping.DPAD_UP, gamepadIndex);
+        break;
+      case 'down':
+        pressed = this.isButtonPressed(this.buttonMapping.DPAD_DOWN, gamepadIndex);
+        break;
+      case 'left':
+        pressed = this.isButtonPressed(this.buttonMapping.DPAD_LEFT, gamepadIndex);
+        break;
+      case 'right':
+        pressed = this.isButtonPressed(this.buttonMapping.DPAD_RIGHT, gamepadIndex);
+        break;
+    }
+
+    // Check analog stick
+    if (!pressed && this.analogStates[gamepadIndex]) {
+      const { x, y } = this.analogStates[gamepadIndex];
+      switch (direction) {
+        case 'up':
+          pressed = y < -0.5;
+          break;
+        case 'down':
+          pressed = y > 0.5;
+          break;
+        case 'left':
+          pressed = x < -0.5;
+          break;
+        case 'right':
+          pressed = x > 0.5;
+          break;
+      }
+    }
+
+    if (!pressed) {
+      delete this.lastRepeatTime[repeatKey];
+      return false;
+    }
+
+    // Handle button repeat
+    const lastTime = this.lastRepeatTime[repeatKey] || 0;
+    if (now - lastTime >= this.repeatDelay) {
+      this.lastRepeatTime[repeatKey] = now;
+      return true;
+    }
+
+    return false;
+  }
+
+  vibrate(duration = 100, strength = 0.5) {
+    Object.values(this.gamepads).forEach(gamepad => {
+      if (gamepad.vibrationActuator) {
+        gamepad.vibrationActuator.playEffect('dual-rumble', {
+          duration,
+          strongMagnitude: strength,
+          weakMagnitude: strength * 0.5
+        });
+      }
+    });
+  }
+}
+
 class SoundManager {
   constructor() {
     this.audioContext = null;
     this.masterGain = null;
     this.volume = 0.7;
     this.muted = false;
     this.isInitialized = false;
     
     // Load volume preferences
     this.loadVolumePreferences();
   }
 
   async initialize() {
     if (this.isInitialized) return;
     
     try {
       this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
       this.masterGain = this.audioContext.createGain();
       this.masterGain.connect(this.audioContext.destination);
       this.updateVolume();
       this.isInitialized = true;
     } catch (error) {
       console.warn('Web Audio API not supported:', error);
     }
   }
 
   updateVolume() {
     if (this.masterGain) {
       this.masterGain.gain.value = this.muted ? 0 : this.volume;
     }
   }
 
   setVolume(volume) {
     this.volume = Math.max(0, Math.min(1, volume));
     this.updateVolume();
     this.saveVolumePreferences();
   }
 
   toggleMute() {
     this.muted = !this.muted;
     this.updateVolume();
     this.saveVolumePreferences();
   }
 
   saveVolumePreferences() {
     try {
       localStorage.setItem('connect3_volume', this.volume.toString());
       localStorage.setItem('connect3_muted', this.muted.toString());
     } catch (error) {
       // Silently fail if localStorage is not available (sandboxed environment)
       console.warn('Volume preferences could not be saved:', error);
     }
   }
 
   loadVolumePreferences() {
     try {
       const savedVolume = localStorage.getItem('connect3_volume');
       const savedMuted = localStorage.getItem('connect3_muted');
       
       if (savedVolume !== null) {
         this.volume = parseFloat(savedVolume);
       }
       if (savedMuted !== null) {
         this.muted = savedMuted === 'true';
       }
     } catch (error) {
       // Use default values if localStorage is not available
       console.warn('Volume preferences could not be loaded, using defaults:', error);
       this.volume = 0.7;
       this.muted = false;
     }
   }
 
   createOscillator(frequency, type = 'sine', duration = 0.3) {
     if (!this.isInitialized) return null;
     
     const oscillator = this.audioContext.createOscillator();
     const gainNode = this.audioContext.createGain();
     
     oscillator.type = type;
     oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime);
     
     gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
     gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
     
     oscillator.connect(gainNode);
     gainNode.connect(this.masterGain);
     
     return { oscillator, gainNode, duration };
   }
 
   createFilteredNoise(frequency, duration = 0.2) {
     if (!this.isInitialized) return null;
     
     const bufferSize = this.audioContext.sampleRate * duration;
     const noiseBuffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
     const output = noiseBuffer.getChannelData(0);
     
     for (let i = 0; i < bufferSize; i++) {
       output[i] = Math.random() * 2 - 1;
     }
     
     const whiteNoise = this.audioContext.createBufferSource();
     whiteNoise.buffer = noiseBuffer;
     
     const bandpass = this.audioContext.createBiquadFilter();
     bandpass.type = 'bandpass';
     bandpass.frequency.value = frequency;
     bandpass.Q.value = 30;
     
     const gainNode = this.audioContext.createGain();
     gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
     gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
     
     whiteNoise.connect(bandpass);
     bandpass.connect(gainNode);
     gainNode.connect(this.masterGain);
     
     return { source: whiteNoise, duration };
   }
 
   playSwapSound() {
     if (!this.isInitialized) return;
     
     const sound1 = this.createOscillator(400, 'sine', 0.15);
     const sound2 = this.createOscillator(600, 'sine', 0.15);
     
     if (sound1 && sound2) {
       sound1.oscillator.start();
       sound1.oscillator.stop(this.audioContext.currentTime + sound1.duration);
       
       setTimeout(() => {
         sound2.oscillator.start();
         sound2.oscillator.stop(this.audioContext.currentTime + sound2.duration);
       }, 50);
     }
   }
 
   playMatchSound(blockCount = 3) {
     if (!this.isInitialized) return;
     
     const baseFreq = 220 + (blockCount * 40);
     const sound = this.createOscillator(baseFreq, 'triangle', 0.4);
     const harmonic = this.createOscillator(baseFreq * 1.5, 'sine', 0.4);
     
     if (sound && harmonic) {
       sound.gainNode.gain.setValueAtTime(0.4, this.audioContext.currentTime);
       harmonic.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       
       sound.oscillator.start();
       harmonic.oscillator.start();
       
       sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
       harmonic.oscillator.stop(this.audioContext.currentTime + harmonic.duration);
     }
   }
 
   playComboSound(comboLevel) {
     if (!this.isInitialized) return;
     
     const frequencies = [440, 550, 660, 770, 880, 990, 1100, 1210, 1320, 1430];
     const freq = frequencies[Math.min(comboLevel - 2, frequencies.length - 1)];
     
     // Main tone
     const main = this.createOscillator(freq, 'sawtooth', 0.6);
     // Higher harmonic
     const high = this.createOscillator(freq * 1.5, 'sine', 0.4);
     // Lower bass
     const bass = this.createOscillator(freq * 0.5, 'triangle', 0.8);
     
     if (main && high && bass) {
       main.gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
       high.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       bass.gainNode.gain.setValueAtTime(0.4, this.audioContext.currentTime);
       
       main.oscillator.start();
       high.oscillator.start();
       bass.oscillator.start();
       
       main.oscillator.stop(this.audioContext.currentTime + main.duration);
       high.oscillator.stop(this.audioContext.currentTime + high.duration);
       bass.oscillator.stop(this.audioContext.currentTime + bass.duration);
     }
   }
 
   playNewRowSound() {
     if (!this.isInitialized) return;
     
     const sound = this.createFilteredNoise(800, 0.3);
     if (sound) {
       sound.source.start();
       sound.source.stop(this.audioContext.currentTime + sound.duration);
     }
   }
 
   playGameOverSound() {
     if (!this.isInitialized) return;
     
     const frequencies = [440, 370, 311, 262];
     frequencies.forEach((freq, index) => {
       setTimeout(() => {
         const sound = this.createOscillator(freq, 'square', 0.8);
         if (sound) {
           sound.oscillator.start();
           sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
         }
       }, index * 200);
     });
   }
 
   playButtonSound() {
     if (!this.isInitialized) return;
     
     const sound = this.createOscillator(800, 'sine', 0.1);
     if (sound) {
       sound.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       sound.oscillator.start();
       sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
     }
   }
 
   playLevelUpSound() {
     if (!this.isInitialized) return;
     
     const arpeggio = [523, 659, 784, 1047]; // C5, E5, G5, C6
     arpeggio.forEach((freq, index) => {
       setTimeout(() => {
         const sound = this.createOscillator(freq, 'triangle', 0.3);
         if (sound) {
           sound.oscillator.start();
           sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
         }
       }, index * 100);
     });
   }
 }
 
 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.nextRow = [];
     this.fallSpeed = 5000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
     this.previousLevel = 1;
     
     this.soundManager = new SoundManager();
+    this.gamepadManager = new GamepadManager();
     
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
     this.setupVolumeControls();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
       leaderboardLoading: document.getElementById('leaderboard-loading'),
       volumeBtn: document.getElementById('volume-btn'),
       volumeSlider: document.getElementById('volume-slider'),
       volumeSliderContainer: document.getElementById('volume-slider-container')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
     this.updateCursor();
   }
 
   createParticleEffect(blockElement) {
     const rect = blockElement.getBoundingClientRect();
     const gridRect = this.elements.gameGrid.getBoundingClientRect();
     
     for (let i = 0; i < 3; i++) {
       const particle = document.createElement('div');
       particle.className = 'particle';
       particle.textContent = '✨';
       
       const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
       const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
       
       particle.style.left = offsetX + 'px';
       particle.style.top = offsetY + 'px';
       particle.style.animationDelay = (i * 100) + 'ms';
       
       this.elements.gameGrid.appendChild(particle);
       
       setTimeout(() => {
         if (particle.parentNode) {
           particle.parentNode.removeChild(particle);
         }
       }, 1000);
     }
   }
 
   bindEvents() {
     this.elements.startGameBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.startGame();
     });
     this.elements.pauseBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.pauseGame();
     });
     this.elements.backToMenuBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.quitToMenu();
     });
     this.elements.resumeBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.resumeGame();
     });
     this.elements.quitBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.quitToMenu();
     });
     this.elements.playAgainBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.startGame();
     });
     this.elements.viewLeaderboardBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.showLeaderboard();
     });
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
     
     // Add keyboard event listener
     document.addEventListener('keydown', (e) => this.handleKeyDown(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.fallSpeed = 5000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if adjacent horizontally or vertically
       if ((row === selectedRow && Math.abs(col - selectedCol) === 1) || 
           (col === selectedCol && Math.abs(row - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
     this.soundManager.playSwapSound();
     
     // Add swap animation
     const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
     const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
     
     // Determine swap direction and apply appropriate animations
     if (row1 === row2) {
       // Horizontal swap
       if (col2 > col1) {
         block1.classList.add('swapping-right');
         block2.classList.add('swapping-left');
       } else {
         block1.classList.add('swapping-left');
         block2.classList.add('swapping-right');
       }
     } else {
       // Vertical swap
       if (row2 > row1) {
         block1.classList.add('swapping-down');
         block2.classList.add('swapping-up');
       } else {
         block1.classList.add('swapping-up');
         block2.classList.add('swapping-down');
       }
     }
     
     setTimeout(() => {
       const temp = this.grid[row1][col1];
       this.grid[row1][col1] = this.grid[row2][col2];
       this.grid[row2][col2] = temp;
       
       this.renderGrid();
       this.checkMatches();
     }, 150);
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
   updateCursor() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('cursor'));
     
     if (this.gameRunning && !this.gamePaused) {
       const index = this.cursorRow * this.GRID_WIDTH + this.cursorCol;
       blocks[index]?.classList.add('cursor');
     }
   }
 
   moveCursor(deltaRow, deltaCol) {
     this.cursorRow = (this.cursorRow + deltaRow + this.GRID_HEIGHT) % this.GRID_HEIGHT;
     this.cursorCol = (this.cursorCol + deltaCol + this.GRID_WIDTH) % this.GRID_WIDTH;
     this.updateCursor();
   }
 
+  handleGamepadInput() {
+    // Handle different game states
+    if (this.elements.gameOverModal.classList.contains('active')) {
+      this.handleGameOverGamepad();
+      return;
+    }
+    
+    if (this.elements.pauseModal.classList.contains('active')) {
+      this.handlePauseGamepad();
+      return;
+    }
+    
+    if (this.elements.leaderboardScreen.classList.contains('active')) {
+      this.handleMenuGamepad();
+      return;
+    }
+    
+    if (this.gameRunning) {
+      this.handleGameGamepad();
+    }
+  }
+
+  handleGameGamepad() {
+    // Movement
+    if (this.gamepadManager.isDirectionPressed('up')) {
+      this.moveCursor(-1, 0);
+    }
+    if (this.gamepadManager.isDirectionPressed('down')) {
+      this.moveCursor(1, 0);
+    }
+    if (this.gamepadManager.isDirectionPressed('left')) {
+      this.moveCursor(0, -1);
+    }
+    if (this.gamepadManager.isDirectionPressed('right')) {
+      this.moveCursor(0, 1);
+    }
+
+    // Actions
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_DOWN)) {
+      this.handleCursorSelect();
+      this.gamepadManager.vibrate(50, 0.3);
+    }
+    
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_RIGHT)) {
+      this.clearSelection();
+      this.gamepadManager.vibrate(30, 0.2);
+    }
+    
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.START)) {
+      if (this.gamePaused) {
+        this.resumeGame();
+      } else {
+        this.pauseGame();
+      }
+      this.gamepadManager.vibrate(100, 0.4);
+    }
+    
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.SELECT)) {
+      this.soundManager.toggleMute();
+      this.updateVolumeIcon();
+      this.gamepadManager.vibrate(50, 0.2);
+    }
+    
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.LEFT_SHOULDER)) {
+      this.quitToMenu();
+      this.gamepadManager.vibrate(100, 0.4);
+    }
+  }
+
+  handleGameOverGamepad() {
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.RIGHT_SHOULDER)) {
+      this.startGame();
+      this.gamepadManager.vibrate(100, 0.5);
+    }
+    
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_DOWN) ||
+        this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.START)) {
+      this.showLeaderboard();
+      this.gamepadManager.vibrate(100, 0.4);
+    }
+    
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_RIGHT)) {
+      this.startGame();
+      this.gamepadManager.vibrate(100, 0.5);
+    }
+  }
+
+  handlePauseGamepad() {
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.START) ||
+        this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_DOWN)) {
+      this.resumeGame();
+      this.gamepadManager.vibrate(100, 0.4);
+    }
+    
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_RIGHT) ||
+        this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.LEFT_SHOULDER)) {
+      this.quitToMenu();
+      this.gamepadManager.vibrate(100, 0.4);
+    }
+  }
+
+  handleMenuGamepad() {
+    if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_DOWN) ||
+        this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.START)) {
+      this.startGame();
+      this.gamepadManager.vibrate(100, 0.5);
+    }
+  }
+
   handleKeyDown(e) {
     // Handle different game states
     if (this.elements.gameOverModal.classList.contains('active')) {
       this.handleGameOverKeys(e);
       return;
     }
     
     if (this.elements.pauseModal.classList.contains('active')) {
       this.handlePauseKeys(e);
       return;
     }
     
     if (this.elements.leaderboardScreen.classList.contains('active')) {
       this.handleMenuKeys(e);
       return;
     }
     
     if (this.gameRunning) {
       this.handleGameKeys(e);
     }
   }
 
   handleGameKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'arrowup':
       case 'w':
         e.preventDefault();
         this.moveCursor(-1, 0);
         break;
       case 'arrowdown':
       case 's':
         e.preventDefault();
         this.moveCursor(1, 0);
         break;
       case 'arrowleft':
       case 'a':
         e.preventDefault();
         this.moveCursor(0, -1);
         break;
       case 'arrowright':
       case 'd':
         e.preventDefault();
         this.moveCursor(0, 1);
         break;
       case ' ':
       case 'enter':
         e.preventDefault();
         this.handleCursorSelect();
         break;
       case 'escape':
         e.preventDefault();
         if (this.gamePaused) {
           this.resumeGame();
         } else {
           this.pauseGame();
         }
         break;
     }
   }
 
   handleGameOverKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'r':
         e.preventDefault();
         this.startGame();
         break;
       case 'escape':
       case 'enter':
         e.preventDefault();
         this.showLeaderboard();
         break;
     }
   }
 
   handlePauseKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'escape':
       case 'enter':
       case ' ':
         e.preventDefault();
         this.resumeGame();
         break;
       case 'q':
         e.preventDefault();
         this.quitToMenu();
         break;
     }
   }
 
   handleMenuKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'enter':
       case ' ':
         e.preventDefault();
         this.startGame();
         break;
     }
   }
 
   handleCursorSelect() {
     if (!this.gameRunning || this.gamePaused) return;
     
     // Check if there's a block at cursor position
     if (!this.grid[this.cursorRow][this.cursorCol]) return;
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if cursor is on selected block (deselect)
       if (this.cursorRow === selectedRow && this.cursorCol === selectedCol) {
         this.clearSelection();
         return;
       }
       
       // Check if adjacent horizontally or vertically
       if ((this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) || 
           (this.cursorCol === selectedCol && Math.abs(this.cursorRow - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row: this.cursorRow, col: this.cursorCol };
       this.updateSelection();
     }
   }
 
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
       this.removeMatches(matches, () => {
         // After blocks are removed, apply gravity
         setTimeout(() => {
           this.applyGravity(() => {
             // After gravity is complete, check for new matches
             setTimeout(() => this.checkMatches(), 200);
           });
         }, 150);
       });
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
   removeMatches(matches, callback) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
     // Play appropriate sounds
     if (this.combo > 1) {
       this.soundManager.playComboSound(this.combo);
     } else {
       this.soundManager.playMatchSound(matches.length);
     }
     
+    // Gamepad vibration for matches
+    if (this.combo > 1) {
+      this.gamepadManager.vibrate(200 + (this.combo * 50), 0.3 + (this.combo * 0.1));
+    } else {
+      this.gamepadManager.vibrate(100, 0.2);
+    }
+    
     // Add matched animation before removing
     matches.forEach(({ row, col }, index) => {
       const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('matched');
           blockElement.classList.add('sound-pulse');
           if (this.combo >= 3) {
             this.createParticleEffect(blockElement);
           }
         }, index * 50);
       }
     });
     
     // Remove blocks after animation and call callback
     setTimeout(() => {
       matches.forEach(({ row, col }) => {
         this.grid[row][col] = null;
       });
       this.renderGrid();
       this.updateDisplay();
       
       if (this.combo > 1) {
         this.elements.comboText.textContent = `${this.combo}x COMBO!`;
         if (this.combo >= 7) {
           document.getElementById('app').classList.add('screen-shake');
           setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
         }
       } else {
         this.elements.comboText.textContent = '';
       }
       
       if (callback) callback();
     }, 600);
   }
 
   applyGravity(callback) {
     const fallingBlocks = [];
     const columnData = [];
     
     // Analyze each column and track falling blocks
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
       const originalPositions = [];
       
       // Collect non-null blocks and their original positions
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
           originalPositions.push(row);
         }
       }
       
       columnData[col] = { blocks: column, originalPositions };
       
       // Clear the column
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
         this.grid[row][col] = null;
       }
       
       // Track falling blocks with distance information
       for (let i = 0; i < column.length; i++) {
         const oldRow = originalPositions[i];
         const newRow = this.GRID_HEIGHT - 1 - i;
         
         if (newRow > oldRow) {
           fallingBlocks.push({ 
             col, 
             oldRow, 
             newRow, 
             block: column[i],
             fallDistance: newRow - oldRow
           });
         }
       }
       
       // Place blocks in their new positions
       for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
     // If no blocks need to fall, just call callback
     if (fallingBlocks.length === 0) {
       this.renderGrid();
       if (callback) callback();
       return;
     }
     
     // Render grid first
     this.renderGrid();
     
     // Apply falling animations with staggered timing
     let maxAnimationTime = 0;
     fallingBlocks.forEach((block, index) => {
       const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
       if (blockElement) {
         // Stagger animation start based on column and fall distance
         const startDelay = (block.col * 50) + (block.fallDistance * 20);
         const animationDuration = 400 + (block.fallDistance * 100);
         
         maxAnimationTime = Math.max(maxAnimationTime, startDelay + animationDuration);
         
         setTimeout(() => {
           blockElement.classList.add('gravity-fall');
           setTimeout(() => {
             blockElement.classList.remove('gravity-fall');
           }, animationDuration);
         }, startDelay);
       }
     });
     
     // Call callback after all animations complete
     if (callback) {
       setTimeout(callback, maxAnimationTime + 200);
     }
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
     this.soundManager.playNewRowSound();
     
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
     
     // Wait a bit before rendering to avoid conflicts
     setTimeout(() => {
       this.renderGrid();
       
       // Add new row animation with smoother staggering
       const animationDuration = 500;
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
         if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
           setTimeout(() => {
             blockElement.classList.add('new-row');
             setTimeout(() => blockElement.classList.remove('new-row'), animationDuration);
           }, col * 80);
         }
       }
       
       // Wait for all animations to complete before checking matches
       const totalAnimationTime = (this.GRID_WIDTH - 1) * 80 + animationDuration;
       setTimeout(() => this.checkMatches(), totalAnimationTime + 100);
     }, 50);
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
+    // Poll gamepad input
+    this.gamepadManager.poll();
+    this.handleGamepadInput();
+    
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
       this.fallSpeed = Math.max(2000, 5000 - (this.level - 1) * 100);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
     
     // Check for level up
     if (this.level > this.previousLevel) {
       this.soundManager.playLevelUpSound();
       this.previousLevel = this.level;
     }
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
     this.updateCursor(); // Hide cursor when paused
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
     this.updateCursor(); // Show cursor when resumed
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.updateCursor(); // Hide cursor when quitting
     this.showLeaderboard();
   }
 
   async gameOver() {
     this.gameRunning = false;
     this.soundManager.playGameOverSound();
+    this.gamepadManager.vibrate(500, 0.8); // Strong vibration for game over
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
     if (this.currentUser) {
       await this.saveScore();
       const position = await this.getScorePosition();
       this.elements.scoreMessage.textContent = position <= 20 ? 
         `🎉 You made it to #${position} on the leaderboard!` : 
         `Your best score has been saved!`;
       this.elements.anonymousMessage.style.display = 'none';
     } else {
       this.elements.scoreMessage.textContent = '';
       this.elements.anonymousMessage.style.display = 'block';
     }
     
     this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
   setupVolumeControls() {
     // Initialize volume slider with current volume
     this.elements.volumeSlider.value = Math.round(this.soundManager.volume * 100);
     this.updateVolumeIcon();
 
     // Volume button click handler
     this.elements.volumeBtn.addEventListener('click', (e) => {
       e.stopPropagation();
       this.soundManager.playButtonSound();
       this.elements.volumeSliderContainer.classList.toggle('active');
     });
 
     // Volume slider change handler
     this.elements.volumeSlider.addEventListener('input', (e) => {
       const volume = parseInt(e.target.value) / 100;
       this.soundManager.setVolume(volume);
       this.soundManager.muted = false; // Unmute when adjusting volume
       this.updateVolumeIcon();
     });
 
     // Close volume slider when clicking outside
     document.addEventListener('click', (e) => {
       if (!this.elements.volumeBtn.contains(e.target) && 
           !this.elements.volumeSliderContainer.contains(e.target)) {
         this.elements.volumeSliderContainer.classList.remove('active');
       }
     });
 
     // Mute/unmute on right click or double click
     this.elements.volumeBtn.addEventListener('dblclick', (e) => {
       e.preventDefault();
       this.soundManager.toggleMute();
       this.updateVolumeIcon();
     });
 
     // Prevent volume slider from closing when clicking on it
     this.elements.volumeSliderContainer.addEventListener('click', (e) => {
       e.stopPropagation();
     });
   }
 
   updateVolumeIcon() {
     const volume = this.soundManager.volume;
     const muted = this.soundManager.muted;
     
     if (muted || volume === 0) {
       this.elements.volumeBtn.textContent = '🔇';
       this.elements.volumeBtn.classList.add('muted');
     } else if (volume < 0.3) {
       this.elements.volumeBtn.textContent = '🔉';
       this.elements.volumeBtn.classList.remove('muted');
     } else {
       this.elements.volumeBtn.textContent = '🔊';
       this.elements.volumeBtn.classList.remove('muted');
     }
     
     // Update slider position
     this.elements.volumeSlider.value = Math.round(volume * 100);
   }
 
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
+  
+  // Start gamepad polling
+  function pollGamepads() {
+    if (navigator.getGamepads) {
+      // The polling happens in the game loop, but we need this for connection detection
+      requestAnimationFrame(pollGamepads);
+    }
+  }
+  pollGamepads();
 });

Preview

Falco

When it grows the cursor should follow the movement, so go up 1 tile accordingly.

Also when it ends because the growth hit the limit, the game stops working for a moment before saying game over, can we have an animation play there immediatly so there is feedback to the user why moves aren’t working anymore?

Web-Artifact-Creator-bot
Thinking...The user wants two fixes:
  1. When a new row is added (the grid grows from the bottom), the cursor should move up by 1 tile to maintain its relative position
  2. When the game ends because blocks reach the top, there should be immediate feedback/animation instead of the current delay where the game becomes unresponsive

For the first issue, I need to modify the addNewRow() method to adjust the cursor position when rows are shifted up.

For the second issue, I need to add immediate visual feedback when the game over condition is detected in addNewRow(), probably with a red flash or some indication that the game is ending, and then trigger the game over sequence.

Change Description
Fix the cursor movement and game over feedback:

**Cursor Movement Fix:**
- In `addNewRow()` method, when rows shift up, move the cursor up by 1 row as well
- Ensure cursor stays within grid bounds after movement
- Update cursor position before rendering the grid

**Game Over Animation Fix:**
- Add immediate visual feedback when game over condition is detected
- Show a red flash/overlay animation on the game grid
- Add "GAME OVER" text that appears immediately
- Disable all input immediately when game over is detected
- Make the game over sequence feel more responsive

**Specific Changes:**

1. **In addNewRow() method:**
   - Before shifting rows, move cursor up: `this.cursorRow = Math.max(0, this.cursorRow - 1)`
   - This keeps the cursor on the same relative block as the grid shifts up

2. **Add immediate game over feedback:**
   - When game over condition is detected, immediately set `this.gameRunning = false`
   - Add a red flash animation to the entire game grid
   - Show immediate "GAME OVER!" text overlay
   - Add CSS animation for game over flash effect
   - Remove the delay/unresponsiveness between detection and game over modal

3. **CSS additions:**
   - Add `.game-over-flash` animation class for red screen flash
   - Add `.game-over-overlay` for immediate text feedback
   - Make animations quick and impactful

4. **Update gameOver() method:**
   - Remove any delays that cause unresponsiveness
   - Make transition to game over modal smoother
View Changes

HTML Changes

 <div id="app">
   <!-- Leaderboard Screen -->
   <div id="leaderboard-screen" class="screen active">
     <div class="header">
       <h1>🧩 Connect 3 Puzzle</h1>
       <p>Match 3 or more blocks to clear them and create combos!</p>
     </div>
     
     <div class="leaderboard-container">
       <h2>🏆 Top Scores</h2>
       <div id="leaderboard-loading">Loading leaderboard...</div>
       <table id="leaderboard-table" style="display: none;">
         <thead>
           <tr>
             <th>Rank</th>
             <th>Player</th>
             <th>Score</th>
             <th>Date</th>
           </tr>
         </thead>
         <tbody id="leaderboard-body">
         </tbody>
       </table>
     </div>
     
     <button id="start-game-btn" class="primary-button">🎮 Start New Game</button>
   </div>
 
   <!-- Game Screen -->
   <div id="game-screen" class="screen">
     <div class="game-header">
       <div class="game-info">
         <div class="score-display">
           <span class="label">Score:</span>
           <span id="current-score">0</span>
         </div>
         <div class="level-display">
           <span class="label">Level:</span>
           <span id="current-level">1</span>
         </div>
         <div class="combo-display">
           <span id="combo-text"></span>
         </div>
       </div>
       <div class="game-controls">
         <div class="volume-control">
           <button id="volume-btn" class="control-button">🔊</button>
           <div id="volume-slider-container" class="volume-slider-container">
             <input type="range" id="volume-slider" class="volume-slider" min="0" max="100" value="70">
           </div>
         </div>
         <button id="pause-btn" class="control-button">⏸️</button>
         <button id="back-to-menu-btn" class="control-button">🏠</button>
       </div>
     </div>
 
     <div class="game-container">
       <div id="game-grid" class="game-grid">
         <!-- Grid will be generated by JavaScript -->
       </div>
+      <div id="game-over-overlay" class="game-over-overlay">
+        <div class="game-over-text">GAME OVER!</div>
+      </div>
     </div>
 
     <div class="game-footer">
       <div class="next-preview">
         <span class="label">Next Row:</span>
         <div id="next-row-preview" class="next-row">
           <!-- Preview will be generated by JavaScript -->
         </div>
       </div>
       <div class="control-hints">
         <div id="gamepad-status" class="gamepad-status"></div>
         <div class="keyboard-hints">
           <div class="hint-row">
             <span>⌨️ <strong>Arrow Keys:</strong> Move cursor</span>
             <span><strong>Space/Enter:</strong> Select/Swap</span>
           </div>
           <div class="hint-row">
             <span><strong>Esc:</strong> Pause</span>
             <span><strong>R:</strong> Restart (when game over)</span>
           </div>
         </div>
         <div class="gamepad-hints">
           <div class="hint-row">
             <span>🎮 <strong>D-pad/Stick:</strong> Move cursor</span>
             <span><strong>A:</strong> Select/Swap</span>
           </div>
           <div class="hint-row">
             <span><strong>Start:</strong> Pause</span>
             <span><strong>RB:</strong> Quick restart</span>
           </div>
         </div>
       </div>
     </div>
   </div>
 
   <!-- Game Over Modal -->
   <div id="game-over-modal" class="modal">
     <div class="modal-content">
       <h2>🎯 Game Over!</h2>
       <div class="final-score">
         <span class="label">Final Score:</span>
         <span id="final-score-value">0</span>
       </div>
       <div id="score-message"></div>
       <div id="anonymous-message" style="display: none;">
         <p>Log in to save your score!</p>
         <a href="https://meta.discourse.org" target="_blank" class="login-link">Login to Discourse</a>
       </div>
       <div class="modal-buttons">
         <button id="play-again-btn" class="primary-button">🔄 Play Again</button>
         <button id="view-leaderboard-btn" class="secondary-button">🏆 View Leaderboard</button>
       </div>
     </div>
   </div>
 
   <!-- Pause Modal -->
   <div id="pause-modal" class="modal">
     <div class="modal-content">
       <h2>⏸️ Game Paused</h2>
       <div class="modal-buttons">
         <button id="resume-btn" class="primary-button">▶️ Resume</button>
         <button id="quit-btn" class="secondary-button">🏠 Main Menu</button>
       </div>
     </div>
   </div>
 </div>

CSS Changes

 #app {
   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
   width: 100%;
   max-width: 600px;
   height: 600px;
   margin: 0 auto;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 12px;
   box-shadow: 0 10px 30px rgba(0,0,0,0.3);
   overflow: hidden;
   position: relative;
 }
 
 .screen {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: none;
   flex-direction: column;
   padding: 15px;
   box-sizing: border-box;
 }
 
 .screen.active {
   display: flex;
 }
 
 .header {
   text-align: center;
   margin-bottom: 20px;
 }
 
 .header h1 {
   color: white;
   font-size: 1.8em;
   margin: 0;
   text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 }
 
 .header p {
   color: rgba(255,255,255,0.9);
   margin: 5px 0 0 0;
   font-size: 0.95em;
 }
 
 .leaderboard-container {
   flex: 1;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 15px;
   margin-bottom: 15px;
   overflow-y: auto;
 }
 
 .leaderboard-container h2 {
   margin: 0 0 15px 0;
   color: #333;
   text-align: center;
 }
 
 #leaderboard-table {
   width: 100%;
   border-collapse: collapse;
   font-size: 0.9em;
 }
 
 #leaderboard-table th,
 #leaderboard-table td {
   padding: 8px 12px;
   text-align: left;
   border-bottom: 1px solid #eee;
 }
 
 #leaderboard-table th {
   background: #f8f9fa;
   font-weight: 600;
   color: #555;
 }
 
 #leaderboard-table tr:nth-child(even) {
   background: #f8f9fa;
 }
 
 .rank-medal {
   font-size: 1.2em;
 }
 
 .primary-button {
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border: none;
   color: white;
   font-size: 1.2em;
   font-weight: 600;
   padding: 15px 30px;
   border-radius: 25px;
   cursor: pointer;
   box-shadow: 0 4px 15px rgba(0,0,0,0.2);
   transition: all 0.3s ease;
 }
 
 .primary-button:hover {
   transform: translateY(-2px);
   box-shadow: 0 6px 20px rgba(0,0,0,0.3);
 }
 
 .secondary-button {
   background: rgba(255,255,255,0.2);
   border: 2px solid rgba(255,255,255,0.5);
   color: white;
   font-size: 1em;
   font-weight: 600;
   padding: 10px 20px;
   border-radius: 20px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .secondary-button:hover {
   background: rgba(255,255,255,0.3);
   border-color: rgba(255,255,255,0.8);
 }
 
 .game-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-bottom: 8px;
 }
 
 .game-info {
   display: flex;
   gap: 20px;
   flex-wrap: wrap;
 }
 
 .score-display, .level-display {
   font-weight: 600;
   color: #333;
 }
 
 .label {
   color: #666;
   margin-right: 5px;
 }
 
 #current-score, #current-level {
   color: #ff6b6b;
   font-size: 1.2em;
 }
 
 .combo-display {
   font-weight: bold;
   color: #feca57;
   min-width: 80px;
 }
 
 .game-controls {
   display: flex;
   gap: 10px;
   align-items: center;
 }
 
 .volume-control {
   position: relative;
   display: flex;
   align-items: center;
 }
 
 .volume-slider-container {
   position: absolute;
   top: 50%;
   right: 100%;
   transform: translateY(-50%);
   background: rgba(255, 255, 255, 0.95);
   border-radius: 20px;
   padding: 8px 15px;
   margin-right: 8px;
   box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
   opacity: 0;
   visibility: hidden;
   transition: all 0.3s ease;
   z-index: 100;
 }
 
 .volume-slider-container.active {
   opacity: 1;
   visibility: visible;
 }
 
 .volume-slider {
   width: 80px;
   height: 4px;
   background: #ddd;
   border-radius: 2px;
   outline: none;
   cursor: pointer;
   -webkit-appearance: none;
   appearance: none;
 }
 
 .volume-slider::-webkit-slider-thumb {
   -webkit-appearance: none;
   appearance: none;
   width: 16px;
   height: 16px;
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border-radius: 50%;
   cursor: pointer;
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
   transition: all 0.2s ease;
 }
 
 .volume-slider::-webkit-slider-thumb:hover {
   transform: scale(1.2);
 }
 
 .volume-slider::-moz-range-thumb {
   width: 16px;
   height: 16px;
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border-radius: 50%;
   cursor: pointer;
   border: none;
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
 }
 
 .volume-slider::-webkit-slider-track {
   background: linear-gradient(to right, #ff6b6b 0%, #feca57 100%);
   height: 4px;
   border-radius: 2px;
 }
 
 .volume-slider::-moz-range-track {
   background: linear-gradient(to right, #ff6b6b 0%, #feca57 100%);
   height: 4px;
   border-radius: 2px;
   border: none;
 }
 
 .control-button {
   background: rgba(255,255,255,0.2);
   border: none;
   color: white;
   font-size: 1.5em;
   padding: 8px 12px;
   border-radius: 8px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .control-button:hover {
   background: rgba(255,255,255,0.3);
 }
 
 .control-button.muted {
   opacity: 0.6;
   background: rgba(255, 255, 255, 0.1);
 }
 
 .sound-pulse {
   animation: sound-pulse 0.3s ease-out;
 }
 
 @keyframes sound-pulse {
   0% { transform: scale(1); }
   50% { transform: scale(1.05); opacity: 0.8; }
   100% { transform: scale(1); }
 }
 
 .game-container {
   flex: 1;
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 10px 0;
 }
 
 .game-grid {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   grid-template-rows: repeat(12, 1fr);
   gap: 2px;
   background: rgba(0,0,0,0.3);
   border-radius: 8px;
   padding: 6px;
   width: 240px;
   height: 320px;
 }
 
 .block {
   background: rgba(255,255,255,0.9);
   border-radius: 4px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.4em;
   cursor: pointer;
   transition: all 0.2s ease;
   user-select: none;
   border: 2px solid transparent;
   position: relative;
 }
 
 .block:hover {
   transform: scale(1.05);
   box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 }
 
 .block.selected {
   border-color: #feca57;
   box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
 }
 
 .block.cursor {
   border-color: #00d4ff;
   box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   animation: cursor-pulse 1.5s ease-in-out infinite;
 }
 
 .block.cursor.selected {
   border-color: #ff6b6b;
   box-shadow: 0 0 15px rgba(255, 107, 107, 0.8), 0 0 25px rgba(0, 212, 255, 0.6);
 }
 
 @keyframes cursor-pulse {
   0%, 100% { 
     box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   }
   50% { 
     box-shadow: 0 0 25px rgba(0, 212, 255, 1), inset 0 0 15px rgba(0, 212, 255, 0.5);
   }
 }
 
 .block.falling {
   animation: fall 0.4s ease-out;
 }
 
 .block.matched {
   animation: matched-pop 0.5s ease-out forwards;
 }
 
 .block.swapping-right {
   animation: swap-right 0.3s ease-in-out;
 }
 
 .block.swapping-left {
   animation: swap-left 0.3s ease-in-out;
 }
 
 .block.swapping-up {
   animation: swap-up 0.3s ease-in-out;
 }
 
 .block.swapping-down {
   animation: swap-down 0.3s ease-in-out;
 }
 
 .block.new-row {
   animation: slide-up 0.4s ease-out;
 }
 
 .block.gravity-fall {
   animation: gravity-drop 0.3s ease-in;
 }
 
 @keyframes fall {
   from { 
     transform: translateY(-100%);
     opacity: 0.7;
   }
   to { 
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes matched-pop {
   0% { 
     transform: scale(1);
     opacity: 1;
   }
   30% { 
     transform: scale(1.4) rotate(10deg);
     opacity: 1;
     background: linear-gradient(45deg, #ff6b6b, #feca57);
   }
   70% { 
     transform: scale(1.6) rotate(-5deg);
     opacity: 0.8;
   }
   100% { 
     transform: scale(0) rotate(180deg);
     opacity: 0;
   }
 }
 
 @keyframes swap-right {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(100% + 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-left {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(-100% - 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-up {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(-100% - 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes swap-down {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(100% + 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes slide-up {
   0% {
     transform: translateY(100%);
     opacity: 0.6;
   }
   30% {
     transform: translateY(50%);
     opacity: 0.8;
   }
   100% {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes gravity-drop {
   0% {
     transform: translateY(-100%);
     opacity: 0.8;
   }
   60% {
     transform: translateY(10%);
     opacity: 0.9;
   }
   100% {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 .game-footer {
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-top: 8px;
 }
 
 .control-hints {
   margin-top: 10px;
 }
 
 .gamepad-status {
   text-align: center;
   font-size: 0.75em;
   color: #28a745;
   font-weight: 600;
   margin-bottom: 8px;
   display: none;
 }
 
 .keyboard-hints, .gamepad-hints {
   font-size: 0.8em;
   color: #666;
   margin: 5px 0;
 }
 
 .gamepad-hints {
   border-top: 1px solid #eee;
   padding-top: 8px;
   margin-top: 8px;
 }
 
 .hint-row {
   display: flex;
   justify-content: space-between;
   margin: 2px 0;
   gap: 15px;
 }
 
 .hint-row span {
   flex: 1;
 }
 
 .hint-row strong {
   color: #333;
 }
 
 .next-row {
   display: flex;
   gap: 5px;
   margin-top: 5px;
 }
 
 .next-block {
   width: 30px;
   height: 30px;
   border-radius: 4px;
   background: rgba(255,255,255,0.9);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.2em;
   border: 1px solid #ddd;
 }
 
 .modal {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0,0,0,0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 1000;
 }
 
 .modal.active {
   display: flex;
 }
 
 .modal-content {
   background: white;
   border-radius: 12px;
   padding: 30px;
   text-align: center;
   max-width: 400px;
   width: 90%;
   box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 }
 
 .modal-content h2 {
   margin: 0 0 20px 0;
   color: #333;
 }
 
 .final-score {
   margin: 20px 0;
   font-size: 1.3em;
 }
 
 #final-score-value {
   color: #ff6b6b;
   font-weight: bold;
   font-size: 1.5em;
 }
 
 .modal-buttons {
   display: flex;
   gap: 15px;
   justify-content: center;
   margin-top: 25px;
   flex-wrap: wrap;
 }
 
 .login-link {
   display: inline-block;
   background: #0088cc;
   color: white;
   padding: 10px 20px;
   border-radius: 20px;
   text-decoration: none;
   margin: 10px 0;
   transition: background 0.3s ease;
 }
 
 .login-link:hover {
   background: #0066aa;
 }
 
 #leaderboard-loading {
   text-align: center;
   color: #666;
   font-style: italic;
   padding: 40px;
 }
 
 #score-message {
   margin: 15px 0;
   font-weight: 600;
   color: #333;
 }
 
 .screen-shake {
   animation: shake 0.3s ease-in-out;
 }
 
 @keyframes shake {
   0%, 100% { transform: translateX(0) translateY(0); }
   25% { transform: translateX(-2px) translateY(-1px); }
   50% { transform: translateX(2px) translateY(1px); }
   75% { transform: translateX(-1px) translateY(-2px); }
 }
 
+.game-over-flash {
+  animation: game-over-flash 0.5s ease-out;
+}
+
+@keyframes game-over-flash {
+  0% { 
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  }
+  30% { 
+    background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
+    transform: scale(1.02);
+  }
+  60% {
+    background: linear-gradient(135deg, #ff6666 0%, #ee2222 100%);
+    transform: scale(1.01);
+  }
+  100% { 
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    transform: scale(1);
+  }
+}
+
+.game-over-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(255, 0, 0, 0.8);
+  display: none;
+  align-items: center;
+  justify-content: center;
+  z-index: 500;
+  border-radius: 8px;
+}
+
+.game-over-overlay.active {
+  display: flex;
+  animation: game-over-overlay-appear 0.3s ease-out;
+}
+
+.game-over-text {
+  font-size: 2.5em;
+  font-weight: bold;
+  color: white;
+  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
+  animation: game-over-text-pulse 0.6s ease-in-out;
+}
+
+@keyframes game-over-overlay-appear {
+  0% { 
+    opacity: 0;
+    background: rgba(255, 0, 0, 0);
+  }
+  100% { 
+    opacity: 1;
+    background: rgba(255, 0, 0, 0.8);
+  }
+}
+
+@keyframes game-over-text-pulse {
+  0% { 
+    transform: scale(0.5);
+    opacity: 0;
+  }
+  50% { 
+    transform: scale(1.2);
+    opacity: 1;
+  }
+  100% { 
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
 .combo-particles {
   position: absolute;
   pointer-events: none;
   width: 100%;
   height: 100%;
 }
 
 .particle {
   position: absolute;
   font-size: 1.2em;
   animation: particle-burst 1s ease-out forwards;
   pointer-events: none;
 }
 
 @keyframes particle-burst {
   0% {
     opacity: 1;
     transform: scale(0.5);
   }
   50% {
     opacity: 0.8;
     transform: scale(1.2);
   }
   100% {
     opacity: 0;
     transform: scale(0.3) translateY(-50px);
   }
 }
 
 @media (max-width: 480px) {
   #app {
     border-radius: 0;
     height: 100vh;
     max-width: 100%;
   }
   
   .screen {
     padding: 10px;
   }
   
   .game-grid {
     width: 200px;
     height: 280px;
   }
   
   .block {
     font-size: 1.2em;
   }
   
   .game-info {
     font-size: 0.85em;
   }
   
   .modal-content {
     width: 95%;
     padding: 20px;
   }
   
   .header h1 {
     font-size: 1.5em;
   }
   
   .header p {
     font-size: 0.85em;
   }
   
   .keyboard-hints, .gamepad-hints {
     font-size: 0.7em;
   }
   
   .hint-row {
     gap: 10px;
   }
 }

JS Changes

 class GamepadManager {
   constructor() {
     this.gamepads = {};
     this.buttonStates = {};
     this.lastButtonStates = {};
     this.analogStates = {};
     this.deadzone = 0.3;
     this.repeatDelay = 200; // ms
     this.lastRepeatTime = {};
     this.connected = false;
     
     this.buttonMapping = {
       FACE_DOWN: 0,    // A button - Select/Swap
       FACE_RIGHT: 1,   // B button - Back/Cancel
       FACE_LEFT: 2,    // X button - (unused)
       FACE_UP: 3,      // Y button - (unused)
       LEFT_SHOULDER: 4,  // LB - Back to menu
       RIGHT_SHOULDER: 5, // RB - Quick restart
       LEFT_TRIGGER: 6,   // LT - (unused)
       RIGHT_TRIGGER: 7,  // RT - (unused)
       SELECT: 8,       // Select/Back - Volume toggle
       START: 9,        // Start - Pause/Resume
       LEFT_STICK: 10,  // LS click - (unused)
       RIGHT_STICK: 11, // RS click - (unused)
       DPAD_UP: 12,     // D-pad up
       DPAD_DOWN: 13,   // D-pad down
       DPAD_LEFT: 14,   // D-pad left
       DPAD_RIGHT: 15   // D-pad right
     };
     
     this.bindEvents();
   }
 
   bindEvents() {
     window.addEventListener('gamepadconnected', (e) => {
       this.onGamepadConnected(e.gamepad);
     });
     
     window.addEventListener('gamepaddisconnected', (e) => {
       this.onGamepadDisconnected(e.gamepad);
     });
   }
 
   onGamepadConnected(gamepad) {
     console.log(`Gamepad connected: ${gamepad.id}`);
     this.gamepads[gamepad.index] = gamepad;
     this.buttonStates[gamepad.index] = {};
     this.lastButtonStates[gamepad.index] = {};
     this.analogStates[gamepad.index] = { x: 0, y: 0 };
     this.lastRepeatTime[gamepad.index] = {};
     this.connected = true;
     
     // Update UI to show gamepad is connected
     this.updateConnectionStatus();
   }
 
   onGamepadDisconnected(gamepad) {
     console.log(`Gamepad disconnected: ${gamepad.id}`);
     delete this.gamepads[gamepad.index];
     delete this.buttonStates[gamepad.index];
     delete this.lastButtonStates[gamepad.index];
     delete this.analogStates[gamepad.index];
     delete this.lastRepeatTime[gamepad.index];
     
     this.connected = Object.keys(this.gamepads).length > 0;
     this.updateConnectionStatus();
   }
 
   updateConnectionStatus() {
     const statusElement = document.getElementById('gamepad-status');
     if (statusElement) {
       statusElement.textContent = this.connected ? '🎮 Controller Connected' : '';
       statusElement.style.display = this.connected ? 'block' : 'none';
     }
   }
 
   poll() {
     const gamepads = navigator.getGamepads();
     
     for (let i = 0; i < gamepads.length; i++) {
       if (gamepads[i] && this.gamepads[i]) {
         this.gamepads[i] = gamepads[i];
         this.updateButtonStates(i);
         this.updateAnalogStates(i);
       }
     }
   }
 
   updateButtonStates(gamepadIndex) {
     const gamepad = this.gamepads[gamepadIndex];
     if (!gamepad) return;
 
     this.lastButtonStates[gamepadIndex] = { ...this.buttonStates[gamepadIndex] };
     
     for (let i = 0; i < gamepad.buttons.length; i++) {
       this.buttonStates[gamepadIndex][i] = gamepad.buttons[i].pressed;
     }
   }
 
   updateAnalogStates(gamepadIndex) {
     const gamepad = this.gamepads[gamepadIndex];
     if (!gamepad) return;
 
     // Left stick (axes 0 and 1)
     const x = Math.abs(gamepad.axes[0]) > this.deadzone ? gamepad.axes[0] : 0;
     const y = Math.abs(gamepad.axes[1]) > this.deadzone ? gamepad.axes[1] : 0;
     
     this.analogStates[gamepadIndex] = { x, y };
   }
 
   isButtonPressed(button, gamepadIndex = 0) {
     return this.buttonStates[gamepadIndex] && this.buttonStates[gamepadIndex][button];
   }
 
   isButtonJustPressed(button, gamepadIndex = 0) {
     const current = this.buttonStates[gamepadIndex] && this.buttonStates[gamepadIndex][button];
     const last = this.lastButtonStates[gamepadIndex] && this.lastButtonStates[gamepadIndex][button];
     return current && !last;
   }
 
   isDirectionPressed(direction, gamepadIndex = 0) {
     const now = Date.now();
     const repeatKey = `${gamepadIndex}_${direction}`;
     
     // Check D-pad buttons
     let pressed = false;
     switch (direction) {
       case 'up':
         pressed = this.isButtonPressed(this.buttonMapping.DPAD_UP, gamepadIndex);
         break;
       case 'down':
         pressed = this.isButtonPressed(this.buttonMapping.DPAD_DOWN, gamepadIndex);
         break;
       case 'left':
         pressed = this.isButtonPressed(this.buttonMapping.DPAD_LEFT, gamepadIndex);
         break;
       case 'right':
         pressed = this.isButtonPressed(this.buttonMapping.DPAD_RIGHT, gamepadIndex);
         break;
     }
 
     // Check analog stick
     if (!pressed && this.analogStates[gamepadIndex]) {
       const { x, y } = this.analogStates[gamepadIndex];
       switch (direction) {
         case 'up':
           pressed = y < -0.5;
           break;
         case 'down':
           pressed = y > 0.5;
           break;
         case 'left':
           pressed = x < -0.5;
           break;
         case 'right':
           pressed = x > 0.5;
           break;
       }
     }
 
     if (!pressed) {
       delete this.lastRepeatTime[repeatKey];
       return false;
     }
 
     // Handle button repeat
     const lastTime = this.lastRepeatTime[repeatKey] || 0;
     if (now - lastTime >= this.repeatDelay) {
       this.lastRepeatTime[repeatKey] = now;
       return true;
     }
 
     return false;
   }
 
   vibrate(duration = 100, strength = 0.5) {
     Object.values(this.gamepads).forEach(gamepad => {
       if (gamepad.vibrationActuator) {
         gamepad.vibrationActuator.playEffect('dual-rumble', {
           duration,
           strongMagnitude: strength,
           weakMagnitude: strength * 0.5
         });
       }
     });
   }
 }
 
 class SoundManager {
   constructor() {
     this.audioContext = null;
     this.masterGain = null;
     this.volume = 0.7;
     this.muted = false;
     this.isInitialized = false;
     
     // Load volume preferences
     this.loadVolumePreferences();
   }
 
   async initialize() {
     if (this.isInitialized) return;
     
     try {
       this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
       this.masterGain = this.audioContext.createGain();
       this.masterGain.connect(this.audioContext.destination);
       this.updateVolume();
       this.isInitialized = true;
     } catch (error) {
       console.warn('Web Audio API not supported:', error);
     }
   }
 
   updateVolume() {
     if (this.masterGain) {
       this.masterGain.gain.value = this.muted ? 0 : this.volume;
     }
   }
 
   setVolume(volume) {
     this.volume = Math.max(0, Math.min(1, volume));
     this.updateVolume();
     this.saveVolumePreferences();
   }
 
   toggleMute() {
     this.muted = !this.muted;
     this.updateVolume();
     this.saveVolumePreferences();
   }
 
   saveVolumePreferences() {
     try {
       localStorage.setItem('connect3_volume', this.volume.toString());
       localStorage.setItem('connect3_muted', this.muted.toString());
     } catch (error) {
       // Silently fail if localStorage is not available (sandboxed environment)
       console.warn('Volume preferences could not be saved:', error);
     }
   }
 
   loadVolumePreferences() {
     try {
       const savedVolume = localStorage.getItem('connect3_volume');
       const savedMuted = localStorage.getItem('connect3_muted');
       
       if (savedVolume !== null) {
         this.volume = parseFloat(savedVolume);
       }
       if (savedMuted !== null) {
         this.muted = savedMuted === 'true';
       }
     } catch (error) {
       // Use default values if localStorage is not available
       console.warn('Volume preferences could not be loaded, using defaults:', error);
       this.volume = 0.7;
       this.muted = false;
     }
   }
 
   createOscillator(frequency, type = 'sine', duration = 0.3) {
     if (!this.isInitialized) return null;
     
     const oscillator = this.audioContext.createOscillator();
     const gainNode = this.audioContext.createGain();
     
     oscillator.type = type;
     oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime);
     
     gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
     gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
     
     oscillator.connect(gainNode);
     gainNode.connect(this.masterGain);
     
     return { oscillator, gainNode, duration };
   }
 
   createFilteredNoise(frequency, duration = 0.2) {
     if (!this.isInitialized) return null;
     
     const bufferSize = this.audioContext.sampleRate * duration;
     const noiseBuffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
     const output = noiseBuffer.getChannelData(0);
     
     for (let i = 0; i < bufferSize; i++) {
       output[i] = Math.random() * 2 - 1;
     }
     
     const whiteNoise = this.audioContext.createBufferSource();
     whiteNoise.buffer = noiseBuffer;
     
     const bandpass = this.audioContext.createBiquadFilter();
     bandpass.type = 'bandpass';
     bandpass.frequency.value = frequency;
     bandpass.Q.value = 30;
     
     const gainNode = this.audioContext.createGain();
     gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
     gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
     
     whiteNoise.connect(bandpass);
     bandpass.connect(gainNode);
     gainNode.connect(this.masterGain);
     
     return { source: whiteNoise, duration };
   }
 
   playSwapSound() {
     if (!this.isInitialized) return;
     
     const sound1 = this.createOscillator(400, 'sine', 0.15);
     const sound2 = this.createOscillator(600, 'sine', 0.15);
     
     if (sound1 && sound2) {
       sound1.oscillator.start();
       sound1.oscillator.stop(this.audioContext.currentTime + sound1.duration);
       
       setTimeout(() => {
         sound2.oscillator.start();
         sound2.oscillator.stop(this.audioContext.currentTime + sound2.duration);
       }, 50);
     }
   }
 
   playMatchSound(blockCount = 3) {
     if (!this.isInitialized) return;
     
     const baseFreq = 220 + (blockCount * 40);
     const sound = this.createOscillator(baseFreq, 'triangle', 0.4);
     const harmonic = this.createOscillator(baseFreq * 1.5, 'sine', 0.4);
     
     if (sound && harmonic) {
       sound.gainNode.gain.setValueAtTime(0.4, this.audioContext.currentTime);
       harmonic.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       
       sound.oscillator.start();
       harmonic.oscillator.start();
       
       sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
       harmonic.oscillator.stop(this.audioContext.currentTime + harmonic.duration);
     }
   }
 
   playComboSound(comboLevel) {
     if (!this.isInitialized) return;
     
     const frequencies = [440, 550, 660, 770, 880, 990, 1100, 1210, 1320, 1430];
     const freq = frequencies[Math.min(comboLevel - 2, frequencies.length - 1)];
     
     // Main tone
     const main = this.createOscillator(freq, 'sawtooth', 0.6);
     // Higher harmonic
     const high = this.createOscillator(freq * 1.5, 'sine', 0.4);
     // Lower bass
     const bass = this.createOscillator(freq * 0.5, 'triangle', 0.8);
     
     if (main && high && bass) {
       main.gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
       high.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       bass.gainNode.gain.setValueAtTime(0.4, this.audioContext.currentTime);
       
       main.oscillator.start();
       high.oscillator.start();
       bass.oscillator.start();
       
       main.oscillator.stop(this.audioContext.currentTime + main.duration);
       high.oscillator.stop(this.audioContext.currentTime + high.duration);
       bass.oscillator.stop(this.audioContext.currentTime + bass.duration);
     }
   }
 
   playNewRowSound() {
     if (!this.isInitialized) return;
     
     const sound = this.createFilteredNoise(800, 0.3);
     if (sound) {
       sound.source.start();
       sound.source.stop(this.audioContext.currentTime + sound.duration);
     }
   }
 
   playGameOverSound() {
     if (!this.isInitialized) return;
     
     const frequencies = [440, 370, 311, 262];
     frequencies.forEach((freq, index) => {
       setTimeout(() => {
         const sound = this.createOscillator(freq, 'square', 0.8);
         if (sound) {
           sound.oscillator.start();
           sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
         }
       }, index * 200);
     });
   }
 
   playButtonSound() {
     if (!this.isInitialized) return;
     
     const sound = this.createOscillator(800, 'sine', 0.1);
     if (sound) {
       sound.gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
       sound.oscillator.start();
       sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
     }
   }
 
   playLevelUpSound() {
     if (!this.isInitialized) return;
     
     const arpeggio = [523, 659, 784, 1047]; // C5, E5, G5, C6
     arpeggio.forEach((freq, index) => {
       setTimeout(() => {
         const sound = this.createOscillator(freq, 'triangle', 0.3);
         if (sound) {
           sound.oscillator.start();
           sound.oscillator.stop(this.audioContext.currentTime + sound.duration);
         }
       }, index * 100);
     });
   }
 }
 
 class Connect3Game {
   constructor() {
     this.GRID_WIDTH = 6;
     this.GRID_HEIGHT = 12;
     this.BLOCK_TYPES = ['🔴', '🔵', '🟡', '🟢', '🟣', '🟠'];
     
     this.grid = [];
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = false;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.nextRow = [];
     this.fallSpeed = 5000; // milliseconds
     this.lastFall = 0;
     this.currentUser = null;
     this.previousLevel = 1;
     
     this.soundManager = new SoundManager();
     this.gamepadManager = new GamepadManager();
     
     this.initializeElements();
     this.initializeGrid();
     this.generateNextRow();
     this.bindEvents();
     this.loadLeaderboard();
     this.checkAuthentication();
     this.setupVolumeControls();
   }
 
   async checkAuthentication() {
     try {
       const initData = await window.discourseArtifactReady;
       this.currentUser = {
         username: initData.username,
         name: initData.name,
         user_id: initData.user_id
       };
     } catch (error) {
       this.currentUser = null;
     }
   }
 
   initializeElements() {
     this.elements = {
       leaderboardScreen: document.getElementById('leaderboard-screen'),
       gameScreen: document.getElementById('game-screen'),
       gameOverModal: document.getElementById('game-over-modal'),
       pauseModal: document.getElementById('pause-modal'),
       startGameBtn: document.getElementById('start-game-btn'),
       pauseBtn: document.getElementById('pause-btn'),
       backToMenuBtn: document.getElementById('back-to-menu-btn'),
       resumeBtn: document.getElementById('resume-btn'),
       quitBtn: document.getElementById('quit-btn'),
       playAgainBtn: document.getElementById('play-again-btn'),
       viewLeaderboardBtn: document.getElementById('view-leaderboard-btn'),
       gameGrid: document.getElementById('game-grid'),
       currentScore: document.getElementById('current-score'),
       currentLevel: document.getElementById('current-level'),
       comboText: document.getElementById('combo-text'),
       finalScoreValue: document.getElementById('final-score-value'),
       scoreMessage: document.getElementById('score-message'),
       anonymousMessage: document.getElementById('anonymous-message'),
       nextRowPreview: document.getElementById('next-row-preview'),
       leaderboardBody: document.getElementById('leaderboard-body'),
       leaderboardTable: document.getElementById('leaderboard-table'),
       leaderboardLoading: document.getElementById('leaderboard-loading'),
       volumeBtn: document.getElementById('volume-btn'),
       volumeSlider: document.getElementById('volume-slider'),
+      volumeSliderContainer: document.getElementById('volume-slider-container'),
+      gameOverOverlay: document.getElementById('game-over-overlay')
-      volumeSliderContainer: document.getElementById('volume-slider-container')
     };
   }
 
   initializeGrid() {
     this.grid = Array(this.GRID_HEIGHT).fill().map(() => Array(this.GRID_WIDTH).fill(null));
     this.renderGrid();
   }
 
   generateNextRow() {
     this.nextRow = Array(this.GRID_WIDTH).fill().map(() => 
       this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)]
     );
     this.renderNextRowPreview();
   }
 
   renderNextRowPreview() {
     this.elements.nextRowPreview.innerHTML = '';
     this.nextRow.forEach(blockType => {
       const block = document.createElement('div');
       block.className = 'next-block';
       block.textContent = blockType;
       this.elements.nextRowPreview.appendChild(block);
     });
   }
 
   renderGrid() {
     this.elements.gameGrid.innerHTML = '';
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const block = document.createElement('div');
         block.className = 'block';
         block.dataset.row = row;
         block.dataset.col = col;
         
         if (this.grid[row][col]) {
           block.textContent = this.grid[row][col];
           block.style.background = 'rgba(255,255,255,0.9)';
         } else {
           block.style.background = 'rgba(255,255,255,0.1)';
         }
         
         this.elements.gameGrid.appendChild(block);
       }
     }
     this.updateCursor();
   }
 
   createParticleEffect(blockElement) {
     const rect = blockElement.getBoundingClientRect();
     const gridRect = this.elements.gameGrid.getBoundingClientRect();
     
     for (let i = 0; i < 3; i++) {
       const particle = document.createElement('div');
       particle.className = 'particle';
       particle.textContent = '✨';
       
       const offsetX = (rect.left - gridRect.left) + (rect.width / 2);
       const offsetY = (rect.top - gridRect.top) + (rect.height / 2);
       
       particle.style.left = offsetX + 'px';
       particle.style.top = offsetY + 'px';
       particle.style.animationDelay = (i * 100) + 'ms';
       
       this.elements.gameGrid.appendChild(particle);
       
       setTimeout(() => {
         if (particle.parentNode) {
           particle.parentNode.removeChild(particle);
         }
       }, 1000);
     }
   }
 
   bindEvents() {
     this.elements.startGameBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.startGame();
     });
     this.elements.pauseBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.pauseGame();
     });
     this.elements.backToMenuBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.quitToMenu();
     });
     this.elements.resumeBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.resumeGame();
     });
     this.elements.quitBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.quitToMenu();
     });
     this.elements.playAgainBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.startGame();
     });
     this.elements.viewLeaderboardBtn.addEventListener('click', () => {
       this.soundManager.playButtonSound();
       this.showLeaderboard();
     });
     
     this.elements.gameGrid.addEventListener('click', (e) => this.handleBlockClick(e));
     
     // Add keyboard event listener
     document.addEventListener('keydown', (e) => this.handleKeyDown(e));
   }
 
   startGame() {
     this.initializeGrid();
     this.score = 0;
     this.level = 1;
     this.combo = 0;
     this.gameRunning = true;
     this.gamePaused = false;
     this.selectedBlock = null;
     this.cursorRow = 5;
     this.cursorCol = 2;
     this.fallSpeed = 5000;
     this.lastFall = Date.now();
     
     // Fill initial rows
     for (let row = this.GRID_HEIGHT - 4; row < this.GRID_HEIGHT; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.BLOCK_TYPES[Math.floor(Math.random() * this.BLOCK_TYPES.length)];
       }
     }
     
     this.generateNextRow();
     this.updateDisplay();
     this.showGameScreen();
     this.gameLoop();
   }
 
   handleBlockClick(e) {
     if (!this.gameRunning || this.gamePaused) return;
     
     const block = e.target;
     if (!block.classList.contains('block') || !this.grid[block.dataset.row][block.dataset.col]) return;
     
     const row = parseInt(block.dataset.row);
     const col = parseInt(block.dataset.col);
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if adjacent horizontally or vertically
       if ((row === selectedRow && Math.abs(col - selectedCol) === 1) || 
           (col === selectedCol && Math.abs(row - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, row, col);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row, col };
       this.updateSelection();
     }
   }
 
   swapBlocks(row1, col1, row2, col2) {
     this.soundManager.playSwapSound();
     
     // Add swap animation
     const block1 = this.elements.gameGrid.children[row1 * this.GRID_WIDTH + col1];
     const block2 = this.elements.gameGrid.children[row2 * this.GRID_WIDTH + col2];
     
     // Determine swap direction and apply appropriate animations
     if (row1 === row2) {
       // Horizontal swap
       if (col2 > col1) {
         block1.classList.add('swapping-right');
         block2.classList.add('swapping-left');
       } else {
         block1.classList.add('swapping-left');
         block2.classList.add('swapping-right');
       }
     } else {
       // Vertical swap
       if (row2 > row1) {
         block1.classList.add('swapping-down');
         block2.classList.add('swapping-up');
       } else {
         block1.classList.add('swapping-up');
         block2.classList.add('swapping-down');
       }
     }
     
     setTimeout(() => {
       const temp = this.grid[row1][col1];
       this.grid[row1][col1] = this.grid[row2][col2];
       this.grid[row2][col2] = temp;
       
       this.renderGrid();
       this.checkMatches();
     }, 150);
   }
 
   clearSelection() {
     this.selectedBlock = null;
     this.updateSelection();
   }
 
   updateSelection() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('selected'));
     
     if (this.selectedBlock) {
       const index = this.selectedBlock.row * this.GRID_WIDTH + this.selectedBlock.col;
       blocks[index]?.classList.add('selected');
     }
   }
 
   updateCursor() {
     const blocks = this.elements.gameGrid.querySelectorAll('.block');
     blocks.forEach(block => block.classList.remove('cursor'));
     
     if (this.gameRunning && !this.gamePaused) {
       const index = this.cursorRow * this.GRID_WIDTH + this.cursorCol;
       blocks[index]?.classList.add('cursor');
     }
   }
 
   moveCursor(deltaRow, deltaCol) {
     this.cursorRow = (this.cursorRow + deltaRow + this.GRID_HEIGHT) % this.GRID_HEIGHT;
     this.cursorCol = (this.cursorCol + deltaCol + this.GRID_WIDTH) % this.GRID_WIDTH;
     this.updateCursor();
   }
 
   handleGamepadInput() {
     // Handle different game states
     if (this.elements.gameOverModal.classList.contains('active')) {
       this.handleGameOverGamepad();
       return;
     }
     
     if (this.elements.pauseModal.classList.contains('active')) {
       this.handlePauseGamepad();
       return;
     }
     
     if (this.elements.leaderboardScreen.classList.contains('active')) {
       this.handleMenuGamepad();
       return;
     }
     
     if (this.gameRunning) {
       this.handleGameGamepad();
     }
   }
 
   handleGameGamepad() {
     // Movement
     if (this.gamepadManager.isDirectionPressed('up')) {
       this.moveCursor(-1, 0);
     }
     if (this.gamepadManager.isDirectionPressed('down')) {
       this.moveCursor(1, 0);
     }
     if (this.gamepadManager.isDirectionPressed('left')) {
       this.moveCursor(0, -1);
     }
     if (this.gamepadManager.isDirectionPressed('right')) {
       this.moveCursor(0, 1);
     }
 
     // Actions
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_DOWN)) {
       this.handleCursorSelect();
       this.gamepadManager.vibrate(50, 0.3);
     }
     
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_RIGHT)) {
       this.clearSelection();
       this.gamepadManager.vibrate(30, 0.2);
     }
     
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.START)) {
       if (this.gamePaused) {
         this.resumeGame();
       } else {
         this.pauseGame();
       }
       this.gamepadManager.vibrate(100, 0.4);
     }
     
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.SELECT)) {
       this.soundManager.toggleMute();
       this.updateVolumeIcon();
       this.gamepadManager.vibrate(50, 0.2);
     }
     
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.LEFT_SHOULDER)) {
       this.quitToMenu();
       this.gamepadManager.vibrate(100, 0.4);
     }
   }
 
   handleGameOverGamepad() {
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.RIGHT_SHOULDER)) {
       this.startGame();
       this.gamepadManager.vibrate(100, 0.5);
     }
     
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_DOWN) ||
         this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.START)) {
       this.showLeaderboard();
       this.gamepadManager.vibrate(100, 0.4);
     }
     
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_RIGHT)) {
       this.startGame();
       this.gamepadManager.vibrate(100, 0.5);
     }
   }
 
   handlePauseGamepad() {
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.START) ||
         this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_DOWN)) {
       this.resumeGame();
       this.gamepadManager.vibrate(100, 0.4);
     }
     
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_RIGHT) ||
         this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.LEFT_SHOULDER)) {
       this.quitToMenu();
       this.gamepadManager.vibrate(100, 0.4);
     }
   }
 
   handleMenuGamepad() {
     if (this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.FACE_DOWN) ||
         this.gamepadManager.isButtonJustPressed(this.gamepadManager.buttonMapping.START)) {
       this.startGame();
       this.gamepadManager.vibrate(100, 0.5);
     }
   }
 
   handleKeyDown(e) {
     // Handle different game states
     if (this.elements.gameOverModal.classList.contains('active')) {
       this.handleGameOverKeys(e);
       return;
     }
     
     if (this.elements.pauseModal.classList.contains('active')) {
       this.handlePauseKeys(e);
       return;
     }
     
     if (this.elements.leaderboardScreen.classList.contains('active')) {
       this.handleMenuKeys(e);
       return;
     }
     
     if (this.gameRunning) {
       this.handleGameKeys(e);
     }
   }
 
   handleGameKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'arrowup':
       case 'w':
         e.preventDefault();
         this.moveCursor(-1, 0);
         break;
       case 'arrowdown':
       case 's':
         e.preventDefault();
         this.moveCursor(1, 0);
         break;
       case 'arrowleft':
       case 'a':
         e.preventDefault();
         this.moveCursor(0, -1);
         break;
       case 'arrowright':
       case 'd':
         e.preventDefault();
         this.moveCursor(0, 1);
         break;
       case ' ':
       case 'enter':
         e.preventDefault();
         this.handleCursorSelect();
         break;
       case 'escape':
         e.preventDefault();
         if (this.gamePaused) {
           this.resumeGame();
         } else {
           this.pauseGame();
         }
         break;
     }
   }
 
   handleGameOverKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'r':
         e.preventDefault();
         this.startGame();
         break;
       case 'escape':
       case 'enter':
         e.preventDefault();
         this.showLeaderboard();
         break;
     }
   }
 
   handlePauseKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'escape':
       case 'enter':
       case ' ':
         e.preventDefault();
         this.resumeGame();
         break;
       case 'q':
         e.preventDefault();
         this.quitToMenu();
         break;
     }
   }
 
   handleMenuKeys(e) {
     switch(e.key.toLowerCase()) {
       case 'enter':
       case ' ':
         e.preventDefault();
         this.startGame();
         break;
     }
   }
 
   handleCursorSelect() {
     if (!this.gameRunning || this.gamePaused) return;
     
     // Check if there's a block at cursor position
     if (!this.grid[this.cursorRow][this.cursorCol]) return;
     
     if (this.selectedBlock) {
       const selectedRow = this.selectedBlock.row;
       const selectedCol = this.selectedBlock.col;
       
       // Check if cursor is on selected block (deselect)
       if (this.cursorRow === selectedRow && this.cursorCol === selectedCol) {
         this.clearSelection();
         return;
       }
       
       // Check if adjacent horizontally or vertically
       if ((this.cursorRow === selectedRow && Math.abs(this.cursorCol - selectedCol) === 1) || 
           (this.cursorCol === selectedCol && Math.abs(this.cursorRow - selectedRow) === 1)) {
         this.swapBlocks(selectedRow, selectedCol, this.cursorRow, this.cursorCol);
       }
       
       this.clearSelection();
     } else {
       this.selectedBlock = { row: this.cursorRow, col: this.cursorCol };
       this.updateSelection();
     }
   }
 
   checkMatches() {
     const matches = this.findMatches();
     if (matches.length > 0) {
       this.removeMatches(matches, () => {
         // After blocks are removed, apply gravity
         setTimeout(() => {
           this.applyGravity(() => {
             // After gravity is complete, check for new matches
             setTimeout(() => this.checkMatches(), 200);
           });
         }, 150);
       });
     } else {
       this.combo = 0;
       this.updateDisplay();
     }
   }
 
   findMatches() {
     const matches = new Set();
     
     // Check horizontal matches
     for (let row = 0; row < this.GRID_HEIGHT; row++) {
       let count = 1;
       let current = this.grid[row][0];
       
       for (let col = 1; col < this.GRID_WIDTH; col++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = col - count; i < col; i++) {
               matches.add(`${row}-${i}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_WIDTH - count; i < this.GRID_WIDTH; i++) {
           matches.add(`${row}-${i}`);
         }
       }
     }
     
     // Check vertical matches
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       let count = 1;
       let current = this.grid[0][col];
       
       for (let row = 1; row < this.GRID_HEIGHT; row++) {
         if (this.grid[row][col] === current && current !== null) {
           count++;
         } else {
           if (count >= 3 && current !== null) {
             for (let i = row - count; i < row; i++) {
               matches.add(`${i}-${col}`);
             }
           }
           count = 1;
           current = this.grid[row][col];
         }
       }
       
       if (count >= 3 && current !== null) {
         for (let i = this.GRID_HEIGHT - count; i < this.GRID_HEIGHT; i++) {
           matches.add(`${i}-${col}`);
         }
       }
     }
     
     return Array.from(matches).map(pos => {
       const [row, col] = pos.split('-').map(Number);
       return { row, col };
     });
   }
 
   removeMatches(matches, callback) {
     this.combo++;
     const baseScore = matches.length * 10;
     const comboMultiplier = Math.min(this.combo, 10);
     const levelMultiplier = this.level;
     
     const points = baseScore * comboMultiplier * levelMultiplier;
     this.score += points;
     
     // Play appropriate sounds
     if (this.combo > 1) {
       this.soundManager.playComboSound(this.combo);
     } else {
       this.soundManager.playMatchSound(matches.length);
     }
     
     // Gamepad vibration for matches
     if (this.combo > 1) {
       this.gamepadManager.vibrate(200 + (this.combo * 50), 0.3 + (this.combo * 0.1));
     } else {
       this.gamepadManager.vibrate(100, 0.2);
     }
     
     // Add matched animation before removing
     matches.forEach(({ row, col }, index) => {
       const blockElement = this.elements.gameGrid.children[row * this.GRID_WIDTH + col];
       if (blockElement) {
         setTimeout(() => {
           blockElement.classList.add('matched');
           blockElement.classList.add('sound-pulse');
           if (this.combo >= 3) {
             this.createParticleEffect(blockElement);
           }
         }, index * 50);
       }
     });
     
     // Remove blocks after animation and call callback
     setTimeout(() => {
       matches.forEach(({ row, col }) => {
         this.grid[row][col] = null;
       });
       this.renderGrid();
       this.updateDisplay();
       
       if (this.combo > 1) {
         this.elements.comboText.textContent = `${this.combo}x COMBO!`;
         if (this.combo >= 7) {
           document.getElementById('app').classList.add('screen-shake');
           setTimeout(() => document.getElementById('app').classList.remove('screen-shake'), 300);
         }
       } else {
         this.elements.comboText.textContent = '';
       }
       
       if (callback) callback();
     }, 600);
   }
 
   applyGravity(callback) {
     const fallingBlocks = [];
     const columnData = [];
     
     // Analyze each column and track falling blocks
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       const column = [];
       const originalPositions = [];
       
       // Collect non-null blocks and their original positions
       for (let row = this.GRID_HEIGHT - 1; row >= 0; row--) {
         if (this.grid[row][col] !== null) {
           column.push(this.grid[row][col]);
           originalPositions.push(row);
         }
       }
       
       columnData[col] = { blocks: column, originalPositions };
       
       // Clear the column
       for (let row = 0; row < this.GRID_HEIGHT; row++) {
         this.grid[row][col] = null;
       }
       
       // Track falling blocks with distance information
       for (let i = 0; i < column.length; i++) {
         const oldRow = originalPositions[i];
         const newRow = this.GRID_HEIGHT - 1 - i;
         
         if (newRow > oldRow) {
           fallingBlocks.push({ 
             col, 
             oldRow, 
             newRow, 
             block: column[i],
             fallDistance: newRow - oldRow
           });
         }
       }
       
       // Place blocks in their new positions
       for (let i = 0; i < column.length; i++) {
         this.grid[this.GRID_HEIGHT - 1 - i][col] = column[i];
       }
     }
     
     // If no blocks need to fall, just call callback
     if (fallingBlocks.length === 0) {
       this.renderGrid();
       if (callback) callback();
       return;
     }
     
     // Render grid first
     this.renderGrid();
     
     // Apply falling animations with staggered timing
     let maxAnimationTime = 0;
     fallingBlocks.forEach((block, index) => {
       const blockElement = this.elements.gameGrid.children[block.newRow * this.GRID_WIDTH + block.col];
       if (blockElement) {
         // Stagger animation start based on column and fall distance
         const startDelay = (block.col * 50) + (block.fallDistance * 20);
         const animationDuration = 400 + (block.fallDistance * 100);
         
         maxAnimationTime = Math.max(maxAnimationTime, startDelay + animationDuration);
         
         setTimeout(() => {
           blockElement.classList.add('gravity-fall');
           setTimeout(() => {
             blockElement.classList.remove('gravity-fall');
           }, animationDuration);
         }, startDelay);
       }
     });
     
     // Call callback after all animations complete
     if (callback) {
       setTimeout(callback, maxAnimationTime + 200);
     }
   }
 
   addNewRow() {
     // Check if top row has blocks (game over)
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       if (this.grid[0][col] !== null) {
         this.gameOver();
         return;
       }
     }
     
     this.soundManager.playNewRowSound();
     
+    // Move cursor up with the shifting rows to maintain relative position
+    this.cursorRow = Math.max(0, this.cursorRow - 1);
+    
     // Shift all rows up
     for (let row = 0; row < this.GRID_HEIGHT - 1; row++) {
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         this.grid[row][col] = this.grid[row + 1][col];
       }
     }
     
     // Add new row at bottom
     for (let col = 0; col < this.GRID_WIDTH; col++) {
       this.grid[this.GRID_HEIGHT - 1][col] = this.nextRow[col];
     }
     
     this.generateNextRow();
     
     // Wait a bit before rendering to avoid conflicts
     setTimeout(() => {
       this.renderGrid();
+      this.updateCursor(); // Update cursor position after grid shift
       
       // Add new row animation with smoother staggering
       const animationDuration = 500;
       for (let col = 0; col < this.GRID_WIDTH; col++) {
         const blockElement = this.elements.gameGrid.children[(this.GRID_HEIGHT - 1) * this.GRID_WIDTH + col];
         if (blockElement && this.grid[this.GRID_HEIGHT - 1][col]) {
           setTimeout(() => {
             blockElement.classList.add('new-row');
             setTimeout(() => blockElement.classList.remove('new-row'), animationDuration);
           }, col * 80);
         }
       }
       
       // Wait for all animations to complete before checking matches
       const totalAnimationTime = (this.GRID_WIDTH - 1) * 80 + animationDuration;
       setTimeout(() => this.checkMatches(), totalAnimationTime + 100);
     }, 50);
   }
 
   gameLoop() {
     if (!this.gameRunning || this.gamePaused) return;
     
     // Poll gamepad input
     this.gamepadManager.poll();
     this.handleGamepadInput();
     
     const now = Date.now();
     if (now - this.lastFall >= this.fallSpeed) {
       this.addNewRow();
       this.lastFall = now;
       
       // Increase level and speed
       this.level = Math.floor(this.score / 1000) + 1;
       this.fallSpeed = Math.max(2000, 5000 - (this.level - 1) * 100);
     }
     
     requestAnimationFrame(() => this.gameLoop());
   }
 
   updateDisplay() {
     this.elements.currentScore.textContent = this.score.toLocaleString();
     this.elements.currentLevel.textContent = this.level;
     
     // Check for level up
     if (this.level > this.previousLevel) {
       this.soundManager.playLevelUpSound();
       this.previousLevel = this.level;
     }
   }
 
   pauseGame() {
     this.gamePaused = true;
     this.elements.pauseModal.classList.add('active');
     this.updateCursor(); // Hide cursor when paused
   }
 
   resumeGame() {
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.lastFall = Date.now();
     this.updateCursor(); // Show cursor when resumed
     this.gameLoop();
   }
 
   quitToMenu() {
     this.gameRunning = false;
     this.gamePaused = false;
     this.elements.pauseModal.classList.remove('active');
     this.updateCursor(); // Hide cursor when quitting
     this.showLeaderboard();
   }
 
   async gameOver() {
+    // Immediately disable game and input
     this.gameRunning = false;
+    this.gamePaused = true;
+    
+    // Immediate visual feedback
+    document.getElementById('app').classList.add('game-over-flash');
+    this.elements.gameOverOverlay.classList.add('active');
+    
+    // Play sounds and vibration
     this.soundManager.playGameOverSound();
+    this.gamepadManager.vibrate(500, 0.8);
+    
+    // Update cursor to hide it
+    this.updateCursor();
+    
+    // Set final score immediately
-    this.gamepadManager.vibrate(500, 0.8); // Strong vibration for game over
     this.elements.finalScoreValue.textContent = this.score.toLocaleString();
     
+    // Wait for immediate feedback animation, then show modal
+    setTimeout(async () => {
+      // Hide immediate feedback
+      this.elements.gameOverOverlay.classList.remove('active');
+      document.getElementById('app').classList.remove('game-over-flash');
+      
+      // Handle score saving and messaging
+      if (this.currentUser) {
+        await this.saveScore();
+        const position = await this.getScorePosition();
+        this.elements.scoreMessage.textContent = position <= 20 ? 
+          `🎉 You made it to #${position} on the leaderboard!` : 
+          `Your best score has been saved!`;
+        this.elements.anonymousMessage.style.display = 'none';
+      } else {
+        this.elements.scoreMessage.textContent = '';
+        this.elements.anonymousMessage.style.display = 'block';
+      }
+      
+      // Show game over modal
+      this.elements.gameOverModal.classList.add('active');
+    }, 1000);
-    if (this.currentUser) {
-      await this.saveScore();
-      const position = await this.getScorePosition();
-      this.elements.scoreMessage.textContent = position <= 20 ? 
-        `🎉 You made it to #${position} on the leaderboard!` : 
-        `Your best score has been saved!`;
-      this.elements.anonymousMessage.style.display = 'none';
-    } else {
-      this.elements.scoreMessage.textContent = '';
-      this.elements.anonymousMessage.style.display = 'block';
-    }
-    
-    this.elements.gameOverModal.classList.add('active');
   }
 
   async saveScore() {
     if (!this.currentUser) return;
     
     try {
       const key = `score_${this.currentUser.username}`;
       const existingData = await window.discourseArtifact.get(key);
       
       let scoreData = {
         username: this.currentUser.username,
         name: this.currentUser.name,
         score: this.score,
         date: new Date().toISOString()
       };
       
       if (existingData) {
         const existing = JSON.parse(existingData);
         if (existing.score >= this.score) return; // Don't save if not a high score
       }
       
       await window.discourseArtifact.set(key, JSON.stringify(scoreData), { public: true });
     } catch (error) {
       console.error('Error saving score:', error);
     }
   }
 
   async getScorePosition() {
     try {
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => JSON.parse(kv.value))
         .sort((a, b) => b.score - a.score);
       
       const userScore = scores.find(s => s.username === this.currentUser.username);
       return userScore ? scores.indexOf(userScore) + 1 : scores.length + 1;
     } catch (error) {
       return 999;
     }
   }
 
   async loadLeaderboard() {
     try {
       this.elements.leaderboardLoading.style.display = 'block';
       this.elements.leaderboardTable.style.display = 'none';
       
       const result = await window.discourseArtifact.index({ all_users: true });
       const scores = result.key_values
         .filter(kv => kv.key.startsWith('score_'))
         .map(kv => ({
           ...JSON.parse(kv.value),
           user: kv.user
         }))
         .sort((a, b) => b.score - a.score)
         .slice(0, 20);
       
       this.renderLeaderboard(scores);
     } catch (error) {
       console.error('Error loading leaderboard:', error);
       this.elements.leaderboardLoading.textContent = 'Error loading leaderboard';
     }
   }
 
   renderLeaderboard(scores) {
     this.elements.leaderboardBody.innerHTML = '';
     
     if (scores.length === 0) {
       this.elements.leaderboardLoading.textContent = 'No scores yet. Be the first to play!';
       return;
     }
     
     scores.forEach((score, index) => {
       const row = document.createElement('tr');
       const rank = index + 1;
       const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : rank;
       const date = new Date(score.date).toLocaleDateString();
       const displayName = score.user.name || score.username;
       
       row.innerHTML = `
         <td><span class="rank-medal">${medal}</span></td>
         <td>${displayName}</td>
         <td>${score.score.toLocaleString()}</td>
         <td>${date}</td>
       `;
       
       this.elements.leaderboardBody.appendChild(row);
     });
     
     this.elements.leaderboardLoading.style.display = 'none';
     this.elements.leaderboardTable.style.display = 'table';
   }
 
   setupVolumeControls() {
     // Initialize volume slider with current volume
     this.elements.volumeSlider.value = Math.round(this.soundManager.volume * 100);
     this.updateVolumeIcon();
 
     // Volume button click handler
     this.elements.volumeBtn.addEventListener('click', (e) => {
       e.stopPropagation();
       this.soundManager.playButtonSound();
       this.elements.volumeSliderContainer.classList.toggle('active');
     });
 
     // Volume slider change handler
     this.elements.volumeSlider.addEventListener('input', (e) => {
       const volume = parseInt(e.target.value) / 100;
       this.soundManager.setVolume(volume);
       this.soundManager.muted = false; // Unmute when adjusting volume
       this.updateVolumeIcon();
     });
 
     // Close volume slider when clicking outside
     document.addEventListener('click', (e) => {
       if (!this.elements.volumeBtn.contains(e.target) && 
           !this.elements.volumeSliderContainer.contains(e.target)) {
         this.elements.volumeSliderContainer.classList.remove('active');
       }
     });
 
     // Mute/unmute on right click or double click
     this.elements.volumeBtn.addEventListener('dblclick', (e) => {
       e.preventDefault();
       this.soundManager.toggleMute();
       this.updateVolumeIcon();
     });
 
     // Prevent volume slider from closing when clicking on it
     this.elements.volumeSliderContainer.addEventListener('click', (e) => {
       e.stopPropagation();
     });
   }
 
   updateVolumeIcon() {
     const volume = this.soundManager.volume;
     const muted = this.soundManager.muted;
     
     if (muted || volume === 0) {
       this.elements.volumeBtn.textContent = '🔇';
       this.elements.volumeBtn.classList.add('muted');
     } else if (volume < 0.3) {
       this.elements.volumeBtn.textContent = '🔉';
       this.elements.volumeBtn.classList.remove('muted');
     } else {
       this.elements.volumeBtn.textContent = '🔊';
       this.elements.volumeBtn.classList.remove('muted');
     }
     
     // Update slider position
     this.elements.volumeSlider.value = Math.round(volume * 100);
   }
 
   showLeaderboard() {
     this.elements.leaderboardScreen.classList.add('active');
     this.elements.gameScreen.classList.remove('active');
     this.elements.gameOverModal.classList.remove('active');
     this.loadLeaderboard();
   }
 
   showGameScreen() {
     this.elements.leaderboardScreen.classList.remove('active');
     this.elements.gameScreen.classList.add('active');
     this.elements.gameOverModal.classList.remove('active');
   }
 }
 
 // Initialize game when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
   new Connect3Game();
   
   // Start gamepad polling
   function pollGamepads() {
     if (navigator.getGamepads) {
       // The polling happens in the game loop, but we need this for connection detection
       requestAnimationFrame(pollGamepads);
     }
   }
   pollGamepads();
 });

Preview

Falco

Can the game over screen have a (Show Leaderboard) alongside the (Play AAgain) ?

Web-Artifact-Creator-bot
Thinking...The user wants to add a "Show Leaderboard" button alongside the "Play Again" button on the game over screen. Looking at the current code, I can see there's already a "View Leaderboard" button in the game over modal:
<div class="modal-buttons">
  <button id="play-again-btn" class="primary-button">🔄 Play Again</button>
  <button id="view-leaderboard-btn" class="secondary-button">🏆 View Leaderboard</button>
</div>

And it’s already bound to an event listener:

this.elements.viewLeaderboardBtn.addEventListener('click', () => {
  this.soundManager.playButtonSound();
  this.showLeaderboard();
});

So it seems like this functionality already exists. Let me double-check… yes, there’s already a “:trophy: View Leaderboard” button in the game over modal. Maybe the user didn’t notice it or it’s not working properly?

Actually, looking more carefully, maybe the user wants the text to be “Show Leaderboard” instead of “View Leaderboard”. Let me update the text to make it clearer.

Change Description
Update the game over modal to make the leaderboard button more prominent:

**Changes to make:**
1. Change the "View Leaderboard" button text to "🏆 Show Leaderboard" to make it clearer
2. Make sure both buttons are equally visible and styled consistently
3. Ensure the button functionality is working properly

**HTML Changes:**
- Update the button text from "🏆 View Leaderboard" to "🏆 Show Leaderboard"

**CSS Changes:**
- Ensure both buttons in the modal have consistent styling and prominence
- Make sure the "Show Leaderboard" button is clearly visible alongside "Play Again"

The functionality should already be there, just need to make the button text clearer and ensure both buttons are equally prominent visually.
View Changes

HTML Changes

 <div id="app">
   <!-- Leaderboard Screen -->
   <div id="leaderboard-screen" class="screen active">
     <div class="header">
       <h1>🧩 Connect 3 Puzzle</h1>
       <p>Match 3 or more blocks to clear them and create combos!</p>
     </div>
     
     <div class="leaderboard-container">
       <h2>🏆 Top Scores</h2>
       <div id="leaderboard-loading">Loading leaderboard...</div>
       <table id="leaderboard-table" style="display: none;">
         <thead>
           <tr>
             <th>Rank</th>
             <th>Player</th>
             <th>Score</th>
             <th>Date</th>
           </tr>
         </thead>
         <tbody id="leaderboard-body">
         </tbody>
       </table>
     </div>
     
     <button id="start-game-btn" class="primary-button">🎮 Start New Game</button>
   </div>
 
   <!-- Game Screen -->
   <div id="game-screen" class="screen">
     <div class="game-header">
       <div class="game-info">
         <div class="score-display">
           <span class="label">Score:</span>
           <span id="current-score">0</span>
         </div>
         <div class="level-display">
           <span class="label">Level:</span>
           <span id="current-level">1</span>
         </div>
         <div class="combo-display">
           <span id="combo-text"></span>
         </div>
       </div>
       <div class="game-controls">
         <div class="volume-control">
           <button id="volume-btn" class="control-button">🔊</button>
           <div id="volume-slider-container" class="volume-slider-container">
             <input type="range" id="volume-slider" class="volume-slider" min="0" max="100" value="70">
           </div>
         </div>
         <button id="pause-btn" class="control-button">⏸️</button>
         <button id="back-to-menu-btn" class="control-button">🏠</button>
       </div>
     </div>
 
     <div class="game-container">
       <div id="game-grid" class="game-grid">
         <!-- Grid will be generated by JavaScript -->
       </div>
       <div id="game-over-overlay" class="game-over-overlay">
         <div class="game-over-text">GAME OVER!</div>
       </div>
     </div>
 
     <div class="game-footer">
       <div class="next-preview">
         <span class="label">Next Row:</span>
         <div id="next-row-preview" class="next-row">
           <!-- Preview will be generated by JavaScript -->
         </div>
       </div>
       <div class="control-hints">
         <div id="gamepad-status" class="gamepad-status"></div>
         <div class="keyboard-hints">
           <div class="hint-row">
             <span>⌨️ <strong>Arrow Keys:</strong> Move cursor</span>
             <span><strong>Space/Enter:</strong> Select/Swap</span>
           </div>
           <div class="hint-row">
             <span><strong>Esc:</strong> Pause</span>
             <span><strong>R:</strong> Restart (when game over)</span>
           </div>
         </div>
         <div class="gamepad-hints">
           <div class="hint-row">
             <span>🎮 <strong>D-pad/Stick:</strong> Move cursor</span>
             <span><strong>A:</strong> Select/Swap</span>
           </div>
           <div class="hint-row">
             <span><strong>Start:</strong> Pause</span>
             <span><strong>RB:</strong> Quick restart</span>
           </div>
         </div>
       </div>
     </div>
   </div>
 
   <!-- Game Over Modal -->
   <div id="game-over-modal" class="modal">
     <div class="modal-content">
       <h2>🎯 Game Over!</h2>
       <div class="final-score">
         <span class="label">Final Score:</span>
         <span id="final-score-value">0</span>
       </div>
       <div id="score-message"></div>
       <div id="anonymous-message" style="display: none;">
         <p>Log in to save your score!</p>
         <a href="https://meta.discourse.org" target="_blank" class="login-link">Login to Discourse</a>
       </div>
       <div class="modal-buttons">
         <button id="play-again-btn" class="primary-button">🔄 Play Again</button>
+        <button id="view-leaderboard-btn" class="secondary-button">🏆 Show Leaderboard</button>
-        <button id="view-leaderboard-btn" class="secondary-button">🏆 View Leaderboard</button>
       </div>
     </div>
   </div>
 
   <!-- Pause Modal -->
   <div id="pause-modal" class="modal">
     <div class="modal-content">
       <h2>⏸️ Game Paused</h2>
       <div class="modal-buttons">
         <button id="resume-btn" class="primary-button">▶️ Resume</button>
         <button id="quit-btn" class="secondary-button">🏠 Main Menu</button>
       </div>
     </div>
   </div>
 </div>

CSS Changes

 #app {
   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
   width: 100%;
   max-width: 600px;
   height: 600px;
   margin: 0 auto;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 12px;
   box-shadow: 0 10px 30px rgba(0,0,0,0.3);
   overflow: hidden;
   position: relative;
 }
 
 .screen {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: none;
   flex-direction: column;
   padding: 15px;
   box-sizing: border-box;
 }
 
 .screen.active {
   display: flex;
 }
 
 .header {
   text-align: center;
   margin-bottom: 20px;
 }
 
 .header h1 {
   color: white;
   font-size: 1.8em;
   margin: 0;
   text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
 }
 
 .header p {
   color: rgba(255,255,255,0.9);
   margin: 5px 0 0 0;
   font-size: 0.95em;
 }
 
 .leaderboard-container {
   flex: 1;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 15px;
   margin-bottom: 15px;
   overflow-y: auto;
 }
 
 .leaderboard-container h2 {
   margin: 0 0 15px 0;
   color: #333;
   text-align: center;
 }
 
 #leaderboard-table {
   width: 100%;
   border-collapse: collapse;
   font-size: 0.9em;
 }
 
 #leaderboard-table th,
 #leaderboard-table td {
   padding: 8px 12px;
   text-align: left;
   border-bottom: 1px solid #eee;
 }
 
 #leaderboard-table th {
   background: #f8f9fa;
   font-weight: 600;
   color: #555;
 }
 
 #leaderboard-table tr:nth-child(even) {
   background: #f8f9fa;
 }
 
 .rank-medal {
   font-size: 1.2em;
 }
 
 .primary-button {
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border: none;
   color: white;
   font-size: 1.2em;
   font-weight: 600;
   padding: 15px 30px;
   border-radius: 25px;
   cursor: pointer;
   box-shadow: 0 4px 15px rgba(0,0,0,0.2);
   transition: all 0.3s ease;
 }
 
 .primary-button:hover {
   transform: translateY(-2px);
   box-shadow: 0 6px 20px rgba(0,0,0,0.3);
 }
 
 .secondary-button {
+  background: linear-gradient(45deg, #667eea, #764ba2);
+  border: 2px solid rgba(255,255,255,0.3);
-  background: rgba(255,255,255,0.2);
-  border: 2px solid rgba(255,255,255,0.5);
   color: white;
+  font-size: 1.1em;
-  font-size: 1em;
   font-weight: 600;
+  padding: 12px 24px;
-  padding: 10px 20px;
   border-radius: 20px;
   cursor: pointer;
+  box-shadow: 0 4px 15px rgba(0,0,0,0.15);
   transition: all 0.3s ease;
 }
 
 .secondary-button:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(0,0,0,0.25);
+  border-color: rgba(255,255,255,0.6);
-  background: rgba(255,255,255,0.3);
-  border-color: rgba(255,255,255,0.8);
 }
 
 .game-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-bottom: 8px;
 }
 
 .game-info {
   display: flex;
   gap: 20px;
   flex-wrap: wrap;
 }
 
 .score-display, .level-display {
   font-weight: 600;
   color: #333;
 }
 
 .label {
   color: #666;
   margin-right: 5px;
 }
 
 #current-score, #current-level {
   color: #ff6b6b;
   font-size: 1.2em;
 }
 
 .combo-display {
   font-weight: bold;
   color: #feca57;
   min-width: 80px;
 }
 
 .game-controls {
   display: flex;
   gap: 10px;
   align-items: center;
 }
 
 .volume-control {
   position: relative;
   display: flex;
   align-items: center;
 }
 
 .volume-slider-container {
   position: absolute;
   top: 50%;
   right: 100%;
   transform: translateY(-50%);
   background: rgba(255, 255, 255, 0.95);
   border-radius: 20px;
   padding: 8px 15px;
   margin-right: 8px;
   box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
   opacity: 0;
   visibility: hidden;
   transition: all 0.3s ease;
   z-index: 100;
 }
 
 .volume-slider-container.active {
   opacity: 1;
   visibility: visible;
 }
 
 .volume-slider {
   width: 80px;
   height: 4px;
   background: #ddd;
   border-radius: 2px;
   outline: none;
   cursor: pointer;
   -webkit-appearance: none;
   appearance: none;
 }
 
 .volume-slider::-webkit-slider-thumb {
   -webkit-appearance: none;
   appearance: none;
   width: 16px;
   height: 16px;
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border-radius: 50%;
   cursor: pointer;
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
   transition: all 0.2s ease;
 }
 
 .volume-slider::-webkit-slider-thumb:hover {
   transform: scale(1.2);
 }
 
 .volume-slider::-moz-range-thumb {
   width: 16px;
   height: 16px;
   background: linear-gradient(45deg, #ff6b6b, #feca57);
   border-radius: 50%;
   cursor: pointer;
   border: none;
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
 }
 
 .volume-slider::-webkit-slider-track {
   background: linear-gradient(to right, #ff6b6b 0%, #feca57 100%);
   height: 4px;
   border-radius: 2px;
 }
 
 .volume-slider::-moz-range-track {
   background: linear-gradient(to right, #ff6b6b 0%, #feca57 100%);
   height: 4px;
   border-radius: 2px;
   border: none;
 }
 
 .control-button {
   background: rgba(255,255,255,0.2);
   border: none;
   color: white;
   font-size: 1.5em;
   padding: 8px 12px;
   border-radius: 8px;
   cursor: pointer;
   transition: all 0.3s ease;
 }
 
 .control-button:hover {
   background: rgba(255,255,255,0.3);
 }
 
 .control-button.muted {
   opacity: 0.6;
   background: rgba(255, 255, 255, 0.1);
 }
 
 .sound-pulse {
   animation: sound-pulse 0.3s ease-out;
 }
 
 @keyframes sound-pulse {
   0% { transform: scale(1); }
   50% { transform: scale(1.05); opacity: 0.8; }
   100% { transform: scale(1); }
 }
 
 .game-container {
   flex: 1;
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 10px 0;
 }
 
 .game-grid {
   display: grid;
   grid-template-columns: repeat(6, 1fr);
   grid-template-rows: repeat(12, 1fr);
   gap: 2px;
   background: rgba(0,0,0,0.3);
   border-radius: 8px;
   padding: 6px;
   width: 240px;
   height: 320px;
 }
 
 .block {
   background: rgba(255,255,255,0.9);
   border-radius: 4px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.4em;
   cursor: pointer;
   transition: all 0.2s ease;
   user-select: none;
   border: 2px solid transparent;
   position: relative;
 }
 
 .block:hover {
   transform: scale(1.05);
   box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 }
 
 .block.selected {
   border-color: #feca57;
   box-shadow: 0 0 10px rgba(254, 202, 87, 0.5);
 }
 
 .block.cursor {
   border-color: #00d4ff;
   box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   animation: cursor-pulse 1.5s ease-in-out infinite;
 }
 
 .block.cursor.selected {
   border-color: #ff6b6b;
   box-shadow: 0 0 15px rgba(255, 107, 107, 0.8), 0 0 25px rgba(0, 212, 255, 0.6);
 }
 
 @keyframes cursor-pulse {
   0%, 100% { 
     box-shadow: 0 0 15px rgba(0, 212, 255, 0.8), inset 0 0 10px rgba(0, 212, 255, 0.3);
   }
   50% { 
     box-shadow: 0 0 25px rgba(0, 212, 255, 1), inset 0 0 15px rgba(0, 212, 255, 0.5);
   }
 }
 
 .block.falling {
   animation: fall 0.4s ease-out;
 }
 
 .block.matched {
   animation: matched-pop 0.5s ease-out forwards;
 }
 
 .block.swapping-right {
   animation: swap-right 0.3s ease-in-out;
 }
 
 .block.swapping-left {
   animation: swap-left 0.3s ease-in-out;
 }
 
 .block.swapping-up {
   animation: swap-up 0.3s ease-in-out;
 }
 
 .block.swapping-down {
   animation: swap-down 0.3s ease-in-out;
 }
 
 .block.new-row {
   animation: slide-up 0.4s ease-out;
 }
 
 .block.gravity-fall {
   animation: gravity-drop 0.3s ease-in;
 }
 
 @keyframes fall {
   from { 
     transform: translateY(-100%);
     opacity: 0.7;
   }
   to { 
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes matched-pop {
   0% { 
     transform: scale(1);
     opacity: 1;
   }
   30% { 
     transform: scale(1.4) rotate(10deg);
     opacity: 1;
     background: linear-gradient(45deg, #ff6b6b, #feca57);
   }
   70% { 
     transform: scale(1.6) rotate(-5deg);
     opacity: 0.8;
   }
   100% { 
     transform: scale(0) rotate(180deg);
     opacity: 0;
   }
 }
 
 @keyframes swap-right {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(100% + 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-left {
   0% { transform: translateX(0); }
   50% { transform: translateX(calc(-100% - 2px)); }
   100% { transform: translateX(0); }
 }
 
 @keyframes swap-up {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(-100% - 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes swap-down {
   0% { transform: translateY(0); }
   50% { transform: translateY(calc(100% + 2px)); }
   100% { transform: translateY(0); }
 }
 
 @keyframes slide-up {
   0% {
     transform: translateY(100%);
     opacity: 0.6;
   }
   30% {
     transform: translateY(50%);
     opacity: 0.8;
   }
   100% {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 @keyframes gravity-drop {
   0% {
     transform: translateY(-100%);
     opacity: 0.8;
   }
   60% {
     transform: translateY(10%);
     opacity: 0.9;
   }
   100% {
     transform: translateY(0);
     opacity: 1;
   }
 }
 
 .game-footer {
   background: rgba(255,255,255,0.95);
   border-radius: 8px;
   padding: 10px 15px;
   margin-top: 8px;
 }
 
 .control-hints {
   margin-top: 10px;
 }
 
 .gamepad-status {
   text-align: center;
   font-size: 0.75em;
   color: #28a745;
   font-weight: 600;
   margin-bottom: 8px;
   display: none;
 }
 
 .keyboard-hints, .gamepad-hints {
   font-size: 0.8em;
   color: #666;
   margin: 5px 0;
 }
 
 .gamepad-hints {
   border-top: 1px solid #eee;
   padding-top: 8px;
   margin-top: 8px;
 }
 
 .hint-row {
   display: flex;
   justify-content: space-between;
   margin: 2px 0;
   gap: 15px;
 }
 
 .hint-row span {
   flex: 1;
 }
 
 .hint-row strong {
   color: #333;
 }
 
 .next-row {
   display: flex;
   gap: 5px;
   margin-top: 5px;
 }
 
 .next-block {
   width: 30px;
   height: 30px;
   border-radius: 4px;
   background: rgba(255,255,255,0.9);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 1.2em;
   border: 1px solid #ddd;
 }
 
 .modal {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0,0,0,0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 1000;
 }
 
 .modal.active {
   display: flex;
 }
 
 .modal-content {
   background: white;
   border-radius: 12px;
   padding: 30px;
   text-align: center;
   max-width: 400px;
   width: 90%;
   box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 }
 
 .modal-content h2 {
   margin: 0 0 20px 0;
   color: #333;
 }
 
 .final-score {
   margin: 20px 0;
   font-size: 1.3em;
 }
 
 #final-score-value {
   color: #ff6b6b;
   font-weight: bold;
   font-size: 1.5em;
 }
 
 .modal-buttons {
   display: flex;
   gap: 15px;
   justify-content: center;
   margin-top: 25px;
   flex-wrap: wrap;
 }
 
 .login-link {
   display: inline-block;
   background: #0088cc;
   color: white;
   padding: 10px 20px;
   border-radius: 20px;
   text-decoration: none;
   margin: 10px 0;
   transition: background 0.3s ease;
 }
 
 .login-link:hover {
   background: #0066aa;
 }
 
 #leaderboard-loading {
   text-align: center;
   color: #666;
   font-style: italic;
   padding: 40px;
 }
 
 #score-message {
   margin: 15px 0;
   font-weight: 600;
   color: #333;
 }
 
 .screen-shake {
   animation: shake 0.3s ease-in-out;
 }
 
 @keyframes shake {
   0%, 100% { transform: translateX(0) translateY(0); }
   25% { transform: translateX(-2px) translateY(-1px); }
   50% { transform: translateX(2px) translateY(1px); }
   75% { transform: translateX(-1px) translateY(-2px); }
 }
 
 .game-over-flash {
   animation: game-over-flash 0.5s ease-out;
 }
 
 @keyframes game-over-flash {
   0% { 
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   }
   30% { 
     background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
     transform: scale(1.02);
   }
   60% {
     background: linear-gradient(135deg, #ff6666 0%, #ee2222 100%);
     transform: scale(1.01);
   }
   100% { 
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     transform: scale(1);
   }
 }
 
 .game-over-overlay {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(255, 0, 0, 0.8);
   display: none;
   align-items: center;
   justify-content: center;
   z-index: 500;
   border-radius: 8px;
 }
 
 .game-over-overlay.active {
   display: flex;
   animation: game-over-overlay-appear 0.3s ease-out;
 }
 
 .game-over-text {
   font-size: 2.5em;
   font-weight: bold;
   color: white;
   text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
   animation: game-over-text-pulse 0.6s ease-in-out;
 }
 
 @keyframes game-over-overlay-appear {
   0% { 
     opacity: 0;
     background: rgba(255, 0, 0, 0);
   }
   100% { 
     opacity: 1;
     background: rgba(255, 0, 0, 0.8);
   }
 }
 
 @keyframes game-over-text-pulse {
   0% { 
     transform: scale(0.5);
     opacity: 0;
   }
   50% { 
     transform: scale(1.2);
     opacity: 1;
   }
   100% { 
     transform: scale(1);
     opacity: 1;
   }
 }
 
 .combo-particles {
   position: absolute;
   pointer-events: none;
   width: 100%;
   height: 100%;
 }
 
 .particle {
   position: absolute;
   font-size: 1.2em;
   animation: particle-burst 1s ease-out forwards;
   pointer-events: none;
 }
 
 @keyframes particle-burst {
   0% {
     opacity: 1;
     transform: scale(0.5);
   }
   50% {
     opacity: 0.8;
     transform: scale(1.2);
   }
   100% {
     opacity: 0;
     transform: scale(0.3) translateY(-50px);
   }
 }
 
 @media (max-width: 480px) {
   #app {
     border-radius: 0;
     height: 100vh;
     max-width: 100%;
   }
   
   .screen {
     padding: 10px;
   }
   
   .game-grid {
     width: 200px;
     height: 280px;
   }
   
   .block {
     font-size: 1.2em;
   }
   
   .game-info {
     font-size: 0.85em;
   }
   
   .modal-content {
     width: 95%;
     padding: 20px;
   }
   
   .header h1 {
     font-size: 1.5em;
   }
   
   .header p {
     font-size: 0.85em;
   }
   
   .keyboard-hints, .gamepad-hints {
     font-size: 0.7em;
   }
   
   .hint-row {
     gap: 10px;
   }
 }

Preview