@manufosela/game-tetris
Complete Tetris game implementation as a Lit 3 web component with full keyboard controls
Demo code (CodePen-ready HTML, CSS, JS)
HTML (html)
<h1>game-tetris Demo</h1>
<p class="description">A complete Tetris game implementation as a web component. Use keyboard controls to play!</p>
<div class="settings">
<div class="setting">
<label for="themeSelect">Theme:</label>
<select id="themeSelect">
<option value="">Default</option>
<option value="theme-neon">Neon</option>
<option value="theme-retro">Retro</option>
</select>
</div>
<div class="setting">
<label for="ghostToggle">Ghost Piece:</label>
<input type="checkbox" id="ghostToggle" checked>
</div>
<div class="setting">
<label for="gridToggle">Show Grid:</label>
<input type="checkbox" id="gridToggle" checked>
</div>
</div>
<div class="game-wrapper" id="gameWrapper">
<game-tetris id="tetris"></game-tetris>
</div>
<div class="controls-info">
<h3>Keyboard Controls</h3>
<div class="controls-grid">
<div class="control-item">
<kbd>Left / A</kbd>
<span>Move left</span>
</div>
<div class="control-item">
<kbd>Right / D</kbd>
<span>Move right</span>
</div>
<div class="control-item">
<kbd>Down / S</kbd>
<span>Soft drop</span>
</div>
<div class="control-item">
<kbd>Up / W / X</kbd>
<span>Rotate</span>
</div>
<div class="control-item">
<kbd>Space</kbd>
<span>Hard drop</span>
</div>
<div class="control-item">
<kbd>P / Esc</kbd>
<span>Pause</span>
</div>
</div>
</div>
<div class="event-log">
<h3>Event Log</h3>
<div class="events" id="events"></div>
</div> CSS (css)
:root {
--bg: #0c0f14;
--bg-elevated: #141923;
--bg-panel: #171d28;
--border: #262f3f;
--text: #f4f6fb;
--text-muted: #a7b0c2;
--text-dim: #7d879b;
--accent: #ff8a3d;
--accent-strong: #ff6a00;
--accent-soft: rgba(255, 138, 61, 0.16);
--shadow: 0 20px 50px rgba(5, 8, 14, 0.45);
--radius-lg: 22px;
--radius-md: 14px;
--radius-sm: 10px;
--max-width: 1160px;
}
* {
box-sizing: border-box;
}
h1 {
color: var(--accent);
margin-bottom: 10px;
text-align: center;
}
.description {
color: var(--text-muted);
margin-bottom: 30px;
text-align: center;
max-width: 600px;
}
.game-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.settings {
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
.setting {
display: flex;
align-items: center;
gap: 8px;
background: var(--bg-elevated);
padding: 10px 15px;
border-radius: 8px;
}
.setting label {
font-size: 12px;
color: var(--text-muted);
}
.setting input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.setting select {
background: var(--bg-panel);
color: var(--text);
border: 1px solid var(--accent);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.event-log {
width: 100%;
max-width: 600px;
background: var(--bg-elevated);
border-radius: 8px;
padding: 15px;
margin-top: 20px;
}
.event-log h3 {
color: var(--accent);
margin: 0 0 10px 0;
font-size: 14px;
}
.events {
max-height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 11px;
}
.events p {
margin: 5px 0;
padding: 5px 10px;
background: var(--bg-panel);
border-radius: 4px;
}
.events p.game-start {
border-left: 3px solid #2ecc71;
}
.events p.game-over {
border-left: 3px solid #e74c3c;
}
.events p.line-clear {
border-left: 3px solid #f39c12;
}
.events p.level-up {
border-left: 3px solid #9b59b6;
}
.events p.score-update {
border-left: 3px solid #3498db;
}
/* Custom theme for tetris */
game-tetris {
--game-tetris-bg: var(--bg-panel);
--game-tetris-board-bg: var(--bg);
--game-tetris-board-border: 3px solid var(--accent);
--game-tetris-panel-bg: var(--bg-elevated);
}
.theme-neon game-tetris {
--game-tetris-bg: #0a0a0a;
--game-tetris-board-bg: #000;
--game-tetris-board-border: 3px solid #0ff;
--game-tetris-panel-bg: #111;
--game-tetris-label-color: #0ff;
--game-tetris-score-color: #0f0;
--game-tetris-button-bg: #0ff;
--game-tetris-button-hover-bg: #0aa;
}
.theme-retro game-tetris {
--game-tetris-bg: #2d2d2d;
--game-tetris-board-bg: #1a1a1a;
--game-tetris-board-border: 3px solid #ff6b35;
--game-tetris-panel-bg: #3d3d3d;
--game-tetris-label-color: #ff6b35;
--game-tetris-score-color: #ffd700;
--game-tetris-button-bg: #ff6b35;
}
.controls-info {
background: var(--bg-elevated);
padding: 15px 20px;
border-radius: 8px;
margin-top: 20px;
max-width: 600px;
}
.controls-info h3 {
color: var(--accent);
margin: 0 0 15px 0;
font-size: 14px;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
font-size: 12px;
}
.control-item {
display: flex;
gap: 10px;
}
.control-item kbd {
background: var(--bg-panel);
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--border);
font-family: monospace;
min-width: 60px;
text-align: center;
} JS (js)
import "https://esm.sh/@manufosela/game-tetris";
const tetris = document.getElementById('tetris');
const gameWrapper = document.getElementById('gameWrapper');
const events = document.getElementById('events');
// Log event helper
function logEvent(message, type) {
const p = document.createElement('p');
p.className = type;
p.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
events.insertBefore(p, events.firstChild);
if (events.children.length > 20) {
events.removeChild(events.lastChild);
}
}
// Event listeners
tetris.addEventListener('game-start', (e) => {
logEvent(`Game started! Level: ${e.detail.level}, Speed: ${e.detail.speed}ms`, 'game-start');
});
tetris.addEventListener('game-over', (e) => {
logEvent(`Game Over! Score: ${e.detail.score}, Level: ${e.detail.level}, Lines: ${e.detail.lines}`, 'game-over');
});
tetris.addEventListener('line-clear', (e) => {
const linesText = e.detail.lines === 1 ? 'line' : 'lines';
logEvent(`Cleared ${e.detail.lines} ${linesText}! Total: ${e.detail.totalLines}`, 'line-clear');
});
tetris.addEventListener('level-up', (e) => {
logEvent(`Level up! Now level ${e.detail.level}, Speed: ${e.detail.speed}ms`, 'level-up');
});
tetris.addEventListener('score-update', (e) => {
if (e.detail.pointsAdded > 10) {
logEvent(`+${e.detail.pointsAdded} points! Total: ${e.detail.score}`, 'score-update');
}
});
// Settings controls
document.getElementById('themeSelect').addEventListener('change', (e) => {
gameWrapper.className = 'game-wrapper ' + e.target.value;
});
document.getElementById('ghostToggle').addEventListener('change', (e) => {
tetris.showGhost = e.target.checked;
});
document.getElementById('gridToggle').addEventListener('change', (e) => {
tetris.showGrid = e.target.checked;
});