Untitled
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