├── assets ├── images │ ├── apple.png │ ├── menu.png │ ├── snake.png │ └── gameover.png ├── styles │ └── main.css └── js │ ├── main.js │ ├── menu.js │ ├── game_over.js │ └── game.js ├── README.md ├── contracts └── DAppHeroGame.sol └── index.html /assets/images/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoorvlathey/Blockchain-Snake-Game-Using-DappHero-/HEAD/assets/images/apple.png -------------------------------------------------------------------------------- /assets/images/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoorvlathey/Blockchain-Snake-Game-Using-DappHero-/HEAD/assets/images/menu.png -------------------------------------------------------------------------------- /assets/images/snake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoorvlathey/Blockchain-Snake-Game-Using-DappHero-/HEAD/assets/images/snake.png -------------------------------------------------------------------------------- /assets/images/gameover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoorvlathey/Blockchain-Snake-Game-Using-DappHero-/HEAD/assets/images/gameover.png -------------------------------------------------------------------------------- /assets/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #061f27; 3 | color: white; 4 | } 5 | 6 | button { 7 | background-color: #44b9fc; 8 | border: none; 9 | color: white; 10 | padding: 15px 32px; 11 | text-align: center; 12 | text-decoration: none; 13 | display: inline-block; 14 | font-size: 16px; 15 | } 16 | 17 | .center { 18 | margin-left: 46%; 19 | border-radius: 1.4em; 20 | } 21 | 22 | canvas { 23 | margin-left: auto; 24 | margin-right: auto; 25 | border: 5px solid white; 26 | } -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | var game; 2 | 3 | // create new game instance 600px wide and 450px tall 4 | game = new Phaser.Game(600, 450, Phaser.AUTO, ''); //phaser.auto is renderer type. 5 | 6 | // First param, how state will be called 7 | // Second param, obj containing needed methods for state functionality 8 | game.state.add('Menu', Menu); 9 | 10 | game.state.start('Menu'); 11 | 12 | // adding the game state 13 | game.state.add('Game', Game); 14 | 15 | game.state.start('Menu'); 16 | 17 | game.state.add('Game_Over', Game_Over); -------------------------------------------------------------------------------- /assets/js/menu.js: -------------------------------------------------------------------------------- 1 | var Menu = { 2 | 3 | preload : function() { 4 | // First arg how img referred to 5 | // Second arg path to file 6 | game.load.image('menu', './assets/images/menu.png'); 7 | }, 8 | 9 | create: function() { 10 | // // Add sprite to game. Here it's game logo 11 | // // Parameters are: X, Y, image name 12 | // this.add.sprite(0, 0, 'menu'); 13 | 14 | // Add menu screen as button 15 | this.add.button(0, 0, 'menu', this.startGame, this); 16 | }, 17 | 18 | startGame: function() { 19 | if(getStatus() === "true") { 20 | // change state to actual game 21 | this.state.start('Game'); 22 | } else if(getStatus() === "STATUS") { 23 | console.log("Wait to Load web3 data") 24 | } else { 25 | alert("Deposit ETH to Play!") 26 | } 27 | } 28 | 29 | }; -------------------------------------------------------------------------------- /assets/js/game_over.js: -------------------------------------------------------------------------------- 1 | var Game_Over = { 2 | 3 | preload : function() { 4 | // Load the needed image for this game screen. 5 | game.load.image('gameover', './assets/images/gameover.png'); 6 | }, 7 | 8 | create : function() { 9 | 10 | // Create button to start game like in Menu. 11 | this.add.button(0, 0, 'gameover', this.startGame, this); 12 | claimPrize(score); 13 | 14 | // Add text with information about the score from last game. 15 | game.add.text(235, 350, "LAST SCORE", { font: "bold 16px sans-serif", fill: "#46c0f9", align: "center"}); 16 | game.add.text(350, 348, score.toString(), { font: "bold 20px sans-serif", fill: "#fff", align: "center" }); 17 | 18 | }, 19 | 20 | startGame: function () { 21 | 22 | // Change the state back to Game. 23 | //this.state.start('Game'); 24 | 25 | } 26 | 27 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blockchain Snake Game (Using DappHero) 2 | Score Higher than the Rest and Win Crypto! | [Rinkeby Demo](https://apoorvlathey.com/projects/BlockchainSnakeGame/) 3 | 4 | Video Demo: [YouTube](https://youtu.be/cBSaPn-Pdh0) 5 | 6 | ![](https://i.imgur.com/RWyIiuT.png) 7 | 8 | Combining the Classic Snake game and the latest technology: Blockchain, this game aims to reward crypto to the Players that score better than the rest. 9 | 10 | In order to start the match, the player deposits .001 ETH into the Game Pool and begins the game. On biting the snake's tail or hitting the wall, the game ends and the score is sent to the Game's Smart Contract. If the player's score is greater than the current overall Player Average than he is rewarded with .001 ETH (initial deposit) + 1% winning prize (0.00001 ETH). Losers lose entire .001 ETH to the Game Pool. 11 | 12 | The entire blockchain interaction is handled via DappHero, game logic in phaser.js. 13 | 14 | The game follows a unique concept to maintain balance: 15 | If the OverallAverageScore to beat is lower, then the players can easily win emptying out our Prize Pool one-by-one, but because in order to win, the score must be higher than the Total Average Score. This means that with each new win the target to beat (Overall Average) keeps on increasing meaning that it becomes more difficult for each following player. This means more less-skilled players would now lose, losing their .001 ETH deposit thereby maintaining a balance between Prize Pool Reserves and Score To Beat. 16 | 17 | ![](https://i.imgur.com/F7UJiq8.png) 18 | -------------------------------------------------------------------------------- /contracts/DAppHeroGame.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | library SafeMath { 4 | 5 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 6 | uint256 c = a + b; 7 | require(c >= a, "SafeMath: addition overflow"); 8 | 9 | return c; 10 | } 11 | 12 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 13 | return sub(a, b, "SafeMath: subtraction overflow"); 14 | } 15 | 16 | function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 17 | require(b <= a, errorMessage); 18 | uint256 c = a - b; 19 | 20 | return c; 21 | } 22 | 23 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 24 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 25 | // benefit is lost if 'b' is also tested. 26 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 27 | if (a == 0) { 28 | return 0; 29 | } 30 | 31 | uint256 c = a * b; 32 | require(c / a == b, "SafeMath: multiplication overflow"); 33 | 34 | return c; 35 | } 36 | 37 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 38 | return div(a, b, "SafeMath: division by zero"); 39 | } 40 | 41 | function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 42 | // Solidity only automatically asserts when dividing by 0 43 | require(b > 0, errorMessage); 44 | uint256 c = a / b; 45 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 46 | 47 | return c; 48 | } 49 | 50 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 51 | return mod(a, b, "SafeMath: modulo by zero"); 52 | } 53 | 54 | function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 55 | require(b != 0, errorMessage); 56 | return a % b; 57 | } 58 | } 59 | 60 | contract DAppHeroGame { 61 | using SafeMath for uint256; 62 | uint256 public prizePool; 63 | uint256 public avgScore; 64 | 65 | struct Challenge { 66 | bool status; // 0 = Ended; 1 = Started 67 | uint256 deposited; 68 | } 69 | 70 | mapping(address => Challenge) public userChallenge; 71 | 72 | constructor(uint256 _initialAvgScore) public { 73 | avgScore = _initialAvgScore; 74 | } 75 | 76 | function newChallenge() external payable { 77 | require(msg.value > 0); 78 | 79 | userChallenge[msg.sender].deposited = (userChallenge[msg.sender].deposited).add(msg.value); 80 | userChallenge[msg.sender].status = true; 81 | prizePool = prizePool.add(msg.value); 82 | } 83 | 84 | function claim(uint256 _score) external { 85 | require(userChallenge[msg.sender].deposited > 0 && userChallenge[msg.sender].status == true); 86 | 87 | if(_score > avgScore) { 88 | //Send original + 1% extra from pool 89 | (msg.sender).transfer((userChallenge[msg.sender].deposited).mul(101).div(100)); 90 | prizePool = prizePool.sub((userChallenge[msg.sender].deposited).mul(101).div(100)); 91 | } 92 | 93 | avgScore = avgScore.add(_score).div(2); 94 | userChallenge[msg.sender].deposited = 0; 95 | userChallenge[msg.sender].status = false; 96 | } 97 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blockchain Snake Game 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

-- Blockchain Snake Game --

20 |

21 | 28 |

29 |

30 | Average Score To Beat: 31 | 40 | 41 |

42 |

43 | CURRENT GAME POOL: 44 | 55 | 56 | ETH 57 | 65 | 66 | 73 | 80 | 81 | 89 | 95 | 103 | 109 | 116 | 117 | 123 | 136 |

137 | 138 | 143 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /assets/js/game.js: -------------------------------------------------------------------------------- 1 | var snake, 2 | apple, 3 | squareSize, 4 | score, 5 | speed, 6 | updateDelay, 7 | direction, 8 | new_direction, 9 | addNew, 10 | cursors, 11 | scoreTextValue, 12 | speedTextValue, 13 | textStyle_Key, 14 | textStyle_Value; 15 | 16 | var Game = { 17 | preload: function() { 18 | game.load.image("snake", "./assets/images/snake.png"); 19 | game.load.image("apple", "./assets/images/apple.png"); 20 | }, 21 | 22 | create: function() { 23 | // By setting up global variables in the create function, we initialise them on game start. 24 | // We need them to be globally available so that the update function can alter them. 25 | 26 | snake = []; // stack 27 | apple = {}; // obj for apple 28 | squareSize = 15; // our image is 15x15 29 | score = 0; 30 | speed = 0; // game speed 31 | updateDelay = 0; // to control over update rates 32 | direction = "right"; // direction of snake 33 | new_direction = null; // buffer to store new direction into 34 | addNew = false; // variable used when an apple has been eaten 35 | 36 | // Setup a Phaser controller for keyboard input 37 | cursors = game.input.keyboard.createCursorKeys(); 38 | 39 | game.stage.backgroundColor = "#061f27"; 40 | 41 | // generate initial stack. our snake is 10 elements long 42 | // beginning at X=150 Y=150 and increasing X on every iteration 43 | for (var i = 0; i < 10; i++) { 44 | snake[i] = game.add.sprite(150 + i * squareSize, 150, "snake"); 45 | } 46 | 47 | // generate first apple 48 | this.generateApple(); 49 | 50 | // add text to top of game 51 | textStyle_Key = { 52 | font: "bold 14px sans-serif", 53 | fill: "#46c0f9", 54 | align: "center" 55 | }; 56 | textStyle_Value = { 57 | font: "bold 18px sans-serif", 58 | fill: "#fff", 59 | align: "center" 60 | }; 61 | 62 | // Score 63 | game.add.text(30, 20, "SCORE", textStyle_Key); 64 | scoreTextValue = game.add.text(90, 18, score.toString(), textStyle_Value); 65 | // Speed 66 | game.add.text(500, 20, "SPEED", textStyle_Key); 67 | speedTextValue = game.add.text(558, 18, speed.toString(), textStyle_Value); 68 | }, 69 | 70 | update: function() { 71 | // Handle arrow key presses, while not allowing illegal direction changes that will kill the player. 72 | if (cursors.right.isDown && direction != "left") { 73 | new_direction = "right"; 74 | } else if (cursors.left.isDown && direction != "right") { 75 | new_direction = "left"; 76 | } else if (cursors.up.isDown && direction != "down") { 77 | new_direction = "up"; 78 | } else if (cursors.down.isDown && direction != "up") { 79 | new_direction = "down"; 80 | } 81 | 82 | // A formula to calculate game speed based on the score. 83 | // The higher the score, the higher the game speed, with a maximum of 10; 84 | speed = Math.min(100, Math.floor(score / 5)); 85 | // Update speed value on game screen. 86 | speedTextValue.text = "" + speed; 87 | 88 | // Since the update function of Phaser has an update rate of around 60 FPS, 89 | // we need to slow that down make the game playable. 90 | 91 | // Increase a counter on every update call. 92 | updateDelay++; 93 | 94 | // Do game stuff only if the counter is aliquot to (10 - the game speed). 95 | // The higher the speed, the more frequently this is fulfilled, 96 | // making the snake move faster. 97 | if (updateDelay % (10 - speed) == 0) { 98 | // Snake movement 99 | 100 | var firstCell = snake[snake.length - 1], 101 | lastCell = snake.shift(), 102 | oldLastCellx = lastCell.x, 103 | oldLastCelly = lastCell.y; 104 | 105 | // Update Direction 106 | if (new_direction) { 107 | direction = new_direction; 108 | new_direction = null; 109 | } 110 | 111 | // Change the last cell's coordinates relative to the head of the snake, according to the direction. 112 | 113 | if (direction == "right") { 114 | lastCell.x = firstCell.x + 15; 115 | lastCell.y = firstCell.y; 116 | } else if (direction == "left") { 117 | lastCell.x = firstCell.x - 15; 118 | lastCell.y = firstCell.y; 119 | } else if (direction == "up") { 120 | lastCell.x = firstCell.x; 121 | lastCell.y = firstCell.y - 15; 122 | } else if (direction == "down") { 123 | lastCell.x = firstCell.x; 124 | lastCell.y = firstCell.y + 15; 125 | } 126 | 127 | // Place the last cell in the front of the stack. 128 | // Mark it the first cell. 129 | 130 | snake.push(lastCell); 131 | firstCell = lastCell; 132 | 133 | // Increase length of snake if an apple had been eaten. 134 | // Create a block in the back of the snake with the old position of the previous last block 135 | // (it has moved now along with the rest of the snake). 136 | 137 | if (addNew) { 138 | snake.unshift(game.add.sprite(oldLastCellx, oldLastCelly, "snake")); 139 | addNew = false; 140 | } 141 | 142 | // Check for apple collision. 143 | this.appleCollision(); 144 | 145 | // Check for collision with self. Parameter is the head of the snake. 146 | this.selfCollision(firstCell); 147 | 148 | // Check with collision with wall. Parameter is the head of the snake. 149 | this.wallCollision(firstCell); 150 | } 151 | }, 152 | 153 | generateApple: function() { 154 | // Chose a random place on the grid. 155 | // X is between 0 and 585 (39*15) 156 | // Y is between 0 and 435 (29*15) 157 | 158 | var randomX = Math.floor(Math.random() * 40) * squareSize, 159 | randomY = Math.floor(Math.random() * 30) * squareSize; 160 | 161 | // Add a new apple. 162 | apple = game.add.sprite(randomX, randomY, "apple"); 163 | }, 164 | 165 | appleCollision: function() { 166 | // Check if any part of the snake is overlapping the apple. 167 | // This is needed if the apple spawns inside of the snake. 168 | for (var i = 0; i < snake.length; i++) { 169 | if (snake[i].x == apple.x && snake[i].y == apple.y) { 170 | // Next time the snake moves, a new block will be added to its length. 171 | addNew = true; 172 | 173 | // Destroy the old apple. 174 | apple.destroy(); 175 | 176 | // Make a new one. 177 | this.generateApple(); 178 | 179 | // Increase score. 180 | score++; 181 | 182 | // Refresh scoreboard. 183 | scoreTextValue.text = score.toString(); 184 | } 185 | } 186 | }, 187 | 188 | selfCollision: function(head) { 189 | // Check if the head of the snake overlaps with any part of the snake. 190 | for (var i = 0; i < snake.length - 1; i++) { 191 | if (head.x == snake[i].x && head.y == snake[i].y) { 192 | // If so, go to game over screen. 193 | game.state.start("Game_Over"); 194 | } 195 | } 196 | }, 197 | 198 | wallCollision: function(head) { 199 | // Check if the head of the snake is in the boundaries of the game field. 200 | 201 | if (head.x >= 600 || head.x < 0 || head.y >= 450 || head.y < 0) { 202 | // If it's not in, we've hit a wall. Go to game over screen. 203 | game.state.start("Game_Over"); 204 | } 205 | } 206 | }; 207 | --------------------------------------------------------------------------------