function HSVtoRGB(h, s, v) { var r, g, b, i, f, p, q, t; if (arguments.length === 1) { (s = h.s), (v = h.v), (h = h.h); } i = Math.floor(h * 6); f = h * 6 - i; p = v * (1 - s); q = v * (1 - f * s); t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: (r = v), (g = t), (b = p); break; case 1: (r = q), (g = v), (b = p); break; case 2: (r = p), (g = v), (b = t); break; case 3: (r = p), (g = q), (b = v); break; case 4: (r = t), (g = p), (b = v); break; case 5: (r = v), (g = p), (b = q); break; } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255), }; } var grid = []; var scl = 4; var padding = 4; var columns = 80; var rows = 160; var buff; var t = 0; var playerBlock; var nextBlock; var vis; var fullLine; var cleartime = 0; var placed = false; var staticCount = 0; var linesCleared = 0; var score = 0; var gameOffset = 4 * scl; var nextOffset; var gameRes; var placeSound; var lineSound; var gameMusic; var pixelFont; var gameOver = true; var paused = true; var startScreen; var pauseScreen; var aboutScreen; var gameoverScreen; var gameoverText; var timeText = "00:00"; var levelSlider; var levelText; var difficulty = 1; var sfxSlider1; var sfxSlider2; var musSlider1; var musSlider2; //difficulty vars var speed = 0.5; var staticChance = 8; var dupChance = 0.5; var brick = [ [0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 0], [0, 1, 0, 0, 0, 0, 1, 0], [0, 1, 0, 2, 2, 0, 1, 0], [0, 1, 0, 2, 2, 0, 1, 0], [0, 1, 0, 0, 0, 0, 1, 0], [0, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0], ]; var staticbrick = [ [2, 0, 0, 0, 0, 0, 0, 2], [0, 1, 2, 1, 1, 2, 1, 0], [0, 2, 1, 1, 1, 1, 2, 0], [0, 1, 1, 0, 0, 1, 1, 0], [0, 1, 1, 0, 0, 1, 1, 0], [0, 2, 1, 1, 1, 1, 2, 0], [0, 1, 2, 1, 1, 2, 1, 0], [0, 0, 0, 0, 0, 0, 0, 2], ]; var cols = [ [255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0], [255, 255, 255], ]; var blockType = [ [ [0, 0, 0, 1, 1, 0, 1, 1], // O rotations [0, 0, 0, 1, 1, 0, 1, 1], [0, 0, 0, 1, 1, 0, 1, 1], [0, 0, 0, 1, 1, 0, 1, 1], ], [ [0, 0, 0, 1, 1, 0, 0, 2], // L rotations [0, 0, 1, 0, 2, 0, 2, 1], [0, 2, 1, 2, 1, 1, 1, 0], [0, 0, 0, 1, 1, 1, 2, 1], ], [ [0, 0, 1, 0, 1, 1, 1, 2], // J rotations [0, 1, 1, 1, 2, 1, 2, 0], [0, 0, 0, 1, 0, 2, 1, 2], [0, 0, 0, 1, 1, 0, 2, 0], ], [ [0, 0, 1, 0, 1, 1, 2, 1], // S rotations [0, 1, 0, 2, 1, 1, 1, 0], [0, 0, 1, 0, 1, 1, 2, 1], [0, 1, 0, 2, 1, 1, 1, 0], ], [ [0, 1, 1, 1, 1, 0, 2, 0], // Z rotations [0, 0, 0, 1, 1, 1, 1, 2], [0, 1, 1, 1, 1, 0, 2, 0], [0, 0, 0, 1, 1, 1, 1, 2], ], [ [0, 0, 1, 0, 2, 0, 1, 1], // T rotations [0, 1, 1, 0, 1, 1, 1, 2], [1, 0, 0, 1, 1, 1, 2, 1], [0, 0, 0, 1, 0, 2, 1, 1], ], [ [0, 0, 1, 0, 2, 0, 3, 0], // I rotations [0, 0, 0, 1, 0, 2, 0, 3], [0, 0, 1, 0, 2, 0, 3, 0], [0, 0, 0, 1, 0, 2, 0, 3], ], ]; var blockWidth = [ [1, 1, 1, 1], [1, 2, 1, 2], [1, 2, 1, 2], [2, 1, 2, 1], [2, 1, 2, 1], [2, 1, 2, 1], [3, 0, 3, 0], ]; var blockHeight = [ [1, 1, 1, 1], [2, 1, 2, 1], [2, 1, 2, 1], [1, 2, 1, 2], [1, 2, 1, 2], [1, 2, 1, 2], [0, 3, 0, 3], ]; function preload() { soundFormats("mp3", "ogg"); placeSound = loadSound("sounds/place"); lineSound = loadSound("sounds/line"); gameMusic = loadSound("sounds/music"); pixelFont = loadFont("fonts/retroFont.ttf"); } //block object function Block(x, y) { this.pos = createVector(0, 0); this.grav = speed; this.sprite = null; this.grid = []; this.type = 0; this.col = 0; this.static = false; this.rot = 0; this.rotReset = true; this.clearGrid = function () { this.grid = []; for (let i = 0; i < 32; i++) { this.grid.push(new Array(32).fill(null)); } }; this.renderBlock = function () { this.clearGrid(); AddBlock( this.grid, 0, 31, blockType[this.type][this.rot], this.col, this.static ); renderFromArray(this.grid, this.sprite); }; this.newBlock = function () { this.static = false; this.sprite = createImage(32, 32); this.type = int(random(blockType.length)); this.col = int(random(4)); this.pos = createVector( int(columns / 2 - (blockWidth[this.type][0] + 1)), 0 ); staticCount += 1; if (staticCount == staticChance) { this.static = true; staticCount = 0; } this.renderBlock(); }; this.show = function () { image( this.sprite, this.pos.x * scl + gameOffset, (this.pos.y - 32) * scl, 32 * scl, 32 * scl ); }; this.update = function () { let gridx = Math.floor(this.pos.x); let gridy = Math.floor(this.pos.y); //check if block hit the ground if (gridy + 1 >= rows) { placed = true; } else { //check if sand under any block for (let i = 0; i < 4; i++) { let index = i * 2; let offx = blockType[this.type][this.rot][index]; let offy = blockType[this.type][this.rot][index + 1]; let brickx = int(gridx + offx * 8); let bricky = int(gridy - offy * 8); if (bricky <= 0) { continue; } for (let j = 0; j < 8; j++) { if (grid[bricky + 1][brickx + j] != null) { if (grid[bricky][brickx + j]) { this.pos.y -= 1; } placed = true; } } } } if (placed) { if (this.pos.y - 8 * (blockHeight[this.type][this.rot] + 1) < 0) { gameOver = true; gameOverScore(); gameoverScreen.open = true; } AddBlock( grid, gridx, min(gridy, rows - 1), blockType[this.type][this.rot], this.col, this.static ); placeSound.play(); return; } this.pos.y += this.grav; }; this.rotate = function () { this.rot = (this.rot + 1) % 4; this.clearGrid(); this.sprite = createImage(32, 32); AddBlock( this.grid, 0, 31, blockType[this.type][this.rot], this.col, this.static ); renderFromArray(this.grid, this.sprite); let limit = blockWidth[this.type][this.rot] + 1; if (this.pos.x > columns - limit * 8) { this.pos.x = columns - limit * 8; } }; this.controls = function () { if (keyIsDown(UP_ARROW)) { if (this.rotReset) { this.rotate(); this.rotReset = false; } } else { this.rotReset = true; } if (keyIsDown(LEFT_ARROW)) { this.pos.x -= 1; if (this.pos.x < 0) { this.pos.x = 0; } } if (keyIsDown(RIGHT_ARROW)) { this.pos.x += 1; let limit = blockWidth[this.type][this.rot] + 1; if (this.pos.x > columns - limit * 8) { this.pos.x = columns - limit * 8; } } if (keyIsDown(DOWN_ARROW)) { this.pos.y += 1; score += 1; } }; } function resetGame() { //board has 10x20 blocks //and 80x160 grains score = 0; linesCleared = 0; staticCount = 0; t = 0; buff = createImage(columns, rows); grid = []; for (let y = 0; y < rows; y++) { grid[y] = []; for (let x = 0; x < columns; x++) { grid[y].push(null); } } playerBlock = new Block(width / 2 - gameOffset, 0); playerBlock.newBlock(); nextBlock = new Block(width / 2 - gameOffset, 0); nextBlock.newBlock(); } function startGame() { resetGame(); paused = false; gameOver = false; startScreen.open = false; } function unpauseGame() { paused = false; pauseScreen.open = false; } function newGame() { pauseScreen.open = false; gameoverScreen.open = false; startScreen.open = true; } function SFXvolume(val) { let soundlevel = val / 10; placeSound.setVolume(soundlevel / 2); lineSound.setVolume(soundlevel / 2); sfxSlider1.value = val; sfxSlider2.value = val; } function MUSvolume(val) { let soundlevel = val / 10; gameMusic.setVolume(soundlevel / 2); musSlider1.value = val; musSlider2.value = val; } function toggleAbout() { aboutScreen.open = !aboutScreen.open; startScreen.open = !startScreen.open; } function adjustDifficulty() { difficulty = levelSlider.value; speed = 0.5 + map(difficulty, 1, 10, 0, 3) / 2; staticChance = Math.floor(map(difficulty, 1, 10, 16, 4)); dupChance = map(difficulty, 0, 1, 1, 0.1); } function gameOverScore() { gameoverText.innerHTML = ""; gameoverText.innerHTML += "SCORE:
" + score; gameoverText.innerHTML += "
LINES:
" + linesCleared; } function shareText() { let scoreInfoText = `█▀ ▄▀█ █▄░█ █▀▄ ▀█▀ █▀█ █ █▀ ▄█ █▀█ █░▀█ █▄▀ ░█░ █▀▄ █ ▄█ `; let levelText = difficulty.toString(); let linesText = linesCleared.toString(); let scoreText = score.toString(); scoreInfoText += "LEVEL: " + levelText + " ".repeat(6 - levelText.length) + "| "; scoreInfoText += "LINES: " + linesText + " ".repeat(6 - linesText.length) + "\n"; scoreInfoText += "SCORE: " + scoreText + " ".repeat(9 - scoreText.length) + "| "; scoreInfoText += "TIME: " + timeText + " ".repeat(7 - timeText.length) + "\n"; scoreInfoText += "Play now at https://sandtris.com/"; scoreInfoText += navigator.clipboard.writeText(scoreInfoText); alert("Share Text Copied to Clipboard!"); } function setup() { //dom elements startScreen = document.getElementById("startpage"); pauseScreen = document.getElementById("pausepage"); gameoverScreen = document.getElementById("gameoverpage"); aboutScreen = document.getElementById("aboutpage"); levelSlider = document.getElementById("lvlSlider"); levelSlider.value = 1; levelText = document.getElementById("levelText"); sfxSlider1 = document.getElementById("sfx1Slider"); sfxSlider2 = document.getElementById("sfx2Slider"); sfxSlider1.value = 10; sfxSlider2.value = 10; musSlider1 = document.getElementById("mus1Slider"); musSlider2 = document.getElementById("mus2Slider"); musSlider1.value = 10; musSlider2.value = 10; gameoverText = document.getElementById("gameoverText"); gameRes = createVector(columns * scl, rows * scl); nextOffset = gameRes.x + gameOffset * 4; cnv = createCanvas(gameRes.x + gameOffset * 17, gameRes.y); cnv.parent("cnv"); textFont(pixelFont); frameRate(60); noSmooth(); gameMusic.play(); gameMusic.setVolume(0.5); gameMusic.loop(); resetGame(); } //adds 4 bricks to array in block form function AddBlock(target, x, y, type, c, static) { for (let i = 0; i < 4; i++) { AddSingleBrick( target, x + type[i * 2] * 8, y - type[i * 2 + 1] * 8, c, static ); } } //adds a single brick to array function AddSingleBrick(target, x, y, c, static) { let template = brick; if (static) { template = staticbrick; } for (let i = 0; i < 8; i++) { for (let j = 0; j < 8; j++) { if (y - i < 0) { continue; } let col = HSVtoRGB(c / 5, 0.8, map(template[i][j], 0, 1, 0.2, 0.7)); //[Block Group, r,g,b, visited, STATIC] target[y - i][x + j] = [c, col.r, col.g, col.b, 0, static]; } } } //renders image from given array function renderFromArray(a, target) { let rows = a.length; let columns = a[0].length; target.loadPixels(); for (let y = 0; y < rows; y++) { for (let x = 0; x < columns; x++) { let index = (y * columns + x) * 4; if (a[y][x] == null) { target.pixels[index] = 0; target.pixels[index + 1] = 0; target.pixels[index + 2] = 0; target.pixels[index + 3] = 0; continue; } target.pixels[index] = a[y][x][1]; target.pixels[index + 1] = a[y][x][2]; target.pixels[index + 2] = a[y][x][3]; target.pixels[index + 3] = 255; } } target.updatePixels(); } //resets and sand automata logic function updateLogic(x, y) { if (grid[y][x] == null) { return; } //reset visited logic grid[y][x][4] = 0; //sand automata rules if (y >= rows - 1) { return; } //bottom empty if (grid[y + 1][x] == null) { grid[y + 1][x] = grid[y][x]; grid[y][x] = null; return; } //check if static block if (grid[y][x][5]) { return; } //bottom corners let bl = x > 0 && grid[y + 1][x - 1] == null; let br = x < columns - 1 && grid[y + 1][x + 1] == null; if (bl && br) { if (random() < 0.5) { grid[y + 1][x - 1] = grid[y][x]; grid[y][x] = null; return; } grid[y + 1][x + 1] = grid[y][x]; grid[y][x] = null; return; } if (bl) { grid[y + 1][x - 1] = grid[y][x]; grid[y][x] = null; return; } if (br) { grid[y + 1][x + 1] = grid[y][x]; grid[y][x] = null; return; } } //alternates update loop l-r, r-l function updateGrid() { if (t % 4 == 0) { //left to right half the time for (let y = rows - 1; y >= 0; y--) { for (let x = 0; x < columns; x++) { updateLogic(x, y); } } return; } if (t % 4 == 2) { //right to left half the time for (let y = rows - 1; y >= 0; y--) { for (let x = columns - 1; x >= 0; x--) { updateLogic(x, y); } } } } //checks if color goes from left to right function checkLine() { //visited array vis = []; for (let y = 0; y < rows; y++) { vis = []; fullLine = false; if (grid[y][0] == null || grid[y][0][4] == 1) { continue; } floodFill(0, y, grid[y][0][0]); if (!fullLine) { continue; } console.log("LINE AT ", y); //breaks here to store vis and fullLine return; } } //helper for checkLine using floodFill function floodFill(x, y, c) { if ( x < 0 || x >= columns || y < 0 || y >= rows || grid[y][x] == null || grid[y][x][4] == 1 || grid[y][x][0] != c ) { return; } if (x == columns - 1) { fullLine = true; } //mark visited grid[y][x][4] = 1; vis.push([x, y]); //recurse to neighbors floodFill(x + 1, y, c); floodFill(x - 1, y, c); floodFill(x, y + 1, c); floodFill(x, y - 1, c); } function setLineColor(t) { let col = 255; if (t % 10 < 5) { col = 0; } for (let p of vis) { grid[p[1]][p[0]][1] = col; grid[p[1]][p[0]][2] = col; grid[p[1]][p[0]][3] = col; } } function deleteLine(p) { for (let p of vis) { grid[p[1]][p[0]] = null; } score += vis.length; vis = []; } function UI() { //render renderFromArray(grid, buff); //display background(206, 174, 127); //game background fill(10); rect(gameOffset, 0, columns * scl, rows * scl); //game image image(buff, gameOffset, 0, columns * scl, rows * scl); //current block show if (!gameOver && !placed) { playerBlock.show(); } //next block background fill(10); rect(nextOffset, gameOffset * 2, gameOffset * 10, gameOffset * 10); //next block show image( nextBlock.sprite, nextOffset + (5 - (blockWidth[nextBlock.type][0] + 1)) * gameOffset, (5 - (6 - blockHeight[nextBlock.type][0]) + 1) * gameOffset, 32 * scl, 32 * scl ); //show text let minutes = Math.floor(t / 3600); let seconds = Math.floor(t / 60) % 60; if (minutes < 10) { minutes = "0" + minutes; } if (seconds < 10) { seconds = "0" + seconds; } timeText = minutes + ":" + seconds; fill(25).strokeWeight(1).textSize(32); text(timeText, nextOffset - 2, gameOffset * 16); text("LINES:", nextOffset - 2, gameOffset * 19); text(linesCleared, nextOffset - 2, gameOffset * 21); text("SCORE:", nextOffset - 2, gameOffset * 24); text(score, nextOffset - 2, gameOffset * 26); text("LEVEL:", nextOffset - 2, gameOffset * 29); text(difficulty, nextOffset - 2, gameOffset * 31); //DOM levelText.innerHTML = "LEVEL: " + levelSlider.value; } function keyPressed() { if (keyCode === 80) { if (gameOver) { return; } paused = !paused; pauseScreen.open = !pauseScreen.open; } } function GameLogic() { if (paused) { return; } if (gameOver) { return; } //check if line is made if (fullLine) { if (cleartime == 0) { linesCleared += 1; lineSound.play(); } cleartime += 1; setLineColor(cleartime); if (cleartime > 30) { console.log("Deleting"); deleteLine(); cleartime = 0; fullLine = false; } return; } if (placed) { playerBlock = nextBlock; nextBlock = new Block(width / 2, 0); nextBlock.newBlock(); if (playerBlock.col == nextBlock.col) { if (random() < dupChance) { nextBlock.col = (nextBlock.col + 1) % 4; nextBlock.renderBlock(); } } placed = false; } //game logic updateGrid(); playerBlock.update(); playerBlock.controls(); checkLine(); t += 1; } function draw() { //show game UI(); //run game logic GameLogic(); }