Skip to content

Commit

Permalink
publish: tetris
Browse files Browse the repository at this point in the history
  • Loading branch information
zuzhi committed Aug 2, 2023
1 parent b6aa9b0 commit 3922931
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 59 deletions.
264 changes: 209 additions & 55 deletions content/js/tetris/tetris.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ const tetrominoes = [
[[1, 1, 1], [0, 1, 0]] // T
];

// Define a function to get the next piece
function getNextTetromino() {
const tetromino = tetrominoes[Math.floor(Math.random() * tetrominoes.length)];
return tetromino;
}

// Define the game board as a 2D array
const board = Array.from({ length: boardHeight }, () => Array(boardWidth).fill(0));
// console.log("board:", board);

let nextTetromino = getNextTetromino();

// Define the current tetromino and its position
let currentTetromino = tetrominoes[Math.floor(Math.random() * tetrominoes.length)];
let currentX = boardWidth / 2 - currentTetromino[0].length / 2;
Expand All @@ -33,44 +41,163 @@ const ctx = canvas.getContext('2d');
//const pauseButton = document.getElementById('pauseButton');

let isPaused = false;
let isGameOver = false;
let lines = 0;
let level = 1;
let score = 0;
let speed = 800;

const levelSpeeds = [800, 600, 400, 300, 200, 100, 50];

// Define a function to calculate the level based on the score
function calculateLevel(score) {
return Math.floor(Math.log(score / 500 + 1) / Math.log(2) + 1);
}

// Define a function to update the speed based on the level
function updateSpeed() {
level = calculateLevel(score);
speed = levelSpeeds[level - 1];
}

const fontGame = 18 + 'px Courier';
const fontGameBorder = 18 + 'px Victor Mono';
const fontGameInfo = 18 + 'px Victor Mono';
const squareSize = 24;
const dotSize = 2;
const offsetX = (canvas.width / 2.0) - (boardWidth / 2.0 * squareSize) - squareSize / 2.0;
const offsetY = squareSize * 2;

// Define a function to draw a square on the canvas
function drawSquare(x, y) {
// set square color to green
ctx.font = '18px courier';
// set font to bold
function drawSquare(x, y, color='#05fa2a') {
const squareX = x * (squareSize - 5) + offsetX + 6;
const squareY = y * squareSize + offsetY - 3;
ctx.font = fontGame;
ctx.fontWeight = 'bold';
ctx.fillStyle = '#05fa2a';
const squareSize = 20;
const offsetX = (canvas.width / 2.0 - (boardWidth * squareSize / 2.0) - (squareSize / 2.0)) + squareSize;
// set square color to green
ctx.fillStyle = color;
// use fillText to draw square, text is "[]", make sure it's right bottom corner is at x, y
ctx.fillText('[]', x * 20 + 2 + offsetX, y * 20 + 18);
ctx.fillText('[]', squareX, squareY);
}

// Define a function to draw dot in the righ bottom of every square
function drawDot(x, y) {
const squareSize = 20;
const dotSize = 2;
const offsetX = (canvas.width / 2.0 - (boardWidth * squareSize / 2.0) - (squareSize / 2.0)) + squareSize;
const squareX = x * squareSize + offsetX;
const squareY = y * squareSize;
const squareX = x * (squareSize - 5) + offsetX;
const squareY = y * squareSize + squareSize;
const dotX = squareX + squareSize - dotSize;
const dotY = squareY + squareSize - dotSize;

ctx.fillStyle = 'green';
ctx.fillStyle = '#05fa2a';
ctx.fillRect(dotX, dotY, dotSize, dotSize);
}

// Define a function to draw the border
function drawBorder() {
const leftBorderX = offsetX - squareSize/2 - 5;
const rightBorderX = leftBorderX + boardWidth * squareSize - squareSize;
const bottomBorderY = offsetY + boardHeight * squareSize;
const borderY = 0;

ctx.font = fontGameBorder;
ctx.fillStyle = 'green';

// Left border
for (let i = 0; i < boardHeight; i++) {
ctx.fillText('<!', leftBorderX, borderY + i * squareSize + offsetY);
}
// Right border
for (let i = 0; i < boardHeight; i++) {
ctx.fillText('!>', rightBorderX, borderY + i * squareSize + offsetY);
}
// Bottom border
for (let i = 0; i < boardWidth; i++) {
if (i === 0) {
ctx.fillText('<!', leftBorderX + i * squareSize, bottomBorderY);
}
if (i === boardWidth - 1) {
ctx.fillText('!>', rightBorderX, bottomBorderY);
}
}
// Outer bottom border
for (let i = 0; i < boardWidth * 2 - 2; i++) {
if (i !== 0 && i !== 1) {
ctx.fillText('=', leftBorderX + i/2 * squareSize, bottomBorderY);
if (i % 2 === 1) {
ctx.fillText('/', leftBorderX + i/2 * squareSize, bottomBorderY + squareSize);
} else {
ctx.fillText('\\', leftBorderX + i/2 * squareSize, bottomBorderY + squareSize);
}
}
}
}

// Define a function to pad string with spaces
function padString(left, right, length) {
if (left.length + right.length >= length) {
return left + right; // If the string is already longer or equal to the desired length, return the original string.
}
const numberOfSpacesToAdd = length - left.length - right.length;
const spaces = " ".repeat(numberOfSpacesToAdd);
const extendedStr= left + spaces + right;

return extendedStr;
}

// Define a function to draw the score
function drawGameInfo() {
ctx.font = fontGameInfo;
// set square color to green
ctx.fillStyle = '#05fa2a';
const offsetX = 0;
const offsetY = squareSize * 2;

ctx.fillText(padString('ПОЛНЫХ СТРОК: ', lines.toString(), 20), offsetX, offsetY);
ctx.fillText(padString('УРОВЕНЬ: ', level.toString(), 20), offsetX, offsetY + squareSize);
ctx.fillText('H: НАЛЕВО L: НАПРАВО', (canvas.width / 1.5) - squareSize / 2, offsetY + squareSize);
ctx.fillText(' K: ПОВОРОТ', (canvas.width / 1.5) - squareSize / 2, offsetY + squareSize * 2);
ctx.fillText(padString(' СЧЕТ: ', score.toString(), 12), offsetX, offsetY + squareSize * 2);
ctx.fillStyle = 'green';
ctx.fillText('4:УСКОРИТЬ 5:СБРОСИТЬ', (canvas.width / 1.5) - squareSize / 2, offsetY + squareSize * 3);
ctx.fillText('1: ПОКАЗАТЬ СЛЕДУЮЩУЮ', (canvas.width / 1.5) - squareSize / 2, offsetY + squareSize * 4);
ctx.fillText('0: СТЕРЕТЬ ЭТОТ ТЕКСТ', (canvas.width / 1.5) - squareSize / 2, offsetY + squareSize * 5);
ctx.fillText(' ПРОБЕЛ - СБРОСИТЬ', (canvas.width / 1.5) - squareSize / 2, offsetY + squareSize * 6);
}

function drawNextTetromino(nextTetromino) {
const nextTetrominoHeight = nextTetromino.length;
const nextTetrominoWidth = nextTetromino[0].length;
const offsetX = squareSize * 4;
const offsetY = squareSize * 12;

for (let y = 0; y < nextTetrominoHeight; y++) {
for (let x = 0; x < nextTetrominoWidth; x++) {
if (nextTetromino[y][x]) {
let squareX = x * squareSize + offsetX;
let squareY = y * squareSize + offsetY;
ctx.font = fontGame;
ctx.fontWeight = 'bold';
// set square color to green
ctx.fillStyle = '#05fa2a';
// use fillText to draw square, text is "[]", make sure it's right bottom corner is at x, y
ctx.fillText('[]', squareX, squareY);
}
}
}
}

// Define a function to draw the game board
function drawBoard() {
function drawBoard(squareColor='#05fa2a') {
drawBorder();
drawGameInfo();
for (let y = 0; y < boardHeight; y++) {
for (let x = 0; x < boardWidth; x++) {
drawDot(x, y);
if (board[y][x]) {
drawSquare(x, y);
drawSquare(x, y, squareColor);
}
}
}
drawNextTetromino(nextTetromino);
}

// Define a function to rotate the tetromino clockwise by 90 degrees
Expand Down Expand Up @@ -123,18 +250,36 @@ function gameLoop() {
});

// Remove the completed lines from the game board
let rowsCleared = 0;
for (let y = boardHeight - 1; y >= 0; y--) {
if (board[y].every(cell => cell)) {
// Remove the completed line
board.splice(y, 1);
board.unshift(new Array(boardWidth).fill(0));
rowsCleared++;
lines++;
y++;
}
}
if (rowsCleared > 0) {
if (rowsCleared === 1) {
score += 40 * (level + 1);
}
if (rowsCleared === 2) {
score += 100 * (level + 1);
}
if (rowsCleared === 3) {
score += 300 * (level + 1);
}
if (rowsCleared === 4) {
score += 1200 * (level + 1);
}
}
updateSpeed();

// Choose a new tetromino
currentTetromino = tetrominoes[Math.floor(Math.random() * tetrominoes.length)];
// currentTetromino = tetrominoes[1];
currentTetromino = nextTetromino;
nextTetromino = getNextTetromino();
currentX = boardWidth / 2 - currentTetromino[0].length / 2;
if (currentTetromino[0].length % 2 === 1) {
currentX = boardWidth / 2 - (currentTetromino[0].length + 1) / 2;
Expand All @@ -146,6 +291,10 @@ function gameLoop() {
if (board[0].some(cell => cell)) {
// End the game
console.log('Game over!');
isGameOver = true;
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBoard(squareColor='green');
return;
}

Expand All @@ -168,7 +317,7 @@ function gameLoop() {

// Wait for the next frame
// requestAnimationFrame(gameLoop);
setTimeout(gameLoop, 400);
setTimeout(gameLoop, speed);
}

function render() {
Expand All @@ -189,56 +338,61 @@ function render() {
});
}

//// Add event listeners to the pause button
// Add event listeners to the pause button
//pauseButton.addEventListener('click', () => {
// isPaused = !isPaused;
// pauseButton.textContent = isPaused ? 'Resume' : 'Pause';
//});

// Add event listeners to the keyboard
document.addEventListener('keydown', event => {
if (event.code === "ArrowLeft" || event.code === "KeyH") { // Left arrow
// Move the tetromino left
if (currentX > 0) {
currentX--;
render();
// Check if the tetromino has collided with another tetromino
if (currentTetromino.some((row, y) => row.some((cell, x) => cell && board[currentY + y][currentX + x]))) {
// Move the tetromino back
if (!isPaused && !isGameOver) {
if (event.code === "ArrowLeft" || event.code === "KeyH") { // Left arrow
// Move the tetromino left
if (currentX > 0) {
currentX--;
render();
// Check if the tetromino has collided with another tetromino
if (currentTetromino.some((row, y) => row.some((cell, x) => cell && board[currentY + y][currentX + x]))) {
// Move the tetromino back
currentX++;
render();
}
}
} else if (event.code === "ArrowRight" || event.code === "KeyL") { // Right arrow
// Move the tetromino right
if (currentX + currentTetromino[0].length < 10) {
currentX++;
render();
// Check if the tetromino has collided with another tetromino
if (currentTetromino.some((row, y) => row.some((cell, x) => cell && board[currentY + y][currentX + x]))) {
// Move the tetromino back
currentX--;
render();
}
}
}
} else if (event.code === "ArrowRight" || event.code === "KeyL") { // Right arrow
// Move the tetromino right
if (currentX + currentTetromino[0].length < 10) {
currentX++;
} else if (event.code === "ArrowDown" || event.code === "KeyJ") { // Down arrow
// Move the tetromino down
currentY++;
render();
// Check if the tetromino has collided with another tetromino
if (currentTetromino.some((row, y) => row.some((cell, x) => cell && board[currentY + y][currentX + x]))) {
// Move the tetromino back
currentX--;
// Check if the tetromino has collided with the bottom or another tetromino
if (currentY + currentTetromino.length > boardHeight || currentTetromino.some((row, y) => row.some((cell, x) => cell && board[currentY + y][currentX + x]))) {
// Move the tetromino back up
currentY--;
render();
} else {
score++;
}
} else if (event.code === "ArrowUp" || event.code === "KeyK") { // Up arrow
// Rotate the tetromino clockwise
const rotatedTetromino = rotateTetromino(currentTetromino);
if (canMoveTo(currentX, currentY, rotatedTetromino)) {
currentTetromino = rotatedTetromino;
render();
}
}
} else if (event.code === "ArrowDown" || event.code === "KeyJ") { // Down arrow
// Move the tetromino down
currentY++;
render();
// Check if the tetromino has collided with the bottom or another tetromino
if (currentY + currentTetromino.length > boardHeight || currentTetromino.some((row, y) => row.some((cell, x) => cell && board[currentY + y][currentX + x]))) {
// Move the tetromino back up
currentY--;
render();
}
} else if (event.code === "ArrowUp" || event.code === "KeyK") { // Up arrow
// Rotate the tetromino clockwise
const rotatedTetromino = rotateTetromino(currentTetromino);
if (canMoveTo(currentX, currentY, rotatedTetromino)) {
currentTetromino = rotatedTetromino;
render();
}
} else if (event.code === "Space") { // Space
}
if (event.code === "Space") { // Space
// Pause or resume the game
isPaused = !isPaused;
pauseButton.textContent = isPaused ? 'Resume' : 'Pause';
Expand Down
19 changes: 15 additions & 4 deletions content/md/posts/2023-05-25-tetris.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@
:layout :post
:tags ["tetris"]
:toc false
:draft true
:draft false
:slug "tetris"
:hide false
:js true
}
:external-css ["https://fonts.googleapis.com/css?family=Victor+Mono&display=swap"]}

我在电影[俄罗斯方块](http://movie.mtime.com/234329)中第一次知道 Tetris, 也就是俄罗斯方块的英文名, 是由 tetra 和 tennis 组成的, tetra 的意思是 4 (所有落下的方块都是由 4 个正方形组成的), 而 tennis, 也就是网球, 是俄罗斯方块的发明者, 阿列克谢·帕基特诺夫最喜欢的运动.
我在电影[《俄罗斯方块》](https://www.imdb.com/title/tt12758060/)中第一次知道 [Tetris](https://en.wikipedia.org/wiki/Tetris), 也就是俄罗斯方块的英文名, 是由 tetra 和 tennis 组成的, tetra 的意思是 4 (所有落下的方块都是由 4 个正方形组成的), 而 tennis, 也就是网球, 是俄罗斯方块的发明者, Alexey Pajitnov 最喜欢的运动.

电影非常精彩, 剧情发展紧凑, 情节转折也很吸引人, 节奏如《社交网络》般流畅[^1].

## 复刻 Tetris

电影中印象比较深刻的一幕是 Rogers 在 Pajitnov 家和他一起探讨[ Tetris 的功能](https://www.imdb.com/video/vi3500197657/?ref_=ext_shr_lnk) , 初版的俄罗斯方块看起来很复古很好看, 方块里的正方形甚至是一对中括号.

刚好看到 GitHub 的 CEO 使用 GitHub Copilot X 在 18 分钟内[开发并部署了一个贪吃蛇游戏](https://github.blog/2023-05-05-web-summit-rio-2023-building-an-app-in-18-minutes-with-github-copilot-x/), 我也有了 Copilot Chat private beta 的使用资格, 于是决定在 Copilot 的帮助下复刻一个.:)


<div>
<canvas id="canvas" width="600" height="430"></canvas>
<canvas id="canvas" width="800" height="600"></canvas>
</div>

[^1]: [没想到,《俄罗斯方块》还能拍得这么神!](http://content.mtime.com/article/229064178/)

0 comments on commit 3922931

Please sign in to comment.