VCHS Computer Science
Computer Science at Valley Catholic High School
Pages
Home
APCSA
APCSP
Ten Commandments
Jump
Tetris Version 3
Ready?
Score:
0
Source Code
<!-- CSS --> <style> #game-environs { display: inline-block; } #grid-header { text-align: right; } #grid-footer { text-align: right; } #score-board { } #state { font-size: smaller; float: left; } #grid { position: relative; background-color: black; border-right-style: solid; border-right-color: black; border-bottom-style: solid; border-bottom-color: black; } #info { font-size: smaller; } .block { position: absolute; border-color: silver; } </style> <!-- HTML --> <div id="game-environs"> <div id="grid-header"> <div id="state">Ready?</div> <div id="score-board"><b>Score:</b> <span id="score">0</span></div> </div> <div id="grid" style="height: 400px; width: 200px;"></div> <!-- JavaScript --> <div id="grid-footer"> <div id="info">Press an arrow key to begin.</div> </div> </div> <script> /** * JavaScript source code for an implementation of Tetris. * * Author: John P. Spurgeon * Version: Latest (released 5 April 2019) * * Changes Introduced in Version 3. * * 1. Classic block colors. (See https://tetris.wiki/Tetromino.) * 2. Traditional 10 x 20 grid size. * 3. Improved appearance of block borders. * 4. Rearranged locations of and enhanced instruction and game status * messages. * 5. Tidied up the way the game ends. Now the last shape stacks on top of * another shape instead of overlapping. Any blocks (of the last shape) * that are outside the bounds of the grid are erased. * * Changes Introduced in Version 2. * * 1. Fixed bug discovered by Ishaan Bhutani. Previously, blocks could be * rotated below the bottom of grid. Updated function isOutOfBounds. * 2. Added scoring and a score board. Score is the total number of blocks * cleared as a result of filling rows with blocks. * 3. Added pause capability. User presses Escape to pause and any arrow key * to resume. * 4. Added ability to view version and author information. Press 'v' or 'V' * and 'a' or 'A' respectively. This is an Easter Egg feature; it is not * obvious to the user that the capability exists. * 5. Added area for displaying informational messages below the grid. * * Someday/maybe: * * a) Piece preview: User can see what piece is coming up next. * b) Piece hold: User can save a piece and use it later. * c) Wildcard piece: User can change the shape of the piece. * d) More points for clearing multiple rows at a time. * e) Ability to choose the colors of pieces. * f) Ability to configure the game. */ (function Tetris() { "use strict"; /***********/ /* GLOBALS */ /***********/ const AUTHOR = "by John P. Spurgeon"; const VERSION = "Version 3"; const UNITS = "px"; const StaticBlocks = []; let dynamicShape; let timerId; let gameIsOver = false; let gameIsStarted = false; let gameIsPaused = true; let score = 0; let displayScoreDelay; /****************************/ /* 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.borderStyle = "solid"; block.style.borderWidth = Block.borderWidth + UNITS; 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(); } function moveUp() { --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.shiftUp = function(n) { while(n-- > 0) moveUp(); }; this.shiftDown = function(n) { while(n-- > 0) moveDown(); }; this.shiftRight = function(n) { while(n-- > 0) moveRight(); }; 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.borderWidth = 1; Block.subdivisions = 20; Block.unitsWide = Block.subdivisions; Block.unitsTall = Block.subdivisions; Block.size = (Block.subdivisions - Block.borderWidth) + UNITS; Block.width = Block.size; Block.height = Block.size; Object.freeze(Block); /** * The grid that holds the game pieces. */ const Grid = { blocksWide: 10, blocksTall: 20 }; 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; element.style.borderRightSize = Block.borderWidth; element.style.borderBottomSize = Block.borderWidth; 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) { const n = Math.floor(Grid.blocksWide / 2) - 2; block.shiftRight(n); 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 (isOccupied(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) { // Credit: https://www.youtube.com/watch?v=Atlr5vvdchY 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) { 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.shiftUp = function(n) { blocks.forEach(function(block) { block.shiftUp(1); }); } this.removeOutOfBoundsPieces = function() { blocks.forEach(function(block) { if (isOutOfBounds(block.getPosition())) { block.removeFrom(Grid.element); } }); }; 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 = "darkorange"; Shape.JColor = "blue"; Shape.SColor = "green"; Shape.ZColor = "red"; Shape.TColor = "purple"; Shape.IColor = "deepskyblue"; Shape.OColor = "gold"; Object.freeze(Shape); /* Shape Subclasses */ function LShape() { Shape.call(this, getBlocks(), 1); function getBlocks() { return [ new Block({bgColor: Shape.LColor, left: 0, top: 1}), new Block({bgColor: Shape.LColor, left: 1, top: 1}), new Block({bgColor: Shape.LColor, left: 2, top: 1}), new Block({bgColor: Shape.LColor, left: 2, top: 0}) ]; } } function JShape() { Shape.call(this, getBlocks(), 1); function getBlocks() { return [ new Block({bgColor: Shape.JColor, left: 0, top: 0}), new Block({bgColor: Shape.JColor, left: 1, top: 0}), new Block({bgColor: Shape.JColor, left: 2, top: 0}), new Block({bgColor: Shape.JColor, left: 2, top: 1}) ]; } } function SShape() { Shape.call(this, getBlocks(), 2); function getBlocks() { return [ new Block({bgColor: Shape.SColor, left: 0, top: 0}), new Block({bgColor: Shape.SColor, left: 1, top: 0}), new Block({bgColor: Shape.SColor, left: 1, top: 1}), new Block({bgColor: Shape.SColor, left: 2, top: 1}) ]; } } function ZShape() { Shape.call(this, getBlocks(), 2); function getBlocks() { return [ new Block({bgColor: Shape.ZColor, left: 0, top: 1}), new Block({bgColor: Shape.ZColor, left: 1, top: 1}), new Block({bgColor: Shape.ZColor, left: 1, top: 0}), new Block({bgColor: Shape.ZColor, left: 2, top: 0}) ]; } } function TShape() { Shape.call(this, getBlocks(), 2); function getBlocks() { return [ new Block({bgColor: Shape.TColor, left: 1, top: 0}), new Block({bgColor: Shape.TColor, left: 0, top: 1}), new Block({bgColor: Shape.TColor, left: 1, top: 1}), new Block({bgColor: Shape.TColor, left: 2, top: 1}) ]; } } function OShape() { Shape.call(this, getBlocks(), undefined); function getBlocks() { return [ new Block({bgColor: Shape.OColor, left: 0, top: 0}), new Block({bgColor: Shape.OColor, left: 1, top: 0}), new Block({bgColor: Shape.OColor, left: 0, top: 1}), new Block({bgColor: Shape.OColor, left: 1, top: 1}) ]; } } function IShape() { Shape.call(this, getBlocks(), 2); function getBlocks() { return [ new Block({bgColor: Shape.IColor, left: 0, top: 0}), new Block({bgColor: Shape.IColor, left: 1, top: 0}), new Block({bgColor: Shape.IColor, left: 2, top: 0}), new Block({bgColor: Shape.IColor, left: 3, top: 0}) ]; } } /** * Used to display a user message. */ const Info = Object.freeze({ element: document.getElementById("info"), display: function(message) { Info.element.innerHTML = message; } }); const State = Object.freeze({ element: document.getElementById("state"), display: function(message) { State.element.innerHTML = message; } }); const ScoreBoard = Object.freeze({ delay: 50, element: document.getElementById("score"), setScore: function(value) { ScoreBoard.element.innerHTML = value; } }); const ScoreKeeper = Object.freeze({ incrementScore: function() { ScoreBoard.setScore(++score); } }); /*************/ /* UTILITIES */ /*************/ function blockComparator(b1, b2) { const topDiff = b1.getTop() - b2.getTop(); return (topDiff === 0) ? b1.getLeft() - b2.getLeft() : topDiff; } function isOutOfBounds(pos) { return (pos.left < 0) || (pos.left > Grid.blocksWide - 1) || (pos.top < 0) || (pos.top > Grid.blocksTall - 1); } 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 pauseGame() { gameIsPaused = true; State.display("Paused..."); Info.display("Press an arrow key to continue."); } function ensureGameIsNotPaused() { if (gameIsPaused) { gameIsPaused = false; State.display("Think fast!"); Info.display("Press the Escape key to pause."); } } function onKeyDown(event) { const key = event.key; if (key === "F5") { } else if (gameIsOver) { event.preventDefault(); } else if (key === "ArrowUp") { ensureGameIsNotPaused(); if (!gameIsStarted) startGame(); if (!gameIsOver) dynamicShape.pivot(); event.preventDefault(); } else if (key === "ArrowLeft") { ensureGameIsNotPaused(); if (!gameIsStarted) startGame(); if (!gameIsOver) dynamicShape.moveLeft(); event.preventDefault(); } else if (key === "ArrowRight") { ensureGameIsNotPaused(); if (!gameIsStarted) startGame(); if (!gameIsOver) dynamicShape.moveRight(); event.preventDefault(); } else if (key === "ArrowDown") { ensureGameIsNotPaused(); if (!gameIsStarted) startGame(); else if (!gameIsOver) dynamicShape.moveDown(); event.preventDefault(); } else if (key === "Escape") { pauseGame(); } else if (key === "a" || key === "A") { Info.display(AUTHOR); } else if (key === "v" || key === "V") { Info.display(VERSION); } else console.log(key); } 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); displayScoreDelay += ScoreBoard.delay; setTimeout(ScoreKeeper.incrementScore, displayScoreDelay); }); StaticBlocks.forEach(function(block) { if (block.getTop() < rowNum) block.shiftDown(1); }); } function clearFullRows() { displayScoreDelay = 0; 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 endGame() { gameIsOver = true; clearInterval(timerId); State.display("Game over."); Info.display("Press F5 to restart."); } 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()) { endGame(); while (nextShape.overlapsOtherShape()) { nextShape.shiftUp(1); } nextShape.removeOutOfBoundsPieces(); } return nextShape; } function startGame() { gameIsStarted = true; dynamicShape.moveDown(); timerId = setInterval(function() { if (!gameIsPaused) { dynamicShape.moveDown(); } }, 1000); } /*** MAIN ***/ State.display("Ready?"); Info.display("Press an arrow key to begin."); window.addEventListener("keydown", onKeyDown); setTimeout(function() {dynamicShape = getNextShape();}, 500); })(); </script>
Newer Post
Older Post
Home