Created
November 2, 2020 08:47
-
-
Save timoschinkel/fc73cedb1da263a47dd4ee896b04358b to your computer and use it in GitHub Desktop.
Tetris
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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