VCHS Computer Science
Computer Science at Valley Catholic High School
Pages
Home
APCSA
APCSP
Ten Commandments
Jump
Tetris Version 1
Source Code
<!-- CSS --> <style> #grid { background-color: black; position: relative; border-right: solid 1px black; border-bottom: solid 1px black; } .block { position: absolute; border: solid 1px silver; } </style> <!-- HTML --> <div id="grid" style="height: 400px; width: 250px;"></div> <!-- JavaScript --> <script> /** * JavaScript source code for an implementation of Tetris. * * Author: John P. Sprugeon * Version: 1 (3 April 2019) * * Dependencies: * * 1. Document contains an element with id "grid". * 2. CSS (Cascading Style Sheet): * a. Desired background color is specified for grid. * b. Elements of "block" class are positioned "absolute". * * Possible Enhancements * * 1. Score * 2. Restart * 3. High score * 4. Acceleration * 5. Pause * 6. Levels * 7. Piece preview * 8. Piece hold * 9. Configurable colors * 10. Configurable grid size * 11. Obstacles * 12. Float up (vs. drop down) * 13. Easter eggs (??) */ (function Tetris() { "use strict"; /***********/ /* GLOBALS */ /***********/ const UNITS = "px"; const StaticBlocks = []; let dynamicShape; let timerId; let gameIsOver = false; let gameIsStarted = false; /****************************/ /* OBJECTS AND CONSTRUCTORS */ /****************************/ /** * Constructs a block. * * e.g. new Block({bgColor: "red", left: 0, right: 0}) */ function Block(options) { // Initialize the new block. const element = createBlockElement(); let left = options.left; let top = options.top; updatePosition(); /* Private Functions */ function updateLeft() { element.style.left = (left * Block.unitsWide) + UNITS; } function updateTop() { element.style.top = (top * Block.unitsTall) + UNITS; } function updatePosition() { updateLeft(); updateTop(); } function createBlockElement() { const block = document.createElement("div"); block.classList.add("block"); block.style.height = Block.height; block.style.width = Block.width; block.style.backgroundColor = options.bgColor; return block; } function canMoveLeft() { return left > 0 && !isOffLimits({left: left - 1, top: top}); } function canMoveRight() { return left < Grid.blocksWide - 1 && !isOffLimits({left: left + 1, top: top}); } function canMoveDown() { return top < Grid.blocksTall - 1 && !isOffLimits({left: left, top: top + 1}); } function moveLeft() { --left; updateLeft(); } function moveRight() { ++left; updateLeft(); } function moveDown() { ++top; updateTop(); } /* Public Methods */ this.removeFrom = function(parentElement) { parentElement.removeChild(element); } this.addTo = function(parentElement) { parentElement.append(element); }; this.moveLeft = function() { if (canMoveLeft()) moveLeft(); }; this.moveRight = function() { if (canMoveRight()) moveRight(); }; this.moveDown = function() { if (canMoveDown()) moveDown(); }; this.shiftDown = function() { moveDown(); } this.canMoveRight = canMoveRight; this.canMoveLeft = canMoveLeft; this.canMoveDown = canMoveDown; this.getLeft = function() { return left; } this.getTop = function() { return top; } this.getPosition = function() { return {left: left, top: top}; } this.setLeft = function(value) { left = value; updateLeft(); } this.setTop = function(value) { top = value; updateTop(); } } Block.subdivisions = 25; Block.unitsWide = Block.subdivisions; Block.unitsTall = Block.subdivisions; Block.size = Block.subdivisions + UNITS; Block.width = Block.size; Block.height = Block.size; Object.freeze(Block); /** * The grid that holds the game pieces. */ const Grid = { blocksWide: 10, blocksTall: 16 }; Grid.width = (Grid.blocksWide * Block.unitsWide) + UNITS; Grid.height = (Grid.blocksTall * Block.unitsTall) + UNITS; Grid.element = (function() { const element = document.getElementById("grid"); element.style.height = Grid.height; element.style.width = Grid.width; return element; })(); Object.freeze(Grid); /** * Constructs a shape object composed of blocks. * The value of pIndex is the index of the block * around which the other blocks pivot when the * shape is rotated. If pIndex is undefined, the * shape does not rotate. */ function Shape(blocks, pIndex) { const numBlocks = blocks.length; const pivotBlock = (pIndex === undefined) ? null : blocks[pIndex]; blocks.forEach(function(block) { block.addTo(Grid.element); }); function canMoveRight() { if (gameIsOver) return false; for (let i = 0; i < numBlocks; ++i) { if (!blocks[i].canMoveRight()) return false; } return true; } function canMoveLeft() { if (gameIsOver) return false; for (let i = 0; i < numBlocks; ++i) { if (!blocks[i].canMoveLeft()) return false; } return true; } function canMoveDown() { if (gameIsOver) return false; for (let i = 0; i < numBlocks; ++i) { if (!blocks[i].canMoveDown()) return false; } return true; } function canPivot() { if (gameIsOver || pivotBlock === null) return false; const n = blocks.length; for (let i = 0; i < n; ++i) { const block = blocks[i]; const pos = getPivotedPosition(block); if (isOffLimits(pos)) return false; } return true; } function overlapsOtherShape() { const n = blocks.length; for (let i = 0; i < n; ++i) { const block = blocks[i]; const pos = block.getPosition(); if (isOffLimits(pos)) return true; } return false; } function moveDown() { for (let i = 0; i < numBlocks; ++i) blocks[i].moveDown(); } function moveLeft() { for (let i = 0; i < numBlocks; ++i) blocks[i].moveLeft(); } function moveRight() { for (let i = 0; i < numBlocks; ++i) blocks[i].moveRight(); } function freeze() { blocks.forEach(function(block) { StaticBlocks.push(block); }); StaticBlocks.sort(blockComparator); clearFullRows(); dynamicShape = getNextShape(); } function getPivotedPosition(block) { const B = [block.getTop(), block.getLeft()]; const P = [pivotBlock.getTop(), pivotBlock.getLeft()]; const Vr = [B[0] - P[0], B[1] - P[1]]; const Vt = [-1 * Vr[1], Vr[0]]; return {top: Vt[0] + P[0], left: Vt[1] + P[1]}; } function pivot(block) { // Credit: https://www.youtube.com/watch?v=Atlr5vvdchY if (block === pivotBlock || pivotBlock === null) return; const pos = getPivotedPosition(block); block.setTop(pos.top); block.setLeft(pos.left); } this.moveDown = function() { if (canMoveDown()) moveDown(); else freeze(); }; this.moveLeft = function() { if (canMoveLeft()) moveLeft(); }; this.moveRight = function() { if (canMoveRight()) moveRight(); }; this.overlapsOtherShape = overlapsOtherShape; this.canMoveRight = canMoveRight; this.canMoveLeft = canMoveLeft; this.canMoveDown = canMoveDown; this.freeze = freeze; this.pivot = function() { if (canPivot()) blocks.forEach(pivot); } } Shape.LColor = "orange"; Shape.JColor = "red"; Shape.SColor = "royalblue"; Shape.ZColor = "gold"; Shape.TColor = "green"; Shape.IColor = "hotpink"; Shape.OColor = "purple"; Object.freeze(Shape); /* Shape Subclasses */ function LShape() { Shape.call(this, getBlocks(), 1); function getBlocks() { return [ new Block({bgColor: Shape.LColor, left: 3, top: 1}), new Block({bgColor: Shape.LColor, left: 4, top: 1}), new Block({bgColor: Shape.LColor, left: 5, top: 1}), new Block({bgColor: Shape.LColor, left: 5, top: 0}) ]; } } function JShape() { Shape.call(this, getBlocks(), 1); function getBlocks() { return [ new Block({bgColor: Shape.JColor, left: 3, top: 0}), new Block({bgColor: Shape.JColor, left: 4, top: 0}), new Block({bgColor: Shape.JColor, left: 5, top: 0}), new Block({bgColor: Shape.JColor, left: 5, top: 1}) ]; } } function SShape() { Shape.call(this, getBlocks(), 2); function getBlocks() { return [ new Block({bgColor: Shape.SColor, left: 4, top: 0}), new Block({bgColor: Shape.SColor, left: 5, top: 0}), new Block({bgColor: Shape.SColor, left: 5, top: 1}), new Block({bgColor: Shape.SColor, left: 6, top: 1}) ]; } } function ZShape() { Shape.call(this, getBlocks(), 2); function getBlocks() { return [ new Block({bgColor: Shape.ZColor, left: 4, top: 1}), new Block({bgColor: Shape.ZColor, left: 5, top: 1}), new Block({bgColor: Shape.ZColor, left: 5, top: 0}), new Block({bgColor: Shape.ZColor, left: 6, top: 0}) ]; } } function TShape() { Shape.call(this, getBlocks(), 1); function getBlocks() { return [ new Block({bgColor: Shape.TColor, left: 3, top: 1}), new Block({bgColor: Shape.TColor, left: 4, top: 1}), new Block({bgColor: Shape.TColor, left: 5, top: 1}), new Block({bgColor: Shape.TColor, left: 4, top: 0}) ]; } } function OShape() { Shape.call(this, getBlocks(), undefined); function getBlocks() { return [ new Block({bgColor: Shape.OColor, left: 4, top: 0}), new Block({bgColor: Shape.OColor, left: 5, top: 0}), new Block({bgColor: Shape.OColor, left: 4, top: 1}), new Block({bgColor: Shape.OColor, left: 5, top: 1}) ]; } } function IShape() { Shape.call(this, getBlocks(), 2); function getBlocks() { return [ new Block({bgColor: Shape.IColor, left: 3, top: 0}), new Block({bgColor: Shape.IColor, left: 4, top: 0}), new Block({bgColor: Shape.IColor, left: 5, top: 0}), new Block({bgColor: Shape.IColor, left: 6, top: 0}) ]; } } /*************/ /* UTILITIES */ /*************/ function blockComparator(b1, b2) { const topDiff = b1.getTop() - b2.getTop(); return (topDiff === 0) ? b1.getLeft() - b2.getLeft() : topDiff; } function isOutOfBounds(pos) { if (pos.left < 0) return true; if (pos.left > Grid.blocksWide - 1) return true; if (pos.top < 0) return true; return false; } function isOccupied(pos) { const n = StaticBlocks.length; for (let i = 0; i < n; ++i) { const block = StaticBlocks[i]; if (block.getLeft() === pos.left && block.getTop() === pos.top) return true; } return false; } function isOffLimits(pos) { return isOutOfBounds(pos) || isOccupied(pos); } function onKeyDown(event) { const key = event.key; if (key === "ArrowLeft") { if (!gameIsOver) dynamicShape.moveLeft(); event.preventDefault(); } else if (key === "ArrowRight") { if (!gameIsOver) dynamicShape.moveRight(); event.preventDefault(); } else if (key === "ArrowDown") { if (!gameIsStarted) startGame(); else if (!gameIsOver) dynamicShape.moveDown(); event.preventDefault(); } else if (key === "ArrowUp") { if (!gameIsOver) dynamicShape.pivot(); event.preventDefault(); } } function clearRow(blocks) { const rowNum = blocks[0].getTop(); blocks.forEach(function(block) { const i = StaticBlocks.indexOf(block); StaticBlocks.splice(i, 1); block.removeFrom(Grid.element); }); StaticBlocks.forEach(function(block) { if (block.getTop() < rowNum) block.shiftDown(); }); } function clearFullRows() { const rows = []; let row; const n = StaticBlocks.length; let first = 0, count = 0, rowNum = -1, firstBlock; for (let i = 0; i < n; ++i) { const block = StaticBlocks[i]; if (block.getTop() !== rowNum) { rowNum = block.getTop(); firstBlock = block; row = [block]; } else { row.push(block); if (row.length === Grid.blocksWide) { rows.push(row); } } } while (rows.length > 0) clearRow(rows.pop()); } function getNextShape() { const fxns = [LShape, JShape, SShape, ZShape, TShape, OShape, IShape]; const index = Math.floor(Math.random() * fxns.length); const fxn = fxns[index]; const nextShape = new fxn(); if (nextShape.overlapsOtherShape()) { gameIsOver = true; clearInterval(timerId); } return nextShape; } function startGame() { gameIsStarted = true; dynamicShape.moveDown(); timerId = setInterval(function() { dynamicShape.moveDown(); }, 1000); } /*** MAIN ***/ window.addEventListener("keydown", onKeyDown); setTimeout(function() {dynamicShape = getNextShape();}, 500); })(); </script>
Newer Post
Older Post
Home