Concept
For this project, I created a Tetris. Tetris is a simple game, in which users move or rotate blocks to fill a game field. Once a horizontal line is created with blocks, blocks on the line will be removed and the user will gain a certain amount of points. Users will gain points if:
- Removes blocks by making a horizontal line of blocks.
- Uses hard drop
- Extra points if multiple lines are removed at once
Although I am using a high-resolution screen, I wanted to give a “retro” appearance to the game (8bit). To achieve this effect, I used 8-bit style fonts and images.
Code
Coding the game was indeed complex because the game required not only game logic, which I partially implemented, but also a friendly user interface. For this reason, I used 3 js. files, each responsible of running the code (sketch.js), displaying game interface (Page.js), and running game (Game.js).
Before making an actual “gaming” part of the project, I made the game interface of the game first. For this project, I used Press Start 2P font downloaded from https://fonts.google.com/specimen/Press+Start+2P.
1. sketch.js
<2022/10/3> UPDATE 1
This file manages how different pages will be displayed based on the user input. Depending on the user input, the game will show the main page, game page, instruction page, interrupt page, or game.
function draw() {
background(0);
if (!escapePressed) {
//if user did not press ESC
if (currentPageIndex == 0) {
//MAIN PAGE
displayMain();
if (mySelectorMain != null) {
//draw selector object
mySelectorMain.drawSelector();
} else {
//if selector is new, create new selector object
mySelectorMain = new selector(150, 340, 21);
}
} else if (currentPageIndex == 1) {
//START GAME selected
startGame(); //start game based on Game.js
} else if (currentPageIndex == 2) {
//HOW TO PLAY selected
displayInstructions(); //display instructions
}
} else {
if (currentPageIndex == 1) {
//when user is in the middle of the game
displayReturn("EXIT GAME AND RETURN TO MAIN?");
if (mySelectorESC != null) {
//draw selector object
mySelectorESC.drawSelector();
} else {
//if selector is new, create new selector object
mySelectorESC = new selector(230, 310, 21);
}
} else if (currentPageIndex == 2) {
//when user is reading instructions
displayReturn("QUIT READING AND RETURN TO MAIN?");
if (mySelectorESC != null) {
//draw selector object
mySelectorESC.drawSelector();
} else {
//if selector is new, create new selector object
mySelectorESC = new selector(230, 310, 21);
}
}
}
}
Code above will display appropriate pages based on the current index of the game and ESC input(0: MAIN PAGE, 1: GAME, 2: INSTRUCTION). If ESC is pressed, the game will display RETURN TO HOME PAGE with appropriate texts.
//When keyboard input is given
function keyPressed() {
if (keyCode == DOWN_ARROW) {
if (currentPageIndex == 0) {
/*Main page. Move selector accordingly. Selector will move
to HOW TO PLAY when it was previously at START GAME, and vice versa*/
if (mySelectorMain.y == 340) mySelectorMain.y = 400;
else mySelectorMain.y = 340;
} else if (
(currentPageIndex == 1 || currentPageIndex == 2) &&
escapePressed
) {
/*if ESC is pressed in the middle of the game or in the instruction,
move selector accordingly.Selector will move to NO when it was previously
at YES, and vice versa*/
if (mySelectorESC.y == 310) mySelectorESC.y = 370;
else mySelectorESC.y = 310;
}
}
if (keyCode == UP_ARROW) {
if (currentPageIndex == 0) {
/*Main page. Move selector accordingly. Selector will move
to HOW TO PLAY when it was previously at START GAME, and vice versa*/
if (mySelectorMain.y == 340) mySelectorMain.y = 400;
else mySelectorMain.y = 340;
} else if (
(currentPageIndex == 1 || currentPageIndex == 2) &&
escapePressed
) {
/*if ESC is pressed in the middle of the game or in the instruction,
move selector accordingly.Selector will move to NO when it was previously
at YES, and vice versa*/
if (mySelectorESC.y == 310) mySelectorESC.y = 370;
else mySelectorESC.y = 310;
}
}
if (keyCode == ENTER) {
if (currentPageIndex == 0) {
//user selects START GAME. Change currentPageIndex to 1
if (mySelectorMain.y == 340) currentPageIndex = 1;
//user selects HOW TO PLAY. Change currentPageIndex to 2
else currentPageIndex = 2;
} else if (
(currentPageIndex == 1 || currentPageIndex == 2) &&
escapePressed
) {
/*ESC is pressed in the middle of the game or in the instruction.
If user selects YES, change currentPageIndex to 0 (returning to MAIN PAGE);
if user selects NO, simply return to the current page*/
if (mySelectorESC.y == 310) {
//YES
currentPageIndex = 0;
escapePressed = false;
} else {
//NO
//user selects NO
escapePressed = false;
}
}
//Reset selector objects' position (DEFAULT: First option)
mySelectorMain = null;
mySelectorESC = null;
}
//If ESC is pressed, change escapePressed to true;
if (keyCode == ESCAPE) {
if (currentPageIndex != 0) escapePressed = true;
}
}
Code above deals with user inputs and how the game will react to each input. For this version of the game, user has an option to select different options using up/down arrows and enter. Variables with name including “selector” are objects of triangles that is shown next to the options. It will be further explained in PAGE.JS section.
2. Page.js
<2022/10/3> UPDATE 1
This file manages how each page will visually displayed to the user. It is where all positions and alignments of window/texts are shown. The page starts with preload() to load fonts and images that will be used through out the game.
function preload() {
font = loadFont("assets/8Bit.ttf"); //load textfont to p5js
blockImg = loadImage("assets/block.png"); //load single block unit image to p5js
}
Most of the codes in this page manages positions and alignments of the visual elements for each page (MAIN, GAME, INSTRUCTION, and INTTERUPTION).
//MAIN Page
function displayMain() {
currentPageIndex = 0;
//display main title of the game: TETRIS
setFontStyle(titleFontSize);
text("TETRIS", width / 2, 250);
//display options
setFontStyle(normalFontSize);
text("START GAME", width / 2, 350);
text("HOW TO PLAY", width / 2, 410);
}
//Instruction Page
function displayInstructions() {
setFontStyle(titleFontSize - 40);
text("HOW TO PLAY", width / 2, 95);
createWindow(width / 2, 320, 530, 370);
setFontStyle(normalFontSize);
text(
"↑ : ROTATE BLOCKS\n\n\n← : MOVE BLOCKS LEFT\n\n\n→ : MOVE BLOCKS RIGHT\n\n\n↓ : SOFT DROP\n\n\nSPACE : HARD DROP",
width / 2,
180
);
setFontStyle(normalFontSize - 7);
if (millis() - currentMillis > 800) {
currentMillis = millis();
blink = !blink;
}
if (blink) {
fill(0);
stroke(0);
} else {
fill(255);
stroke(255);
}
text("PRESS ESC TO RETURN", width / 2, 560);
}
//Game
function displayGameBackground(score, level, lines) {
createWindow(width / 4 + 25, height / 2, width / 2 + 6, height - 60 + 6);
setFontStyle(normalFontSize);
text("——— NEXT ———", 462.5, 50);
//Next blocks
createWindow(400, 125, 106, 106);
createWindow(525, 125, 106, 106);
//Score, level, line cleared
createWindow(462.5, 385, 231, 376);
setFontStyle(normalFontSize);
text("SCORE", 462.5, 245);
text(score, 462.5, 290); //display score
text("LEVEL", 462.5, 358.3);
text(level, 462.5, 403.3); //display level of the round
text("LINES", 462.5, 481.7);
text(lines, 462.5, 526.7); //display total # of lines cleared in the round
}
//Return to Main Question Page
function displayReturn(_text) {
//Create window
createWindow(width / 2, height / 2, 400, 300);
setFontStyle(normalFontSize);
//Text Question
text(_text, width / 2, 220, 350);
//Options
text("YES", width / 2, 320);
text("NO", width / 2, 380);
}
//default setting of the font
function setFontStyle(size) {
strokeWeight(1);
textFont(font); //set font style
fill(255);
textWrap(WORD); //wrap text
textAlign(CENTER); //align text to center
textSize(size); //change text size
}
//create a rectangular window where texts will be displayed
function createWindow(x, y, w, h) {
fill(0);
stroke(255); //white border
strokeWeight(6); //border weight of the rectangle
rectMode(CENTER); //rectangle is centered
rect(x, y, w, h);
}
setFontStlye(size) and createWindow(x,y,w,h) are functions, where size is textSize, x and y are coordinates of the rectangle’s/window’s center, and w and h are width and height of the window.
3. Game.JS
<2022/10/3> UPDATE 1
This file contains all the game logics and runs Tetris game. The game consists of 2 main parts: block objects and game field array. Block objects are used to display blocks in motion. Game field array is what the program use to visualize the game. Once blocks cannot further be moved, it will be stored into game field array, so no further calculations will be needed in the future.
Game logic follow procedures listed below:
- Create an empty game field array. (0: empty space, 1: something is present)
board = [];
for (let i = 0; i < boardHeight + 1; i++) {
board[i] = [];
for (let j = 0; j < boardWidth; j++) {
board[i][j] = 0;
}
}
- User selects START GAME. Game Starts based on SCRIPT.JS
else if (currentPageIndex == 1) {
//START GAME selected
startGame();
}
- Load background layout of the game (shapes and texts) based on PAGE.JS
function startGame() {
displayGameBackground(0, 1, 0, 1);
//Code is partially shown for demonstration
}
- Generate, display, and move block object. Types of the block is randomly generated from the array that contains 7 different block types. Up until now, block will move only vertically down every time interval. When the block reaches bottom, or cannot be further moved, a new random block will be generated.
let blockTypeArr = ["I", "J", "L", "O", "S", "T", "Z"]; //Block Type Array
//GAME
function startGame() {
displayGameBackground(0, 1, 0, 1); //display background interface
if (generateBlock) {
//If new block is needed, create a random shaped block
myBlock = new block(int(random(0, 11)), 10, random(blockTypeArr), 4);
generateBlock = false;
}
myBlock.drawBlock();
myBlock.blockDown();
displayBoard();
}
Most of the game logic is handled by block class below. For more, read the commented lines.
//block object for tetris blocks
class block {
constructor(_x, _y, _type, speed) {
this.x = _x;
this.y = _y;
this.type = _type;
this.shapeArr = [];
this.speed = 1;
}
/*this class method will fill shapeArr based on the
type of the block. shapeArr is a 4*4 array, in which
0: empty, 1: block*/
shapeType(_type) {
let shapeArr = [];
/*example L block
0 0 0 0
0 1 0 0
0 1 0 0
0 1 1 0*/
for (let i = 0; i < 4; i++) {
shapeArr[i] = [0, 0, 0, 0];
}
if (_type == "I") {
shapeArr[0][0] = 1;
shapeArr[1][0] = 1;
shapeArr[2][0] = 1;
shapeArr[3][0] = 1;
} else if (_type == "J") {
shapeArr[1][2] = 1;
shapeArr[2][2] = 1;
shapeArr[3][1] = 1;
shapeArr[3][2] = 1;
} else if (_type == "L") {
shapeArr[1][1] = 1;
shapeArr[2][1] = 1;
shapeArr[3][1] = 1;
shapeArr[3][2] = 1;
} else if (_type == "O") {
shapeArr[2][1] = 1;
shapeArr[2][2] = 1;
shapeArr[3][1] = 1;
shapeArr[3][2] = 1;
} else if (_type == "S") {
shapeArr[2][1] = 1;
shapeArr[2][2] = 1;
shapeArr[3][0] = 1;
shapeArr[3][1] = 1;
} else if (_type == "T") {
shapeArr[2][1] = 1;
shapeArr[3][0] = 1;
shapeArr[3][1] = 1;
shapeArr[3][2] = 1;
} else if (_type == "Z") {
shapeArr[2][1] = 1;
shapeArr[2][2] = 1;
shapeArr[3][2] = 1;
shapeArr[3][3] = 1;
}
this.shapeArr = shapeArr;
}
//Checks if block can be moved in a given direction
validMove(dX, dY) {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if ( /*ignores all empty spaces. 1s must always be within the boundary of game field and must not overlap with non-empty, or 1s, when moved*/ this.shapeArr[i][j] == 1 && (board[this.y + i + 1][this.x + j] != 0 || this.y + i + dY >= boardHeight ||
this.x + j + dX < 0 || this.x + j + dX >= boardWidth)
) {
return false;
}
}
}
return true;
}
//Move block down for every time interval
blockDown() {
if (this.validMove(0, 1)) {
if (millis() - counter > 400 / this.speed) {
this.y++;
counter = millis();
}
} else {
//Once block cannot be moved down further, update game field
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.shapeArr[i][j] == 1) board[this.y + i][this.x + j] = 1;
}
}
//generate new block
generateBlock = true;
}
}
//Move Block left or right. left when direction is -1, right when direction is 1
blockLeftRight(dX) {
if (direction == -1) {
}
}
//draw blocks. for each 1s in the shapeArr, block image will be placed
drawBlock() {
this.shapeType(this.type);
blockImg = blockImg.get(0, 0, 20, 20);
push();
translate(25, 30);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.shapeArr[i][j] == 1)
image(blockImg, blockSize * (this.x + j), blockSize * (this.y + i));
}
}
pop();
}
}
//visually display the gameboard.
function displayBoard() {
blockImg = blockImg.get(0, 0, 20, 20);
push();
translate(25, 30);
for (let i = 0; i < boardHeight; i++) {
for (let j = 0; j < boardWidth; j++) {
if (board[i][j] == 1) image(blockImg, blockSize * j, blockSize * i);
}
}
pop();
}
Future Improvements
There are tons of more things to implement in this game:
-
- Moving blocks horizontally using key inputs.
- Rotating blocks using key inputs.
- This means more logic must be added to the movement validation method.
- Add conditions that will check if game is over or not.
- Add conditions that will check if user had made a horizontal line using blocks. Then, the program must be able to remove that line and shift whole game filed down by the number of removed lines unit(s).
- The game will not reset even if user returns to the main page in the middle of the game. This can be fixed by resetting every variable at the start of each game.