Skip to content

Instantly share code, notes, and snippets.

@timoschinkel
Created November 2, 2020 08:47
Show Gist options
  • Save timoschinkel/fc73cedb1da263a47dd4ee896b04358b to your computer and use it in GitHub Desktop.
Save timoschinkel/fc73cedb1da263a47dd4ee896b04358b to your computer and use it in GitHub Desktop.
Tetris
{
"name": "tetris",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {},
"devDependencies": {
"@types/keypress": "^2.0.30",
"@types/node": "^14.14.6",
"keypress": "^0.2.1",
"typescript": "^4.0.5"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
// This is very, very heavily inspired by the [video](https://www.youtube.com/watch?v=8OK8_tHeCIA) and
// [code](https://github.com/OneLoneCoder/videos/blob/master/OneLoneCoder_Tetris.cpp) of One Lonely Code.
// Big thanks to him.
import { stdout, stdin } from 'process';
import { WriteStream } from 'tty';
const keypress = require('keypress');
class PlayingField {
private readonly width: number;
private readonly height: number;
public constructor (width: number, height: number) {
this.width = width;
this.height = height;
}
}
// Javascript equivalent of this_thread::sleep_for()
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Define tetromino shapes as 4x4 matrices
const tetrominos = [ // 4x4, . = empty, X = populated
'..X...X...X...X.',
'..X..XX...X.....',
'.....XX..XX.....',
'..X..XX..X......',
'.X...XX...X.....',
'.X...X...XX.....',
'..X...X..XX.....',
];
const width = 12; // original Tetris values + boundary
const height = 18;
// initialize field
// 0 = empty
// const field = new Array(width * height).fill(0, 0, width * height);
const field:Array<number> = [];
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
field[y * width + x] = (x == 0 || x == width - 1 || y == height - 1) ? 9 : 0
}
}
const screen: Array<string> = [];
function doesPieceFit(tetromino: number, rotation: number, posX: number, posY: number): boolean {
// All Field cells >0 are occupied
for (let px = 0; px < 4; px++) {
for (let py = 0; py < 4; py++) {
// Get index into piece
const pi = rotate(px, py, rotation);
// Get index into field
const fi = (posY + py) * width + (posX + px);
// Check that test is in bounds. Note out of bounds does
// not necessarily mean a fail, as the long vertical piece
// can have cells that lie outside the boundary, so we'll
// just ignore them
if (posX + px >= 0 && posX + px < width)
{
if (posY + py >= 0 && posY + py < height)
{
// In Bounds so do collision check
if (tetrominos[tetromino][pi] != '.' && field[fi] != 0)
return false; // fail on first hit
}
}
}
}
return true;
}
function rotate(px: number, py: number, r: number): number {
// 0 1 2 3
// 4 5 6 7
// 8 9 10 11
//12 13 14 15
let position = py * 4 + px;
switch(r % 4) {
case 1:
//12 8 4 0
//13 9 5 1
//14 10 6 2
//15 11 7 3
position = 12 + py - (px * 4);
break;
case 2:
//15 14 13 12
//11 10 9 8
// 7 6 5 4
// 3 2 1 0
position = 15 - (py * 4) - px;
break;
case 3:
// 3 7 11 15
// 2 6 10 14
// 1 5 9 13
// 0 4 8 12
position = 3 - py + (px * 4);
break;
}
return position;
}
function draw(output: WriteStream, screen: Array<String>): void {
output.cursorTo(0, 0);
for (let y = 0; y < height; y++) {
output.write(screen.slice(y * width, y * width + width).join('') + "\n");
}
}
(async() => {
// Game logic
let gameOver = false;
let speedCount = 0, speed = 20;
let forceDown = false;
let currentPiece = Math.floor(Math.random() * 7);
let currentRotation = 3; //Math.floor(Math.random() * 4);
let currentX = Math.floor(width/2);
let currentY = 0;
let score = 0;
let lines: Array<number> = [];
// custom event based controls
keypress(process.stdin); // make `process.stdin` begin emitting "keypress" events
// listen for the "keypress" event
process.stdin.on('keypress', function (ch, key) {
if (key && key.ctrl && key.name == 'c') {
process.stdin.pause();
process.exit();
}
if (key.ctrl || key.shift || key.meta || gameOver) {
return;
}
if (key.name == 'left' && doesPieceFit(currentPiece, currentRotation, currentX - 1, currentY)) {
currentX--;
}
if (key.name == 'right' && doesPieceFit(currentPiece, currentRotation, currentX + 1, currentY)) {
currentX++;
}
if (key.name == 'down' && doesPieceFit(currentPiece, currentRotation, currentX, currentY + 1)) {
currentY++;
}
if (key.name == 'up' && doesPieceFit(currentPiece, currentRotation + 1, currentX, currentY)) {
currentRotation++;
}
});
process.stdin.setRawMode(true);
process.stdin.resume();
while (!gameOver) { // main loop
// Timing =======================
await sleep(50); // Small Step = 1 Game Tick
speedCount++;
forceDown = (speedCount == speed);
// Input ========================
// skip movement and rotation
// Game Logic ===================
if (forceDown) {
speedCount = 0;
if (doesPieceFit(currentPiece, currentRotation, currentX, currentY + 1)) {
currentY++;
} else {
// It can't! Lock the piece in place
for (let px = 0; px < 4; px++) {
for (let py = 0; py < 4; py++) {
if (tetrominos[currentPiece][rotate(px, py, currentRotation)] != '.') {
field[(currentY + py) * width + (currentX + px)] = currentPiece + 1;
}
}
}
// check for lines
for (let py = 0; py < 4; py++) { // we only need to check the rows where we fixated the piece
if(currentY + py < height - 1) {
let line = true;
for(let px = 1; px < width - 1; px++) {
line = line && field[(currentY + py) * width + px] != 0;
}
if (line) {
// Remove Line, set to =
for (let px = 1; px < width - 1; px++) {
field[(currentY + py) * width + px] = 8;
}
lines.push(currentY + py);
}
}
}
score += 25;
if(lines.length > 0) { score += (1 << lines.length) * 100; }
// Pick New Piece
currentX = Math.floor(width / 2);
currentY = 0;
currentRotation = 0;
currentPiece = Math.floor(Math.random() * 7);
// If piece does not fit straight away, game over!
gameOver = doesPieceFit(currentPiece, currentRotation, currentX, currentY) == false;
}
}
// Display ======================
console.clear();
// Draw Field
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
screen[y*width + x] = ' ABCDEFG=#'[field[y*width + x]];
// stdout.cursorTo(x, y);
// stdout.write(' ABCDEFG=#'[field[y*width + x]]);
}
}
// Draw current piece
for (let px = 0; px < 4; px++) {
for (let py = 0; py < 4; py++) {
if (tetrominos[currentPiece][rotate(px, py, currentRotation)] != '.') {
screen[(currentY + py) * width + (currentX + px)] = String.fromCharCode(currentPiece + 65);
// stdout.cursorTo(currentX + px, currentY + py);
// stdout.write(String.fromCharCode(currentPiece + 65));
}
}
}
// Animate Line Completion
if (lines.length > 0) {
// Display Frame (cheekily to draw lines)
draw(stdout, screen);
await sleep(400); // Delay a bit
// remove lines and move all fixated blocks down
lines.forEach((line) => {
for (let px = 1; px < width - 1; px++) {
for (let py = line; py > 0; py--) {
field[py * width + px] = field[(py - 1) * width + px];
}
field[px] = 0;
}
});
lines = [];
}
// Display frame
draw(stdout, screen);
stdout.cursorTo(stdout.columns -2, stdout.rows - 2); // to right bottom of terminal
}
// console.log('GAME OVER!');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment