Created
November 30, 2020 09:27
-
-
Save mozurin/026907619b8e2f0cf1761c4197f84f3c to your computer and use it in GitHub Desktop.
Simple tetris game for Pixl.js board
This file contains 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
/** | |
* Simple tetris game for Pixl.js board | |
*/ | |
// rotate right | |
g.setRotation(1); | |
// turn on backlight | |
LED1.write(1); | |
// given constants | |
const hwWidth = 64, hwHeight = 128; | |
const blockSize = 6; | |
const tickInterval = 800; | |
const flashDelay = 100; | |
const gameOverFlashCount = 6; | |
const gameOverMessage = 'GAME OVER'; | |
const gameOverPadding = 10; | |
const gameOverDelay = 2000; | |
// calculated constants | |
const vMargin = Math.floor((hwWidth - 2) % blockSize / 2); | |
const hMargin = Math.floor((hwHeight - 2) % blockSize / 2); | |
const gameMapWidth = Math.floor((hwWidth - 2) / blockSize); | |
const gameMapHeight = Math.floor((hwHeight - 2) / blockSize); | |
// game logic class | |
class Tetris { | |
constructor () | |
{ | |
this._map = Array(gameMapHeight).fill(0).map( | |
() => Array(gameMapWidth).fill(0) | |
); | |
} | |
_drawFrame() | |
{ | |
g.drawRect( | |
vMargin, | |
hMargin, | |
vMargin + gameMapWidth * blockSize + 1, | |
hMargin + gameMapHeight * blockSize + 1 | |
); | |
} | |
_drawBlock(gameX, gameY) | |
{ | |
const blockX = gameX * blockSize + vMargin + 1; | |
const blockY = gameY * blockSize + hMargin + 1; | |
g.drawRect( | |
blockX, | |
blockY, | |
blockX + blockSize - 1, | |
blockY + blockSize - 1 | |
); | |
g.drawRect( | |
blockX + 2, | |
blockY + 2, | |
blockX + blockSize - 3, | |
blockY + blockSize - 3 | |
); | |
} | |
_drawFilled() | |
{ | |
this._map.forEach( | |
(row, gy) => row.forEach( | |
(dot, gx) => dot && this._drawBlock(gx, gy) | |
) | |
); | |
} | |
_drawPiece() | |
{ | |
if (!this._piece) { | |
return; | |
} | |
this._piece.forEach( | |
(col, px) => col.forEach( | |
(dot, py) => { | |
if (dot) { | |
this._drawBlock( | |
px + this._pieceX, | |
py + this._pieceY | |
); | |
} | |
} | |
) | |
); | |
} | |
newPiece() | |
{ | |
switch(Math.floor(Math.random() * 7)) { | |
case 0: | |
this._piece = [ | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
]; | |
break; | |
case 1: | |
this._piece = [ | |
[1, 0, 0], | |
[1, 1, 0], | |
[1, 0, 0], | |
]; | |
break; | |
case 2: | |
this._piece = [ | |
[1, 0, 0], | |
[1, 0, 0], | |
[1, 1, 0], | |
]; | |
break; | |
case 3: | |
this._piece = [ | |
[1, 1, 0], | |
[1, 0, 0], | |
[1, 0, 0], | |
]; | |
break; | |
case 4: | |
this._piece = [ | |
[1, 0, 0], | |
[1, 1, 0], | |
[0, 1, 0], | |
]; | |
break; | |
case 5: | |
this._piece = [ | |
[0, 1, 0], | |
[1, 1, 0], | |
[1, 0, 0], | |
]; | |
break; | |
case 6: | |
this._piece = [ | |
[1, 1], | |
[1, 1], | |
]; | |
break; | |
} | |
this._pieceY = 0; | |
this._pieceX = Math.floor((gameMapWidth - this._piece.length) / 2); | |
} | |
_testPiece(piece, pieceX, pieceY) | |
{ | |
let movable = true; | |
piece.forEach( | |
(col, px) => col.forEach( | |
(dot, py) => { | |
if (!dot) { | |
return; | |
} | |
if ( | |
pieceX + px < 0 || pieceX + px >= gameMapWidth || | |
pieceY + py < 0 || pieceY + py >= gameMapHeight || | |
this._map[pieceY + py][pieceX + px] | |
) { | |
movable = false; | |
} | |
} | |
) | |
); | |
return movable; | |
} | |
_movePiece(dx, dy) | |
{ | |
if ( | |
this._testPiece(this._piece, this._pieceX + dx, this._pieceY + dy) | |
) { | |
this._pieceX += dx; | |
this._pieceY += dy; | |
return true; | |
} | |
return false; | |
} | |
_fixPiece() | |
{ | |
let fix = false; | |
this._piece.forEach( | |
(col, px) => col.forEach( | |
(dot, py) => { | |
if (!dot) { | |
return; | |
} | |
if ( | |
this._pieceY + py >= gameMapHeight - 1 || | |
this._map[this._pieceY + py + 1][ | |
this._pieceX + px | |
] | |
) { | |
fix = true; | |
} | |
} | |
) | |
); | |
if (fix) { | |
this._piece.forEach( | |
(col, px) => col.forEach( | |
(dot, py) => { | |
if (!dot) { | |
return; | |
} | |
this._map[this._pieceY + py][ | |
this._pieceX + px | |
] = 1; | |
} | |
) | |
); | |
this._piece = undefined; | |
this._pieceX = this._pieceY = 0; | |
} | |
return fix; | |
} | |
tickAndFixPiece() | |
{ | |
if (this._piece && !this._movePiece(0, 1)) { | |
return this._fixPiece(); | |
} | |
return false; | |
} | |
movePieceLeft() | |
{ | |
if (!this._piece) { | |
return false; | |
} | |
return this._movePiece(-1, 0); | |
} | |
movePieceRight() | |
{ | |
if (!this._piece) { | |
return false; | |
} | |
return this._movePiece(1, 0); | |
} | |
dropPiece() | |
{ | |
if (!this._piece) { | |
return false; | |
} | |
let offset = 0; | |
while ( | |
this._testPiece(this._piece, this._pieceX, this._pieceY + offset) | |
) { | |
offset++; | |
} | |
return this._movePiece(0, offset - 1); | |
} | |
rotatePiece() | |
{ | |
if (!this._piece) { | |
return false; | |
} | |
const newPiece = this._piece.map( | |
(_, px) => this._piece[0].map( | |
(_, py) => this._piece[py][this._piece.length - px - 1] | |
) | |
); | |
if (this._testPiece(newPiece, this._pieceX, this._pieceY)) { | |
this._piece = newPiece; | |
return true; | |
} | |
return false; | |
} | |
hasFilledRow() | |
{ | |
return this._map.some( | |
(row, gy) => row.every(dot => dot) | |
); | |
} | |
clearFilledRow() | |
{ | |
return this._map.forEach( | |
(row, gy) => { | |
if (row.every(dot => dot)) { | |
row.fill(0); | |
} | |
} | |
); | |
} | |
truncateEmptyRow() | |
{ | |
const nonEmptyRows = this._map.filter(row => row.some(dot => dot)); | |
this._map = Array(gameMapHeight - nonEmptyRows.length).fill(0).map( | |
() => Array(gameMapWidth).fill(0) | |
).concat(nonEmptyRows); | |
} | |
isTopRowFilled() | |
{ | |
return this._map.slice(0, 2).some( | |
row => row.some(dot => dot) | |
); | |
} | |
draw() | |
{ | |
g.clear(); | |
this._drawFrame(); | |
this._drawFilled(); | |
this._drawPiece(); | |
g.flip(); | |
} | |
drawGameOver() | |
{ | |
const textWidth = g.stringWidth(gameOverMessage); | |
const boxWidth = textWidth + gameOverPadding * 2; | |
const boxHeight = g.getFontHeight() + gameOverPadding * 2; | |
const boxX = Math.floor((hwWidth - boxWidth) / 2); | |
const boxY = Math.floor((hwHeight - boxHeight) / 2); | |
g.setColor(0); | |
g.fillRect(boxX, boxY, boxX + boxWidth - 1, boxY + boxHeight - 1); | |
g.setColor(1); | |
g.drawRect(boxX, boxY, boxX + boxWidth - 1, boxY + boxHeight - 1); | |
g.drawString( | |
'GAME OVER', | |
boxX + gameOverPadding, | |
boxY + gameOverPadding | |
); | |
g.flip(); | |
} | |
} | |
// main timeline management | |
let tickTimer; | |
let game = new Tetris(); | |
game.newPiece(); | |
game.draw(); | |
function tickFunc() | |
{ | |
if (!game._piece) { | |
game.newPiece(); | |
} else { | |
if (game.tickAndFixPiece()) | |
{ | |
if (game.hasFilledRow()) { | |
game.clearFilledRow(); | |
LED1.write(0); | |
clearInterval(tickTimer); | |
game.draw(); | |
setTimeout( | |
function () | |
{ | |
LED1.write(1); | |
game.truncateEmptyRow(); | |
game.draw(); | |
tickTimer = setInterval(tickFunc, tickInterval); | |
}, | |
flashDelay | |
); | |
return; | |
} else if (game.isTopRowFilled()) { | |
// game over | |
game.draw(); | |
game.drawGameOver(); | |
LED1.write(0); | |
clearInterval(tickTimer); | |
let counter = 0; | |
let ledFlag = true; | |
const flashTimer = setInterval( | |
function () | |
{ | |
LED1.write(ledFlag); | |
ledFlag = !ledFlag; | |
if (++counter >= gameOverFlashCount) { | |
LED1.write(1); | |
clearInterval(flashTimer); | |
setTimeout( | |
function () | |
{ | |
game = new Tetris(); | |
game.newPiece(); | |
game.draw(); | |
tickTimer = setInterval( | |
tickFunc, | |
tickInterval | |
); | |
}, | |
gameOverDelay | |
) | |
} | |
}, | |
flashDelay | |
); | |
return; | |
} | |
} | |
} | |
game.draw(); | |
} | |
tickTimer = setInterval(tickFunc, tickInterval); | |
// button 1 == move left | |
setWatch( | |
function() | |
{ | |
if (game.movePieceLeft()) { | |
game.draw(); | |
} | |
}, | |
BTN1, | |
{repeat: true, debounce: 0} | |
); | |
// button 4 == move right | |
setWatch( | |
function() | |
{ | |
if (game.movePieceRight()) { | |
game.draw(); | |
} | |
}, | |
BTN4, | |
{repeat: true, debounce: 0} | |
); | |
// button 3 == rotate piece (right only) | |
setWatch( | |
function() | |
{ | |
if (game.rotatePiece()) { | |
game.draw(); | |
} | |
}, | |
BTN3, | |
{repeat: true, debounce: 0} | |
); | |
// button 2 == drop piece | |
setWatch( | |
function() | |
{ | |
if (game.dropPiece()) { | |
game.draw(); | |
} | |
}, | |
BTN2, | |
{repeat: true, debounce: 0} | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment