Vanilla JavaScript Columns, because everybody should build a Columns game
Talking about Columns game, Game development and Javascript.
Did you like the vanilla JavaScript Tetris example? Now it’s time to build a Columns game, using vanilla JavaScript as usual, because if everybody should be able to build a Tetris game, build a Columns game is even easier!
Columns is a match-3 puzzle game where colored gems fall in vertical stacks of three.
Players can cycle the order of the gems and must align three or more of the same color horizontally, vertically, or diagonally to clear them.
New gems continuously fall, and the game ends when the stack reaches the top.
Use ARROW KEYS to move, rotate or shift a column blocks. You can also play fromĀ this page.
And just like Tetris, this is the source code, all in one page:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Colmuns</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #222;
margin: 0;
}
canvas {
background-color: black;
border: 2px solid white;
}
</style>
</head>
<body>
<canvas id="thegame"></canvas>
<script>
// get canvas
const canvas = document.getElementById('thegame');
const context = canvas.getContext('2d');
// global constants
const boardRows = 13; // board rows
const boardColumns = 7; // board column
const tileSize = 32; // tile size, in pixels
const columnHeight = 3 // column height
const board = []; // game board
let dropInterval = 500; // drop interval, in milliseconds
// size and scale canvas according to tile and board size
canvas.width = boardColumns * tileSize;
canvas.height = boardRows * tileSize;
context.scale(tileSize, tileSize);
// array with all possible block colors
const blockColors = ['#00FFFF', '#FFFF00', '#800080', '#00A000', '#A00000', '#0000A0', '#F0A000']
// get a random column
let randomColumn = getRandomColumn();
// column starts two tiles outside the playable area
let columnPosition = {
row : -2,
column : 3
};
// drop timer
let dropTimer = 0;
// time since previous frame
let lastTime = 0;
// initialize the board
initializeBoard();
// call update method
update();
// function to initialize the board, setting all values to zero
function initializeBoard() {
for (let i = 0; i < boardRows; i ++) {
board[i] = [];
for (let j = 0; j < boardColumns; j ++) {
board[i][j] = 0;
}
}
}
// game loop
// time: amount of milliseconds passed since script beginning
function update(time = 0) {
// elapsed time since previous time
const deltaTime = time - lastTime;
lastTime = time;
dropTimer += deltaTime;
// if drop timer is bigger than drop interval, it's time to make the column drop
if (dropTimer > dropInterval) {
// reset drop timer
dropTimer = 0;
// move column down
columnPosition.row ++;
// is column colliding?
if (isColumnColliding()) {
// move column up
columnPosition.row --;
// merge columns
mergeColumn();
// clear lines if needed
clearBlocks();
// place a new column
randomColumn = getRandomColumn();
columnPosition = {
row : -2,
column : 3
};
// is newborn column already colliding?
if (isColumnColliding()) {
// GAME OVER!! (just clear the board in this case)
initializeBoard();
}
}
}
// draw the board
drawBoard();
// draw the falling column
drawColumn();
// request new frame and call update function (this one)
requestAnimationFrame(update);
}
// function to get a random column
// it's just an array with three random blocks
function getRandomColumn() {
const column = [];
for (let i = 0; i < columnHeight; i ++) {
column.push(Math.floor(Math.random() * blockColors.length));
}
return column;
}
// function to shift a column
function shiftColumn() {
const head = randomColumn[0];
for (let i = 1; i < columnHeight; i ++) {
randomColumn[i - 1] = randomColumn[i];
}
randomColumn[columnHeight - 1] = head;
}
// functions to merge a column with game board
function mergeColumn() {
for (let i = 0; i < columnHeight; i ++) {
if (columnPosition.row + i >= 0) {
board[columnPosition.row + i][columnPosition.column] = randomColumn[i] + 1;
}
}
}
// function to check if a column is colliding
function isColumnColliding() {
for (let i = 0; i < columnHeight; i ++) {
const row = columnPosition.row + i;
if (row >= 0 && (columnPosition.column < 0 || row >= boardRows || columnPosition.column >= boardColumns || board[row][columnPosition.column] != 0)) {
return true;
}
}
return false;
}
// function to clear blocks and make above blocks fall down
function clearBlocks() {
let itemsToRemove = [];
for (let i = 0; i < boardRows; i ++) {
for (let j = 0; j < boardColumns; j ++) {
if (board[i][j] != 0) {
let value = board[i][j];
// vertical
let combo = 1;
while (j + combo < boardColumns && board[i][j + combo] == value) {
combo ++;
}
if (combo >= 3) {
for (let k = 0; k < combo; k ++) {
itemsToRemove.push({
row : i,
column : j + k
});
}
}
// horizontal
combo = 1;
while (i + combo < boardRows && board[i + combo][j] == value) {
combo ++;
}
if (combo >= 3) {
for (let k = 0; k < combo; k ++) {
itemsToRemove.push({
row : i + k,
column : j
});
}
}
// diagonal 1
combo = 1;
while (i + combo < boardRows && j + combo < boardColumns && board[i + combo][j + combo] == value) {
combo ++;
}
if (combo >= 3) {
for (let k = 0; k < combo; k ++) {
itemsToRemove.push({
row : i + k,
column : j + k
});
}
}
// diagonal 2
combo = 1;
while (i - combo > 0 && j + combo < boardColumns && board[i - combo][j + combo] == value) {
combo ++;
}
if (combo >= 3) {
for (let k = 0; k < combo; k ++) {
itemsToRemove.push({
row : i - k,
column : j + k
});
}
}
}
}
}
// do we have items to remove?
if (itemsToRemove.length > 0) {
// remove them
for (let i = 0; i < itemsToRemove.length; i ++) {
board[itemsToRemove[i].row][itemsToRemove[i].column] = 0;
}
// make blocks fall down
let checkFallingBlocks = true;
while (checkFallingBlocks) {
checkFallingBlocks = false;
for (let i = boardRows - 1; i > 0; i --) {
for (let j = 0; j < boardColumns; j ++) {
if (board[i][j] == 0 && board[i - 1][j] != 0) {
board[i][j] = board[i - 1][j];
board[i - 1][j] = 0;
checkFallingBlocks = true;
}
}
}
}
clearBlocks();
}
}
// function to draw the board
function drawBoard() {
for (let i = 0; i < boardRows; i ++) {
for (let j = 0; j < boardColumns; j ++) {
if (board[i][j] != 0) {
context.fillStyle = blockColors[board[i][j] - 1];
context.fillRect(j, i, 1, 1);
}
else {
context.fillStyle = (i * boardColumns + j) % 2 == 0 ? '#000' : '#111';
context.fillRect(j, i, 1, 1);
}
}
}
}
// function to draw the column
function drawColumn() {
for (let i = 0; i < columnHeight; i ++) {
context.fillStyle = blockColors[randomColumn[i]];
context.fillRect(columnPosition.column, columnPosition.row + i, 1, 1);
}
}
// keydown listener
document.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowLeft' :
columnPosition.column --;
if (isColumnColliding()) {
columnPosition.column ++;
}
break;
case 'ArrowRight' :
columnPosition.column ++;
if (isColumnColliding()) {
columnPosition.column --;
}
break;
case 'ArrowUp' :
shiftColumn();
break;
case 'ArrowDown' :
dropInterval = 50;
break;
}
})
// keyup listener
document.addEventListener('keyup', (event) => {
if (event.key == 'ArrowDown') {
dropInterval = 500;
}
});
</script>
</body>
</html>
Copy/paste in your projects and have fun. This would also be interesting to build using CSS rather than canvas.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.