Skip to content

Instantly share code, notes, and snippets.

@mLuby
Last active August 14, 2020 16:17
Show Gist options
  • Save mLuby/73316b08c31015e1da813e1fbbed890a to your computer and use it in GitHub Desktop.
Save mLuby/73316b08c31015e1da813e1fbbed890a to your computer and use it in GitHub Desktop.
Tetris with Emojis, works in browser or from NodeJS
// <!DOCTYPE html><html><head><meta charset="utf-8"></head><body></body><script>
class Piece {
constructor([startRow, startCol], shape) {
const colors = {I: "🎽", O: "🌕", T: "⚛️ ", S: "❇️ ", Z: "🅰️ ", J: "🛂", L: "✴️ "};
const shapes = {
I: [[1,1,1,1]],
O: [[1,1], [1,1]],
T: [[0,1,0], [1,1,1]],
S: [[0,1,1], [1,1,0]],
Z: [[1,1,0], [0,1,1]],
J: [[1,0,0], [1,1,1]],
L: [[0,0,1], [1,1,1]],
};
this.position = [startRow, startCol];
this.shape = shape || Object.keys(colors)[Math.floor(Math.random()*Object.keys(colors).length)];
this.color = colors[this.shape];
this.orientation = this._padToSquare(shapes[this.shape]);
}
getTiles () { // [[row, col]]
const results = [];
const [currentRow, currentCol] = this.position;
this.orientation.forEach((tiles, row) => tiles.forEach((tile, col) => {
tile === 1 && results.push([row+currentRow, col+currentCol]);
}));
return results;
}
moveLeft () {
this.position = [this.position[0], this.position[1] - 1];
}
moveRight () {
this.position = [this.position[0], this.position[1] + 1];
}
moveDown () {
this.position = [this.position[0] + 1, this.position[1]];
}
moveUp () {
this.position = [this.position[0] - 1, this.position[1]];
}
rotateCCW () {
const squareMatrix = this.orientation;
let start = 0;
let end = squareMatrix.length - 1; // last index
while (start < end) {
let offset = 0;
while (offset < end) {
[
squareMatrix[start][start+offset], // top left rightward
squareMatrix[start+offset][end], // right top downward
squareMatrix[end][end-offset], // bottom right leftward
squareMatrix[end-offset][start], // left bottom upward
] = [
squareMatrix[end-offset][start], // left bottom upward
squareMatrix[start][start+offset], // top left rightward
squareMatrix[start+offset][end], // right top downward
squareMatrix[end][end-offset], // bottom right leftward
];
offset++;
}
start++;
end--;
}
return squareMatrix; // it's in place but still nice to return
}
rotateCW () {
this.rotateCCW();
this.rotateCCW();
this.rotateCCW();
}
_padToSquare (rows) { // square matrix
const maxSideLength = Math.max(rows.length, rows[0].length);
const minSideLength = Math.min(rows.length, rows[0].length);
const lengthDiff = maxSideLength - minSideLength;
if (lengthDiff === 0) return rows.map(row => row.slice(0)); // no change
if (rows.length === maxSideLength) return rows.map(row => [...row, Array(lengthDiff).fill(0)]);
else return [...rows.map(row => row.slice(0)), ...Array(lengthDiff).fill().map(x => Array(maxSideLength).fill(0))];
}
}
class Game {
constructor ({height, width, tickMs, quitter, printer}) {
this.board = Array(height).fill().map(() => Array(width).fill(0));
this.height = height;
this.width = width;
this.tickMs = tickMs;
this.score = 0;
this.interval = null;
this.activePiece = null;
this.quitter = quitter;
this.printer = printer;
this.addPiece(); // create random active piece
this.printer("", [
"KEY BINDINGS:",
"up arrow: rotate",
"down arrow: move down",
"left arrow: move left",
"right arrow: move right",
"space: pause/unpause",
"escape or q: quit",
"PRESS SPACE TO START.",
].join("\n"));
}
isUnpaused () { // Boolean
return Boolean(this.interval);
}
togglePause () {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
this.print("PAUSED. PRESS SPACE TO UNPAUSE.");
} else this.interval = setInterval(() => this._tick(), this.tickMs);
}
end () {
clearInterval(this.interval);
this.interval = null;
this.print("GAME OVER.");
this.quitter();
}
print (message="") {
const tiles = this.activePiece ? this.activePiece.getTiles() : [];
const board = [
"↘️ "+Array(this.board[0].length).fill("⬇️ ").join("")+"↙️ ",
...this.board.map((cols, row) => ("➡️ "+cols.map((tile, col) => {
if (tiles.filter(([trow, tcol]) => row === trow && col === tcol).length) {
return this.activePiece.color;
} else if (tile !== 0) return tile;
else return "🌫 ";
}).join("")+"⬅️ ")),
"↗️ "+Array(this.board[0].length).fill("⬆️ ").join("")+"↖️ ",
].join("\n");
this.printer(board, "SCORE: "+this.score, message);
}
addPiece (shape) { // omitting shape gives a random piece
this.activePiece = new Piece([0, Math.floor(this.board[0].length / 2) - 1], shape);
}
left () {
this.activePiece.moveLeft();
if (!this.activePiece.getTiles().every(tile => this._isNotCollision(tile))) {
this.activePiece.moveRight(); // if there's a collision then reverse move
}
this.print();
}
right () {
this.activePiece.moveRight();
if (!this.activePiece.getTiles().every(tile => this._isNotCollision(tile))) {
this.activePiece.moveLeft(); // if there's a collision then reverse move
}
this.print();
}
down () {
this.activePiece.moveDown();
if (!this.activePiece.getTiles().every(tile => this._isNotCollision(tile))) {
this.activePiece.moveUp(); // if there's a collision then reset up
}
this.print();
}
rotate () {
this.activePiece.rotateCCW();
if (!this.activePiece.getTiles().every(tile => this._isNotCollision(tile))) {
this.activePiece.rotateCW(); // if there's a collision then reset/rotate back
}
this.print();
}
_tick () {
this.activePiece.moveDown();
if (!this.activePiece.getTiles().every(tile => this._isNotCollision(tile))) {
this.activePiece.moveUp(); // once there's a collision then back up one
// engrave piece into game board
this.activePiece.getTiles().forEach(([row, col]) => this.board[row][col] = this.activePiece.color);
// clear full lines and increase score
while (this.board.filter(row => row.every(tile => tile !== 0)).length > 0) {
this.board = this.board.filter(row => !row.every(tile => tile !== 0));
this.score += this.height - this.board.length;
this.board = [...Array(this.height - this.board.length).fill().map(() => Array(this.width).fill(0)), ...this.board];
}
this.addPiece(); // new active piece of random type
}
this.print();
if (!this.activePiece.getTiles().every(tile => this._isNotCollision(tile))) this.end();
}
_isNotCollision ([row, col]) { // Boolean
const inBounds = row >= 0 && row < this.board.length && col >= 0 && col < this.board[0].length;
return inBounds && this.board[row][col] === 0;
}
}
function readUserInput (key, isCtrl) {
if (isCtrl && key === 'c') return game.end();
else if (game.isUnpaused() || ["space", "q", "escape"].includes(key)) {
;(({
left: game.left,
right: game.right,
down: game.down,
up: game.rotate,
space: game.togglePause,
escape: game.end,
q: game.end,
})[key] || (()=>{})).apply(game);
}
};
if (typeof window === "undefined") { // Node
var quitter = process.exit;
var printer = (...args) => console.log('\033[2J' + args.join("\n")); // console clear
// the following hooks up node's STDIN to register key presses
const readline = require('readline');
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on('keypress', (_, {ctrl, name}) => readUserInput(name, ctrl));
} else { // Browser
var quitter = () => alert("Reload page to restart game.");
var printer = (board, score, message) => document.body.innerText = [
board.replace(/ /g, ""), score, message
].join("\n");
document.body.addEventListener("keydown", ({ctrlKey, code}) =>
readUserInput(code.toLowerCase().replace(/(arrow|key)/, ""), ctrlKey)
);
};
const game = new Game({height: 20, width: 10, tickMs: 250, quitter, printer});
// </script></html>
@mLuby
Copy link
Author

mLuby commented Nov 6, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment