Untitled

 avatar
unknown
plain_text
9 months ago
13 kB
12
Indexable
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Heartris – Emoji Tetris</title>
<style>
  :root { --cols: 10; --rows: 20; --cell: 28px; }
  body{margin:0;background:#0b0f14;color:#e7f0ff;font-family:system-ui,Segoe UI,Roboto,Arial,Apple Color Emoji,Noto Color Emoji,Emoji;}
  .wrap{max-width:900px;margin:24px auto;padding:16px}
  h1{margin:0 0 8px;font-weight:800;letter-spacing:.3px}
  .meta{display:flex;gap:16px;flex-wrap:wrap;align-items:center}
  .board-wrap{display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-top:12px}
  .board{display:grid;grid-template-columns:repeat(var(--cols), var(--cell));grid-template-rows:repeat(var(--rows), var(--cell));
         background:#111827;border:2px solid #1f2937;border-radius:12px;box-shadow:0 6px 18px rgba(0,0,0,.35);position:relative}
  .cell{width:var(--cell);height:var(--cell);display:flex;align-items:center;justify-content:center;
        font-size:20px;user-select:none;transition:transform .05s}
  .ghost{opacity:.35;filter:grayscale(.5)}
  .hud{min-width:180px}
  .panel{background:#0f172a;border:1px solid #1f2a44;border-radius:12px;padding:12px 14px;margin-bottom:12px}
  .panel h2{margin:0 0 8px;font-size:16px}
  .val{font-size:20px;font-weight:700}
  .next{display:grid;grid-template-columns:repeat(4, var(--cell));grid-template-rows:repeat(4, var(--cell));
        background:#111827;border:1px dashed #243040;border-radius:10px;padding:4px;margin-top:6px}
  .row{height:var(--cell)}
  .keys{display:grid;grid-template-columns:repeat(3,60px);gap:8px;justify-content:center;margin-top:8px}
  .btn{padding:10px 0;background:#1f2937;border:1px solid #334155;border-radius:10px;color:#e7f0ff;font-weight:700;cursor:pointer}
  .btn:active{transform:translateY(1px)}
  .footer{opacity:.7;font-size:12px;margin-top:6px}
  .overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:rgba(5,10,16,.7);
           color:#fff;text-align:center;border-radius:10px;backdrop-filter:blur(2px)}
  .overlay.show{display:flex}
  .overlay .card{background:#0b1220;border:1px solid #1f2a44;border-radius:12px;padding:16px 18px;max-width:80%}
  .overlay h3{margin:0 0 6px}
  .controls ul{margin:6px 0 0 18px}
  .pill{display:inline-block;padding:2px 8px;border-radius:20px;border:1px solid #2b3a57;background:#0b1220;font-size:12px}
</style>
</head>
<body>
  <div class="wrap">
    <h1>β€οΈπŸ§‘πŸ’›πŸ’šπŸ’™πŸ’œπŸ€ŽπŸ–€πŸ€πŸ©΅πŸ©ΆπŸ©· Heartris</h1>
    <div class="meta">
      <div class="pill">↑ rotate β€’ ← β†’ move β€’ ↓ soft-drop β€’ Space hard-drop β€’ P pause β€’ R restart</div>
    </div>

    <div class="board-wrap">
      <div class="board" id="board">
        <div class="overlay" id="overlay">
          <div class="card">
            <h3 id="ov-title">Paused</h3>
            <div id="ov-sub">Press <b>P</b> to resume</div>
          </div>
        </div>
      </div>

      <div class="hud">
        <div class="panel"><h2>Score</h2><div class="val" id="score">0</div></div>
        <div class="panel"><h2>Lines</h2><div class="val" id="lines">0</div></div>
        <div class="panel"><h2>Level</h2><div class="val" id="level">1</div></div>
        <div class="panel">
          <h2>Next</h2>
          <div class="next" id="next"></div>
        </div>
        <div class="panel controls">
          <h2>Touch Controls</h2>
          <div class="keys">
            <button class="btn" data-act="left">←</button>
            <button class="btn" data-act="rotate">β€Ώ</button>
            <button class="btn" data-act="right">β†’</button>
            <button class="btn" data-act="soft">↓</button>
            <button class="btn" data-act="hard" style="grid-column: span 3">HARD DROP</button>
          </div>
          <div class="footer">Tap outside buttons to focus for keyboard.</div>
        </div>
        <div class="panel">
          <button class="btn" id="restart">Restart (R)</button>
          <button class="btn" id="pause">Pause / Resume (P)</button>
        </div>
      </div>
    </div>
  </div>

<script>
(() => {
  // --- Config ---
  const COLS = 10, ROWS = 20;
  const DROP_START_MS = 900; // base speed (decreases per level)
  const SPEED_STEP = 70;     // ms faster per level
  const LINES_PER_LEVEL = 10;

  const EMOJIS = ["❀️","🧑","πŸ’›","πŸ’š","πŸ’™","πŸ’œ","🀎","πŸ–€","🀍","🩡","🩢","🩷"];

  // Tetromino definitions: 4x4 matrices per rotation
  const TETROMINOES = {
    I: [
      ["....",".XX.",".XX.","...."], // prettier I (two-by-two hearts look better in emojis)
      ["....",".XX.",".XX.","...."],
      ["....",".XX.",".XX.","...."],
      ["....",".XX.",".XX.","...."],
    ],
    J: [
      ["X..","XXX","...","..."],
      [".XX",".X.",".X.","..."],
      ["...","XXX","..X","..."],
      [".X.",".X.","XX.","..."],
    ],
    L: [
      ["..X","XXX","...","..."],
      [".X.",".X.",".XX","..."],
      ["...","XXX","X..","..."],
      ["XX.",".X.",".X.","..."],
    ],
    O: [
      [".XX.",".XX.","....","...."],
      [".XX.",".XX.","....","...."],
      [".XX.",".XX.","....","...."],
      [".XX.",".XX.","....","...."],
    ],
    S: [
      [".XX","XX.","...","..."],
      [".X."," .XX",".X.","..."].map(r=>r.replace(" ","")),
      ["...",".XX","XX.","..."],
      [".X.","XX.",".X.","..."],
    ],
    Z: [
      ["XX.",".XX","...","..."],
      [".X.","XX.",".X.","..."],
      ["...","XX.",".XX","..."],
      [".X.",".XX",".X.","..."],
    ],
    T: [
      [".X.","XXX","...","..."],
      [".X.","XX.",".X.","..."],
      ["...","XXX",".X.","..."],
      [".X.",".XX",".X.","..."],
    ],
  };
  const TYPES = Object.keys(TETROMINOES);

  // --- State ---
  const board = Array.from({length: ROWS}, () => Array(COLS).fill(null)); // null or emoji
  let current = null;   // {x,y,shape,rot,emoji}
  let nextType = null;
  let nextEmoji = null;
  let score = 0, lines = 0, level = 1;
  let dropTimer = null, dropMs = DROP_START_MS;
  let paused = false, over = false;

  // --- DOM ---
  const $board = document.getElementById('board');
  const $overlay = document.getElementById('overlay');
  const $ovTitle = document.getElementById('ov-title');
  const $ovSub   = document.getElementById('ov-sub');
  const $score = document.getElementById('score');
  const $lines = document.getElementById('lines');
  const $level = document.getElementById('level');
  const $next  = document.getElementById('next');
  const $pause = document.getElementById('pause');
  const $restart = document.getElementById('restart');

  // Build board cells
  const cells = [];
  for (let r=0;r<ROWS;r++){
    for (let c=0;c<COLS;c++){
      const d=document.createElement('div');
      d.className='cell';
      $board.appendChild(d);
      cells.push(d);
    }
  }
  // Build next preview (4x4)
  const nextCells = [];
  for(let r=0;r<4;r++){
    for(let c=0;c<4;c++){
      const d=document.createElement('div');
      d.className='cell';
      $next.appendChild(d);
      nextCells.push(d);
    }
  }

  // --- Helpers ---
  const randOf = arr => arr[(Math.random()*arr.length)|0];
  function newPiece(type = randOf(TYPES), emoji = randOf(EMOJIS)){
    return { x: 3, y: 0, type, rot: 0, emoji };
  }
  function getShape(p){ return TETROMINOES[p.type][p.rot]; }

  function forEachBlock(p, fn){
    const shape = getShape(p);
    for(let r=0;r<shape.length;r++){
      const row = shape[r];
      for(let c=0;c<row.length;c++){
        if(row[c]==="X"){
          fn(p.y + r, p.x + c);
        }
      }
    }
  }

  function canMove(nx, ny, nrot = current.rot){
    const test = { ...current, x:nx, y:ny, rot:nrot };
    let ok = true;
    forEachBlock(test, (r,c) => {
      if (c<0 || c>=COLS || r<0 || r>=ROWS || (r>=0 && board[r][c])) ok=false;
    });
    return ok;
  }

  function lockPiece(){
    forEachBlock(current, (r,c) => {
      if (r>=0) board[r][c] = current.emoji;
    });
    clearLines();
    spawn();
  }

  function clearLines(){
    let cleared = 0;
    for(let r=ROWS-1;r>=0;){
      if(board[r].every(v => v)){
        board.splice(r,1);
        board.unshift(Array(COLS).fill(null));
        cleared++;
      }else r--;
    }
    if(cleared){
      // Simple scoring: 100, 300, 500, 800 per 1/2/3/4 lines * level
      const table = [0,100,300,500,800];
      score += table[cleared]*level;
      lines += cleared;
      const newLevel = Math.floor(lines / LINES_PER_LEVEL) + 1;
      if(newLevel !== level){
        level = newLevel;
        dropMs = Math.max(120, DROP_START_MS - (level-1)*SPEED_STEP);
        restartDropTimer();
      }
      updateHUD();
    }
  }

  function ghostY(){
    let gy = current.y;
    while (canMove(current.x, gy+1)) gy++;
    return gy;
  }

  function draw(){
    // Clear
    for (let i=0;i<cells.length;i++){ cells[i].textContent=""; cells[i].classList.remove('ghost'); }

    // Draw settled
    for(let r=0;r<ROWS;r++){
      for(let c=0;c<COLS;c++){
        const v = board[r][c];
        if(v){
          cells[r*COLS+c].textContent = v;
        }
      }
    }
    // Draw ghost
    const gy = ghostY();
    const ghostCells = new Set();
    forEachBlock({...current, y:gy}, (r,c)=>{
      if(r>=0) ghostCells.add(r*COLS+c);
    });
    ghostCells.forEach(i=>{
      if(!cells[i].textContent){ cells[i].textContent = current.emoji; cells[i].classList.add('ghost'); }
    });

    // Draw current
    forEachBlock(current, (r,c)=>{
      if(r>=0) cells[r*COLS+c].textContent = current.emoji;
    });

    // Draw next preview
    nextCells.forEach(n=>{ n.textContent=""; n.classList.remove('ghost'); });
    const shape = TETROMINOES[nextType][0];
    for(let r=0;r<shape.length;r++){
      for(let c=0;c<shape[r].length;c++){
        if(shape[r][c]==="X"){
          nextCells[r*4+c].textContent = nextEmoji;
        }
      }
    }
  }

  function updateHUD(){
    $score.textContent = score;
    $lines.textContent = lines;
    $level.textContent = level;
  }

  function showOverlay(title, sub){
    $ovTitle.textContent = title;
    $ovSub.innerHTML = sub;
    $overlay.classList.add('show');
  }
  function hideOverlay(){ $overlay.classList.remove('show'); }

  function spawn(){
    if(nextType==null){ nextType = randOf(TYPES); nextEmoji = randOf(EMOJIS); }
    current = newPiece(nextType, nextEmoji);
    nextType = randOf(TYPES);
    nextEmoji = randOf(EMOJIS);

    // Spawn adjustment for tall shapes
    if(!canMove(current.x, current.y)){
      over = true;
      pause(true);
      showOverlay("Game Over", `Final score: <b>${score}</b><br>Press <b>R</b> to restart`);
      return;
    }
    draw();
  }

  // --- Controls ---
  function rotate(){
    const nrot = (current.rot + 1) % 4;
    if (canMove(current.x, current.y, nrot)) { current.rot = nrot; draw(); return; }
    // simple wall kick: try left/right
    if (canMove(current.x-1, current.y, nrot)) { current.x--; current.rot = nrot; draw(); return; }
    if (canMove(current.x+1, current.y, nrot)) { current.x++; current.rot = nrot; draw(); return; }
  }
  function move(dx){
    if (canMove(current.x+dx, current.y)){ current.x+=dx; draw(); }
  }
  function softDrop(){
    if (canMove(current.x, current.y+1)){ current.y++; score+=1; updateHUD(); draw(); }
    else { lockPiece(); }
  }
  function hardDrop(){
    let step = 0;
    while (canMove(current.x, current.y+1)){ current.y++; step++; }
    if(step){ score += 2*step; updateHUD(); }
    lockPiece();
  }

  function tick(){
    if (paused || over) return;
    softDrop();
  }

  function restartDropTimer(){
    clearInterval(dropTimer);
    dropTimer = setInterval(tick, dropMs);
  }

  function pause(forceState = !paused){
    paused = forceState;
    if (paused){
      clearInterval(dropTimer);
      showOverlay("Paused", "Press <b>P</b> to resume");
    }else{
      hideOverlay();
      restartDropTimer();
    }
  }

  // Keyboard
  window.addEventListener('keydown', (e)=>{
    if (over){
      if (e.key.toLowerCase()==='r') { start(); }
      return;
    }
    const k = e.key;
    if (k==='ArrowLeft') move(-1);
    else if (k==='ArrowRight') move(1);
    else if (k==='ArrowDown') softDrop();
    else if (k==='ArrowUp') rotate();
    else if (k===' ') { e.preventDefault(); hardDrop(); }
    else if (k.toLowerCase()==='p') pause();
    else if (k.toLowerCase()==='r') start();
  });

  // Touch buttons
  document.querySelectorAll('.btn[data-act]').forEach(btn=>{
    const act = btn.getAttribute('data-act');
    const map = {
      left: ()=>move(-1),
      right: ()=>move(1),
      rotate: ()=>rotate(),
      soft: ()=>softDrop(),
      hard: ()=>hardDrop(),
    };
    btn.addEventListener('click', ()=>{ if(!paused && !over) map[act](); });
  });
  $pause.addEventListener('click', ()=>pause());
  $restart.addEventListener('click', ()=>start());

  // --- Init / Start ---
  function start(){
    // reset state
    for (let r=0;r<ROWS;r++) for (let c=0;c<COLS;c++) board[r][c]=null;
    score=0; lines=0; level=1; dropMs=DROP_START_MS; updateHUD();
    nextType=null; nextEmoji=null;
    over=false; paused=false; hideOverlay();
    spawn();
    restartDropTimer();
  }

  start();
})();
</script>
</body>
</html>
Editor is loading...
Leave a Comment