Membuat Game Tetris Menggunakan JavaScript
Buat kalian generasi 90-an pasti hafal dengan permainan tetris yang sangat populer ini. Permainan ini biasanya ditemukan pada game bot atau bisa kita temukan di konsol nintendo. Permainan tetris adalah permainan menyusun balok , setiap balok terdiri dari 4 kotak dengan bentuk berbeda dan dimunculkan secara acak atau random. Anda diharuskan menyusun setiap balok yang muncul dari atas kebawah supaya simetris (tidak bolong). Jika baris paling bawah ada balok yang sejajar minimal satu, maka anda akan mendapat satu skor. Jika maksimal tumpukan balok terpenuhi sampai ujung atas, maka permainan di anggap selesai (game over).
Dalam tutorial kali ini, kita akan membuat game tetris menggunakan JavaScript. JavaScript memang banyak digunakan oleh para developer game untuk membuat game mereka dari mulai yang sederhana sampai yang kompleks. Disini kita akan membuat game sederhana menggunakan JavaScript untuk membuat permainan legendaris yaitu tetris.
Struktur File :
Buat Folder js , isi filenya :
1. ai.js
2. game_manager.js
3. grid.js
4. piece.js
5. polyfill.js
6. random_piece_generator.js
7. stopwatch.js
8. timer.js
9. tuner.js
Buat Folder style, isi filenya :
1. main.css
Buat satu file index.html diluar folder.
Kemudian isi kode berikut pada setiap filenya.
1. index.html
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>Tetris AI</title> <link href="style/main.css" rel="stylesheet" type="text/css"> </head> <body> <div id="fb-root"></div> <div class="container"> <h1 class="header">Tetris JavaScript</h1> <div class="game-container"> <canvas width="200px" height="400px" id="grid-canvas" class="grid"></canvas> <div class="controls-container"> <div id="score-container" class="score-container">0</div> <div id="next-container" class="next-container"> <canvas id="next-canvas" class="next-canvas" width="80px" height="80px"></canvas> </div> <a id="ai-button" class="start-ai-button">Auto</a> <a id="reset-button" class="reset-button">Reset</a> </div> </div> <hr> <div class="footer"> <p> </p> </div> </div> <script src="js/polyfill.js"></script> <script src="js/grid.js"></script> <script src="js/piece.js"></script> <script src="js/timer.js"></script> <script src="js/stopwatch.js"></script> <script src="js/game_manager.js"></script> <script src="js/random_piece_generator.js"></script> <script src="js/ai.js"></script> <script src="js/tuner.js"></script> <script> var manager = new GameManager(); </script> <script> (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); ga('create', 'UA-68147157-4', 'auto'); ga('send', 'pageview'); </script> </body> </html>
2. main.css
body { background-color: #FCFCFC; } a { color: #0000FF; font-weight: bold; text-decoration: underline; } a:hover { cursor: pointer } hr { border: none; border-bottom: 1px solid #C8C8C8; } .container { width:350px; margin: 0 auto; } .header { text-align: center; font-family: Verdana; color:#0000FF; } .game-container { height:400px; margin-top:10px; } .grid { float:left; background-color: #FFFFFF; box-shadow: 0 0 10px 2px #EEEEEE; border:1px solid #CCCCCC; } .next-canvas { } .controls-container { width:130px; float:right; height:100%; } .score-container { background-color: #FFFFFF; box-shadow: 0 0 10px 2px #EEEEEE; text-align: center; color: #FF0000; font-size:25px; border:1px solid #CCCCCC; font-weight: bold; padding:5px; } .score-container::before { content: "Score"; width:100%; float:right; color: #BBBBBB; font-size:15px; font-weight: normal; } .next-container { background-color: #FFFFFF; box-shadow: 0 0 10px 2px #EEEEEE; text-align: center; color: #FF0000; font-size:25px; border:1px solid #CCCCCC; font-weight: bold; padding:5px; margin-top: 10px; } .next-container::before { content: "Next Piece"; width:100%; float:right; color: #BBBBBB; font-size:15px; font-weight: normal; } .start-ai-button, .reset-button { -moz-box-shadow:inset 0px 1px 0px 0px #ffffff; -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff; box-shadow:inset 0px 1px 0px 0px #ffffff; background-color:#f9f9f9; -webkit-border-top-left-radius:3px; -moz-border-radius-topleft:3px; border-top-left-radius:3px; -webkit-border-top-right-radius:3px; -moz-border-radius-topright:3px; border-top-right-radius:3px; -webkit-border-bottom-right-radius:3px; -moz-border-radius-bottomright:3px; border-bottom-right-radius:3px; -webkit-border-bottom-left-radius:3px; -moz-border-radius-bottomleft:3px; border-bottom-left-radius:3px; text-indent:0; border:1px solid #dcdcdc; display:inline-block; color:#666666; font-family:Arial; font-size:15px; font-weight:bold; font-style:normal; height:40px; line-height:40px; width:100%; text-decoration:none; text-align:center; text-shadow:1px 1px 0px #ffffff; } .start-ai-button:hover, .reset-button:hover { background-color:#e9e9e9; } .start-ai-button:active, .reset-button:active { position:relative; top:1px; } .start-ai-button{ margin-top: 15px; } .reset-button{ margin-top: 10px; } .footer { font-family: Verdana; color:#000000; font-size: 12px; } .fb-like { vertical-align: top; } .fb_iframe_widget span { vertical-align: text-top !important; }
3. ai.js
function AI(heightWeight, linesWeight, holesWeight, bumpinessWeight){ this.heightWeight = heightWeight; this.linesWeight = linesWeight; this.holesWeight = holesWeight; this.bumpinessWeight = bumpinessWeight; }; AI.prototype._best = function(grid, workingPieces, workingPieceIndex){ var best = null; var bestScore = null; var workingPiece = workingPieces[workingPieceIndex]; for(var rotation = 0; rotation < 4; rotation++){ var _piece = workingPiece.clone(); for(var i = 0; i < rotation; i++){ _piece.rotate(grid); } while(_piece.moveLeft(grid)); while(grid.valid(_piece)){ var _pieceSet = _piece.clone(); while(_pieceSet.moveDown(grid)); var _grid = grid.clone(); _grid.addPiece(_pieceSet); var score = null; if (workingPieceIndex == (workingPieces.length - 1)) { score = -this.heightWeight * _grid.aggregateHeight() + this.linesWeight * _grid.lines() - this.holesWeight * _grid.holes() - this.bumpinessWeight * _grid.bumpiness(); }else{ score = this._best(_grid, workingPieces, workingPieceIndex + 1).score; } if (score > bestScore || bestScore == null){ bestScore = score; best = _piece.clone(); } _piece.column++; } } return {piece:best, score:bestScore}; }; AI.prototype.best = function(grid, workingPieces){ return this._best(grid, workingPieces, 0).piece; };
4. game_manager.js
function GameManager(){ var gridCanvas = document.getElementById('grid-canvas'); var nextCanvas = document.getElementById('next-canvas'); var scoreContainer = document.getElementById("score-container"); var resetButton = document.getElementById('reset-button'); var aiButton = document.getElementById('ai-button'); var gridContext = gridCanvas.getContext('2d'); var nextContext = nextCanvas.getContext('2d'); document.addEventListener('keydown', onKeyDown); var grid = new Grid(22, 10); var rpg = new RandomPieceGenerator(); var ai = new AI(0.510066, 0.760666, 0.35663, 0.184483); var workingPieces = [null, rpg.nextPiece()]; var workingPiece = null; var isAiActive = true; var isKeyEnabled = false; var gravityTimer = new Timer(onGravityTimerTick, 500); var score = 0; // Graphics function intToRGBHexString(v){ return 'rgb(' + ((v >> 16) & 0xFF) + ',' + ((v >> 8) & 0xFF) + ',' + (v & 0xFF) + ')'; } function redrawGridCanvas(workingPieceVerticalOffset = 0){ gridContext.save(); // Clear gridContext.clearRect(0, 0, gridCanvas.width, gridCanvas.height); // Draw grid for(var r = 2; r < grid.rows; r++){ for(var c = 0; c < grid.columns; c++){ if (grid.cells[r][c] != 0){ gridContext.fillStyle= intToRGBHexString(grid.cells[r][c]); gridContext.fillRect(20 * c, 20 * (r - 2), 20, 20); gridContext.strokeStyle="#FFFFFF"; gridContext.strokeRect(20 * c, 20 * (r - 2), 20, 20); } } } // Draw working piece for(var r = 0; r < workingPiece.dimension; r++){ for(var c = 0; c < workingPiece.dimension; c++){ if (workingPiece.cells[r][c] != 0){ gridContext.fillStyle = intToRGBHexString(workingPiece.cells[r][c]); gridContext.fillRect(20 * (c + workingPiece.column), 20 * ((r + workingPiece.row) - 2) + workingPieceVerticalOffset, 20, 20); gridContext.strokeStyle="#FFFFFF"; gridContext.strokeRect(20 * (c + workingPiece.column), 20 * ((r + workingPiece.row) - 2) + workingPieceVerticalOffset, 20, 20); } } } gridContext.restore(); } function redrawNextCanvas(){ nextContext.save(); nextContext.clearRect(0, 0, nextCanvas.width, nextCanvas.height); var next = workingPieces[1]; var xOffset = next.dimension == 2 ? 20 : next.dimension == 3 ? 10 : next.dimension == 4 ? 0 : null; var yOffset = next.dimension == 2 ? 20 : next.dimension == 3 ? 20 : next.dimension == 4 ? 10 : null; for(var r = 0; r < next.dimension; r++){ for(var c = 0; c < next.dimension; c++){ if (next.cells[r][c] != 0){ nextContext.fillStyle = intToRGBHexString(next.cells[r][c]); nextContext.fillRect(xOffset + 20 * c, yOffset + 20 * r, 20, 20); nextContext.strokeStyle = "#FFFFFF"; nextContext.strokeRect(xOffset + 20 * c, yOffset + 20 * r, 20, 20); } } } nextContext.restore(); } function updateScoreContainer(){ scoreContainer.innerHTML = score.toString(); } // Drop animation var workingPieceDropAnimationStopwatch = null; function startWorkingPieceDropAnimation(callback = function(){}){ // Calculate animation height animationHeight = 0; _workingPiece = workingPiece.clone(); while(_workingPiece.moveDown(grid)){ animationHeight++; } var stopwatch = new Stopwatch(function(elapsed){ if(elapsed >= animationHeight * 20){ stopwatch.stop(); redrawGridCanvas(20 * animationHeight); callback(); return; } redrawGridCanvas(20 * elapsed / 20); }); workingPieceDropAnimationStopwatch = stopwatch; } function cancelWorkingPieceDropAnimation(){ if(workingPieceDropAnimationStopwatch === null){ return; } workingPieceDropAnimationStopwatch.stop(); workingPieceDropAnimationStopwatch = null; } // Process start of turn function startTurn(){ // Shift working pieces for(var i = 0; i < workingPieces.length - 1; i++){ workingPieces[i] = workingPieces[i + 1]; } workingPieces[workingPieces.length - 1] = rpg.nextPiece(); workingPiece = workingPieces[0]; // Refresh Graphics redrawGridCanvas(); redrawNextCanvas(); if(isAiActive){ isKeyEnabled = false; workingPiece = ai.best(grid, workingPieces); startWorkingPieceDropAnimation(function(){ while(workingPiece.moveDown(grid)); // Drop working piece if(!endTurn()){ alert('Game Over!'); return; } startTurn(); }) }else{ isKeyEnabled = true; gravityTimer.resetForward(500); } } // Process end of turn function endTurn(){ // Add working piece grid.addPiece(workingPiece); // Clear lines score += grid.clearLines(); // Refresh graphics redrawGridCanvas(); updateScoreContainer(); return !grid.exceeded(); } // Process gravity tick function onGravityTimerTick(){ // If working piece has not reached bottom if(workingPiece.canMoveDown(grid)){ workingPiece.moveDown(grid); redrawGridCanvas(); return; } // Stop gravity if working piece has reached bottom gravityTimer.stop(); // If working piece has reached bottom, end of turn has been processed // and game cannot continue because grid has been exceeded if(!endTurn()){ isKeyEnabled = false; alert('Game Over!'); return; } // If working piece has reached bottom, end of turn has been processed // and game can still continue. startTurn(); } // Process keys function onKeyDown(event){ if(!isKeyEnabled){ return; } switch(event.which){ case 32: // spacebar isKeyEnabled = false; gravityTimer.stop(); // Stop gravity startWorkingPieceDropAnimation(function(){ // Start drop animation while(workingPiece.moveDown(grid)); // Drop working piece if(!endTurn()){ alert('Game Over!'); return; } startTurn(); }); break; case 40: // down gravityTimer.resetForward(500); break; case 37: //left if(workingPiece.canMoveLeft(grid)){ workingPiece.moveLeft(grid); redrawGridCanvas(); } break; case 39: //right if(workingPiece.canMoveRight(grid)){ workingPiece.moveRight(grid); redrawGridCanvas(); } break; case 38: //up workingPiece.rotate(grid); redrawGridCanvas(); break; } } aiButton.onclick = function(){ if (isAiActive){ isAiActive = false; aiButton.style.backgroundColor = "#f9f9f9"; }else{ isAiActive = true; aiButton.style.backgroundColor = "#e9e9ff"; isKeyEnabled = false; gravityTimer.stop(); startWorkingPieceDropAnimation(function(){ // Start drop animation while(workingPiece.moveDown(grid)); // Drop working piece if(!endTurn()){ alert('Game Over!'); return; } startTurn(); }); } } resetButton.onclick = function(){ gravityTimer.stop(); cancelWorkingPieceDropAnimation(); grid = new Grid(22, 10); rpg = new RandomPieceGenerator(); workingPieces = [null, rpg.nextPiece()]; workingPiece = null; score = 0; isKeyEnabled = true; updateScoreContainer(); startTurn(); } aiButton.style.backgroundColor = "#e9e9ff"; startTurn(); }
5. grid.js
function Grid(rows, columns){ this.rows = rows; this.columns = columns; this.cells = new Array(rows); for (var r = 0; r < this.rows; r++) { this.cells[r] = new Array(this.columns); for(var c = 0; c < this.columns; c++){ this.cells[r][c] = 0; } } }; Grid.prototype.clone = function(){ var _grid = new Grid(this.rows, this.columns); for (var r = 0; r < this.rows; r++) { for(var c = 0; c < this.columns; c++){ _grid.cells[r][c] = this.cells[r][c]; } } return _grid; }; Grid.prototype.clearLines = function(){ var distance = 0; var row = new Array(this.columns); for(var r = this.rows - 1; r >= 0; r--){ if (this.isLine(r)){ distance++; for(var c = 0; c < this.columns; c++){ this.cells[r][c] = 0; } }else if (distance > 0){ for(var c = 0; c < this.columns; c++){ this.cells[r + distance][c] = this.cells[r][c]; this.cells[r][c] = 0; } } } return distance; }; Grid.prototype.isLine = function(row){ for(var c = 0; c < this.columns; c++){ if (this.cells[row][c] == 0){ return false; } } return true; }; Grid.prototype.isEmptyRow = function(row){ for(var c = 0; c < this.columns; c++){ if (this.cells[row][c] != 0){ return false; } } return true; }; Grid.prototype.exceeded = function(){ return !this.isEmptyRow(0) || !this.isEmptyRow(1); }; Grid.prototype.height = function(){ var r = 0; for(; r < this.rows && this.isEmptyRow(r); r++); return this.rows - r; }; Grid.prototype.lines = function(){ var count = 0; for(var r = 0; r < this.rows; r++){ if (this.isLine(r)){ count++; } } return count; }; Grid.prototype.holes = function(){ var count = 0; for(var c = 0; c < this.columns; c++){ var block = false; for(var r = 0; r < this.rows; r++){ if (this.cells[r][c] != 0) { block = true; }else if (this.cells[r][c] == 0 && block){ count++; } } } return count; }; Grid.prototype.blockades = function(){ var count = 0; for(var c = 0; c < this.columns; c++){ var hole = false; for(var r = this.rows - 1; r >= 0; r--){ if (this.cells[r][c] == 0){ hole = true; }else if (this.cells[r][c] != 0 && hole){ count++; } } } return count; } Grid.prototype.aggregateHeight = function(){ var total = 0; for(var c = 0; c < this.columns; c++){ total += this.columnHeight(c); } return total; }; Grid.prototype.bumpiness = function(){ var total = 0; for(var c = 0; c < this.columns - 1; c++){ total += Math.abs(this.columnHeight(c) - this.columnHeight(c+ 1)); } return total; } Grid.prototype.columnHeight = function(column){ var r = 0; for(; r < this.rows && this.cells[r][column] == 0; r++); return this.rows - r; }; Grid.prototype.addPiece = function(piece) { for(var r = 0; r < piece.cells.length; r++) { for (var c = 0; c < piece.cells[r].length; c++) { var _r = piece.row + r; var _c = piece.column + c; if (piece.cells[r][c] != 0 && _r >= 0){ this.cells[_r][_c] = piece.cells[r][c]; } } } }; Grid.prototype.valid = function(piece){ for(var r = 0; r < piece.cells.length; r++){ for(var c = 0; c < piece.cells[r].length; c++){ var _r = piece.row + r; var _c = piece.column + c; if (piece.cells[r][c] != 0){ if(_r < 0 || _r >= this.rows){ return false; } if(_c < 0 || _c >= this.columns){ return false; } if (this.cells[_r][_c] != 0){ return false; } } } } return true; };
6. piece.js
function Piece(cells){ this.cells = cells; this.dimension = this.cells.length; this.row = 0; this.column = 0; }; Piece.fromIndex = function(index){ var piece; switch (index){ case 0:// O piece = new Piece([ [0x0000AA, 0x0000AA], [0x0000AA, 0x0000AA] ]); break; case 1: // J piece = new Piece([ [0xC0C0C0, 0x000000, 0x000000], [0xC0C0C0, 0xC0C0C0, 0xC0C0C0], [0x000000, 0x000000, 0x000000] ]); break; case 2: // L piece = new Piece([ [0x000000, 0x000000, 0xAA00AA], [0xAA00AA, 0xAA00AA, 0xAA00AA], [0x000000, 0x000000, 0x000000] ]); break; case 3: // Z piece = new Piece([ [0x00AAAA, 0x00AAAA, 0x000000], [0x000000, 0x00AAAA, 0x00AAAA], [0x000000, 0x000000, 0x000000] ]); break; case 4: // S piece = new Piece([ [0x000000, 0x00AA00, 0x00AA00], [0x00AA00, 0x00AA00, 0x000000], [0x000000, 0x000000, 0x000000] ]); break; case 5: // T piece = new Piece([ [0x000000, 0xAA5500, 0x000000], [0xAA5500, 0xAA5500, 0xAA5500], [0x000000, 0x000000, 0x000000] ]); break; case 6: // I piece = new Piece([ [0x000000, 0x000000, 0x000000, 0x000000], [0xAA0000, 0xAA0000, 0xAA0000, 0xAA0000], [0x000000, 0x000000, 0x000000, 0x000000], [0x000000, 0x000000, 0x000000, 0x000000] ]); break; } piece.row = 0; piece.column = Math.floor((10 - piece.dimension) / 2); // Centralize return piece; }; Piece.prototype.clone = function(){ var _cells = new Array(this.dimension); for (var r = 0; r < this.dimension; r++) { _cells[r] = new Array(this.dimension); for(var c = 0; c < this.dimension; c++){ _cells[r][c] = this.cells[r][c]; } } var piece = new Piece(_cells); piece.row = this.row; piece.column = this.column; return piece; }; Piece.prototype.canMoveLeft = function(grid){ for(var r = 0; r < this.cells.length; r++){ for(var c = 0; c < this.cells[r].length; c++){ var _r = this.row + r; var _c = this.column + c - 1; if (this.cells[r][c] != 0){ if (!(_c >= 0 && grid.cells[_r][_c] == 0)){ return false; } } } } return true; }; Piece.prototype.canMoveRight = function(grid){ for(var r = 0; r < this.cells.length; r++){ for(var c = 0; c < this.cells[r].length; c++){ var _r = this.row + r; var _c = this.column + c + 1; if (this.cells[r][c] != 0){ if (!(_c >= 0 && grid.cells[_r][_c] == 0)){ return false; } } } } return true; }; Piece.prototype.canMoveDown = function(grid){ for(var r = 0; r < this.cells.length; r++){ for(var c = 0; c < this.cells[r].length; c++){ var _r = this.row + r + 1; var _c = this.column + c; if (this.cells[r][c] != 0 && _r >= 0){ if (!(_r < grid.rows && grid.cells[_r][_c] == 0)){ return false; } } } } return true; }; Piece.prototype.moveLeft = function(grid){ if(!this.canMoveLeft(grid)){ return false; } this.column--; return true; }; Piece.prototype.moveRight = function(grid){ if(!this.canMoveRight(grid)){ return false; } this.column++; return true; }; Piece.prototype.moveDown = function(grid){ if(!this.canMoveDown(grid)){ return false; } this.row++; return true; }; Piece.prototype.rotateCells = function(){ var _cells = new Array(this.dimension); for (var r = 0; r < this.dimension; r++) { _cells[r] = new Array(this.dimension); } switch (this.dimension) { // Assumed square matrix case 2: _cells[0][0] = this.cells[1][0]; _cells[0][1] = this.cells[0][0]; _cells[1][0] = this.cells[1][1]; _cells[1][1] = this.cells[0][1]; break; case 3: _cells[0][0] = this.cells[2][0]; _cells[0][1] = this.cells[1][0]; _cells[0][2] = this.cells[0][0]; _cells[1][0] = this.cells[2][1]; _cells[1][1] = this.cells[1][1]; _cells[1][2] = this.cells[0][1]; _cells[2][0] = this.cells[2][2]; _cells[2][1] = this.cells[1][2]; _cells[2][2] = this.cells[0][2]; break; case 4: _cells[0][0] = this.cells[3][0]; _cells[0][1] = this.cells[2][0]; _cells[0][2] = this.cells[1][0]; _cells[0][3] = this.cells[0][0]; _cells[1][3] = this.cells[0][1]; _cells[2][3] = this.cells[0][2]; _cells[3][3] = this.cells[0][3]; _cells[3][2] = this.cells[1][3]; _cells[3][1] = this.cells[2][3]; _cells[3][0] = this.cells[3][3]; _cells[2][0] = this.cells[3][2]; _cells[1][0] = this.cells[3][1]; _cells[1][1] = this.cells[2][1]; _cells[1][2] = this.cells[1][1]; _cells[2][2] = this.cells[1][2]; _cells[2][1] = this.cells[2][2]; break; } this.cells = _cells; }; Piece.prototype.computeRotateOffset = function(grid){ var _piece = this.clone(); _piece.rotateCells(); if (grid.valid(_piece)) { return { rowOffset: _piece.row - this.row, columnOffset: _piece.column - this.column }; } // Kicking var initialRow = _piece.row; var initialCol = _piece.column; for (var i = 0; i < _piece.dimension - 1; i++) { _piece.column = initialCol + i; if (grid.valid(_piece)) { return { rowOffset: _piece.row - this.row, columnOffset: _piece.column - this.column }; } for (var j = 0; j < _piece.dimension - 1; j++) { _piece.row = initialRow - j; if (grid.valid(_piece)) { return { rowOffset: _piece.row - this.row, columnOffset: _piece.column - this.column }; } } _piece.row = initialRow; } _piece.column = initialCol; for (var i = 0; i < _piece.dimension - 1; i++) { _piece.column = initialCol - i; if (grid.valid(_piece)) { return { rowOffset: _piece.row - this.row, columnOffset: _piece.column - this.column }; } for (var j = 0; j < _piece.dimension - 1; j++) { _piece.row = initialRow - j; if (grid.valid(_piece)) { return { rowOffset: _piece.row - this.row, columnOffset: _piece.column - this.column }; } } _piece.row = initialRow; } _piece.column = initialCol; return null; }; Piece.prototype.rotate = function(grid){ var offset = this.computeRotateOffset(grid); if (offset != null){ this.rotateCells(grid); this.row += offset.rowOffset; this.column += offset.columnOffset; } };
7. polyfill.js
// Credits: https://gist.github.com/paulirish/1579671 (function() { var lastTime = 0; var vendors = ['webkit', 'moz']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }());
8. random_piece_generator.js
function RandomPieceGenerator(){ Math.seed this.bag = [0, 1, 2, 3, 4, 5, 6]; this.shuffleBag(); this.index = -1; }; RandomPieceGenerator.prototype.nextPiece = function(){ this.index++; if (this.index >= this.bag.length){ this.shuffleBag(); this.index = 0; } return Piece.fromIndex(this.bag[this.index]); }; RandomPieceGenerator.prototype.shuffleBag = function() { var currentIndex = this.bag.length , temporaryValue , randomIndex ; // While there remain elements to shuffle... while (0 !== currentIndex) { // Pick a remaining element... randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // And swap it with the current element. temporaryValue = this.bag[currentIndex]; this.bag[currentIndex] = this.bag[randomIndex]; this.bag[randomIndex] = temporaryValue; } };
9. stopwatch.js
function Stopwatch(callback) { var isStopped = false; var startDate = null; var self = this; var onAnimationFrame = function(){ if(isStopped){ return; } callback(Date.now() - startDate); requestAnimationFrame(onAnimationFrame); }; this.stop = function() { isStopped = true; } startDate = Date.now(); requestAnimationFrame(onAnimationFrame); }
10. timer.js
function Timer(callback, delay) { var lastUpdate = null; var isRunning = false; var loop = function(){ requestAnimationFrame(function(){ var now = Date.now(); if(!isRunning){ lastUpdate = now; loop(); }else{ var elapsed = now - lastUpdate; if(lastUpdate === null || elapsed > delay){ callback(); lastUpdate = now - (elapsed % delay); } loop(); } }); }; this.start = function() { if(isRunning){ return; } lastUpdate = Date.now(); isRunning = true; } this.stop = function() { isRunning = false; } this.reset = function(newDelay) { lastUpdate = Date.now(); this.start(); } this.resetForward = function(newDelay){ callback(); delay = newDelay; lastUpdate = Date.now(); this.start(); } loop(); }
11. tuner.js
function Tuner(){ function randomInteger(min, max){ return Math.floor(Math.random() * (max - min) + min); } function normalize(candidate){ var norm = Math.sqrt(candidate.heightWeight * candidate.heightWeight + candidate.linesWeight * candidate.linesWeight + candidate.holesWeight * candidate.holesWeight + candidate.bumpinessWeight * candidate.bumpinessWeight); candidate.heightWeight /= norm; candidate.linesWeight /= norm; candidate.holesWeight /= norm; candidate.bumpinessWeight /= norm; } function generateRandomCandidate(){ var candidate = { heightWeight: Math.random() - 0.5, linesWeight: Math.random() - 0.5, holesWeight: Math.random() - 0.5, bumpinessWeight: Math.random() - 0.5 }; normalize(candidate); return candidate; } function sort(candidates){ candidates.sort(function(a, b){ return b.fitness - a.fitness; }); } function computeFitnesses(candidates, numberOfGames, maxNumberOfMoves){ for(var i = 0; i < candidates.length; i++){ var candidate = candidates[i]; var ai = new AI(candidate.heightWeight, candidate.linesWeight, candidate.holesWeight, candidate.bumpinessWeight); var totalScore = 0; for(var j = 0; j < numberOfGames; j++){ var grid = new Grid(22, 10); var rpg = new RandomPieceGenerator(); var workingPieces = [rpg.nextPiece(), rpg.nextPiece()]; var workingPiece = workingPieces[0]; var score = 0; var numberOfMoves = 0; while((numberOfMoves++) < maxNumberOfMoves && !grid.exceeded()){ workingPiece = ai.best(grid, workingPieces); while(workingPiece.moveDown(grid)); grid.addPiece(workingPiece); score += grid.clearLines(); for(var k = 0; k < workingPieces.length - 1; k++){ workingPieces[k] = workingPieces[k + 1]; } workingPieces[workingPieces.length - 1] = rpg.nextPiece(); workingPiece = workingPieces[0]; } totalScore += score; } candidate.fitness = totalScore; } } function tournamentSelectPair(candidates, ways){ var indices = []; for(var i = 0; i < candidates.length; i++){ indices.push(i); } /* Note that the following assumes that the candidates array is sorted according to the fitness of each individual candidates. Hence it suffices to pick the least 2 indexes out of the random ones picked. */ var fittestCandidateIndex1 = null; var fittestCanddiateIndex2 = null; for(var i = 0; i < ways; i++){ var selectedIndex = indices.splice(randomInteger(0, indices.length), 1)[0]; if(fittestCandidateIndex1 === null || selectedIndex < fittestCandidateIndex1){ fittestCanddiateIndex2 = fittestCandidateIndex1; fittestCandidateIndex1 = selectedIndex; }else if (fittestCanddiateIndex2 === null || selectedIndex < fittestCanddiateIndex2){ fittestCanddiateIndex2 = selectedIndex; } } return [candidates[fittestCandidateIndex1], candidates[fittestCanddiateIndex2]]; } function crossOver(candidate1, candidate2){ var candidate = { heightWeight: candidate1.fitness * candidate1.heightWeight + candidate2.fitness * candidate2.heightWeight, linesWeight: candidate1.fitness * candidate1.linesWeight + candidate2.fitness * candidate2.linesWeight, holesWeight: candidate1.fitness * candidate1.holesWeight + candidate2.fitness * candidate2.holesWeight, bumpinessWeight: candidate1.fitness * candidate1.bumpinessWeight + candidate2.fitness * candidate2.bumpinessWeight }; normalize(candidate); return candidate; } function mutate(candidate){ var quantity = Math.random() * 0.4 - 0.2; // plus/minus 0.2 switch(randomInteger(0, 4)){ case 0: candidate.heightWeight += quantity; break; case 1: candidate.linesWeight += quantity; break; case 2: candidate.holesWeight += quantity; break; case 3: candidate.bumpinessWeight += quantity; break; } } function deleteNLastReplacement(candidates, newCandidates){ candidates.slice(-newCandidates.length); for(var i = 0; i < newCandidates.length; i++){ candidates.push(newCandidates[i]); } sort(candidates); } /* Population size = 100 Rounds per candidate = 5 Max moves per round = 200 Theoretical fitness limit = 5 * 200 * 4 / 10 = 400 */ this.tune = function(){ var candidates = []; // Initial population generation for(var i = 0; i < 100; i++){ candidates.push(generateRandomCandidate()); } console.log('Computing fitnesses of initial population...'); computeFitnesses(candidates, 5, 200); sort(candidates); var count = 0; while(true){ var newCandidates = []; for(var i = 0; i < 30; i++){ // 30% of population var pair = tournamentSelectPair(candidates, 10); // 10% of population //console.log('fitnesses = ' + pair[0].fitness + ',' + pair[1].fitness); var candidate = crossOver(pair[0], pair[1]); if(Math.random() < 0.05){// 5% chance of mutation mutate(candidate); } normalize(candidate); newCandidates.push(candidate); } console.log('Computing fitnesses of new candidates. (' + count + ')'); computeFitnesses(newCandidates, 5, 200); deleteNLastReplacement(candidates, newCandidates); var totalFitness = 0; for(var i = 0; i < candidates.length; i++){ totalFitness += candidates[i].fitness; } console.log('Average fitness = ' + (totalFitness / candidates.length)); console.log('Highest fitness = ' + candidates[0].fitness + '(' + count + ')'); console.log('Fittest candidate = ' + JSON.stringify(candidates[0]) + '(' + count + ')'); count++; } }; }
Download Source Code
Game Play :
- Untuk mode permainan ada dua tipe, yaitu manual dan auto. Mode auto jika kita ingin memainkan tetris secara otomatis (jalan sendiri).
- Gunakan tombol panah atas pada keyboard untuk mengubah jenis balok, tombol panah kiri dan kanan untuk menggeser balok dan tombol pana bawah untuk menurunkan balok.
- Tekan menu reset untuk memulai permainan baru jika sudah game over.
- Untuk memainkan game tetris ini, klik kanan pada file index.html dan buka dengan browser.