├── Dockerfile.aarch64 ├── Dockerfile.template ├── README.md ├── package.json └── src └── app.js /Dockerfile.aarch64: -------------------------------------------------------------------------------- 1 | FROM balenalib/raspberrypi3-node:8-build AS base 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package.json . 6 | RUN JOBS=MAX npm install --unsafe-perm --production 7 | 8 | 9 | FROM balenalib/raspberrypi3-node:8 10 | 11 | ENV INITSYSTEM on 12 | 13 | WORKDIR /usr/src/app 14 | COPY --from=base /usr/src/app/node_modules ./node_modules 15 | COPY . ./ 16 | 17 | RUN node --check src/app.js 18 | 19 | CMD node src/app.js 20 | -------------------------------------------------------------------------------- /Dockerfile.template: -------------------------------------------------------------------------------- 1 | FROM balenalib/%%BALENA_MACHINE_NAME%%-node:8-build AS base 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package.json . 6 | RUN JOBS=MAX npm install --unsafe-perm --production 7 | 8 | 9 | FROM balenalib/%%BALENA_MACHINE_NAME%%-node:8 10 | 11 | ENV INITSYSTEM on 12 | 13 | WORKDIR /usr/src/app 14 | COPY --from=base /usr/src/app/node_modules ./node_modules 15 | COPY . ./ 16 | 17 | RUN node --check src/app.js 18 | 19 | CMD node src/app.js 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SenseSnake 2 | 3 | Snake for the raspberrypi sense-hat on balena! 4 | 5 | ## Configuration 6 | 7 | Set the following env-vars to control the game: 8 | 9 | ### SNAKE_TICK_DELAY 10 | Set this to change the delay between frames, smaller delay, faster gameplay 11 | Default: 400 ms 12 | Recommended: 400 for slower gameplay for newer players, 200 for a faster start for those who have played a bit more 13 | 14 | ### SNAKE_TICK_MODIFIER 15 | Set this value to change how much the delay reduces by when eating a food 16 | Default: 10ms 17 | Recommended: 10ms with a higher tick delay (~400ms), 5ms with a lower tick delay (~200ms) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sense-snake", 3 | "version": "0.0.1", 4 | "description": "Snake on the sensehat with raspberry pi and resin", 5 | "main": "src/app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/resin-io-playground/sense-snake.git" 12 | }, 13 | "author": "Cameron Diver ", 14 | "license": "Apache-2.0", 15 | "bugs": { 16 | "url": "https://github.com/resin-io-playground/sense-snake/issues" 17 | }, 18 | "homepage": "https://github.com/resin-io-playground/sense-snake#readme", 19 | "dependencies": { 20 | "lodash": "^4.17.4", 21 | "sense-hat-led": "^1.0.1", 22 | "sense-joystick": "0.0.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const senseJoystick = require('sense-joystick'); 2 | const senseLeds = require('sense-hat-led'); 3 | const _ = require('lodash'); 4 | const lodash = _; 5 | 6 | // The delay between calling our tick function. This also handles the snake 7 | // moving so it should not be too quick 8 | const tickDelayStart = process.env.SNAKE_TICK_DELAY || 400; 9 | const tickDelayModifier = process.env.SNAKE_TICK_MODIFIER || 10; 10 | var tickDelay = tickDelayStart; 11 | 12 | const WIDTH = 8; 13 | const HEIGHT = 8; 14 | 15 | // const snakeColour = [0, 255, 0]; 16 | const snakeColour = [0, 255, 255]; 17 | //const headColour = [137, 172, 163]; 18 | const headColour = [0, 172, 163]; 19 | const black = [0, 0, 0]; 20 | const red = [255, 0, 0]; 21 | const foodColour = [255, 127, 0]; 22 | const mazeColour = [255, 0, 0]; 23 | 24 | const snake = { 25 | // Our snake starts small 26 | size: 2, 27 | // snakes are mostly green 28 | colour: snakeColour, 29 | // start in the middle 30 | positions: [[4, 3], [4, 4]] 31 | }; 32 | 33 | var nextDirection; 34 | var lastDirection; 35 | var foodPos; 36 | var currentMaze = 0; 37 | 38 | var pixelBuffer; 39 | 40 | const { mazes, mazePoints, cross } = (() => { 41 | const _ = black; 42 | const X = red; 43 | const mazes = { 44 | none: [ 45 | _, _, _, _, _, _, _, _, 46 | _, _, _, _, _, _, _, _, 47 | _, _, _, _, _, _, _, _, 48 | _, _, _, _, _, _, _, _, 49 | _, _, _, _, _, _, _, _, 50 | _, _, _, _, _, _, _, _, 51 | _, _, _, _, _, _, _, _, 52 | _, _, _, _, _, _, _, _ 53 | ], 54 | corner: [ 55 | _, _, _, _, _, _, _, _, 56 | _, X, X, _, _, X, X, _, 57 | _, X, _, _, _, _, X, _, 58 | _, _, _, _, _, _, _, _, 59 | _, _, _, _, _, _, _, _, 60 | _, X, _, _, _, _, X, _, 61 | _, X, X, _, _, X, X, _, 62 | _, _, _, _, _, _, _, _ 63 | ], 64 | thing: [ 65 | _, _, _, _, _, _, _, _, 66 | _, X, _, _, _, _, X, _, 67 | _, _, X, _, _, X, _, _, 68 | _, _, X, _, _, X, _, _, 69 | _, _, X, _, _, X, _, _, 70 | _, _, X, _, _, X, _, _, 71 | _, X, _, _, _, _, X, _, 72 | _, _, _, _, _, _, _, _ 73 | ] 74 | }; 75 | 76 | const mazePoints = lodash.map(mazes, (maze) => { 77 | return lodash.flatMap(maze, (pixel, pos) => { 78 | if(pixel === X) { 79 | return [[ pos % WIDTH, Math.floor(pos / WIDTH) ]]; 80 | } 81 | return []; 82 | }); 83 | }); 84 | const cross = [ 85 | X, _, _, _, _, _, _, X, 86 | _, X, _, _, _, _, X, _, 87 | _, _, X, _, _, X, _, _, 88 | _, _, _, X, X, _, _, _, 89 | _, _, _, X, X, _, _, _, 90 | _, _, X, _, _, X, _, _, 91 | _, X, _, _, _, _, X, _, 92 | X, _, _, _, _, _, _, X 93 | ]; 94 | 95 | return { mazes, mazePoints, cross } 96 | })(); 97 | const mazeOptions = _.keys(mazes); 98 | 99 | const clearScreen = () => { 100 | pixelBuffer = _.clone(mazes.none); 101 | }; 102 | 103 | const positionToIdx = ([ x, y ]) => { 104 | if (x < 0 || x >= WIDTH) { 105 | throw new Error(`x is out of bounds: ${x}`); 106 | } 107 | if (y < 0 || y >= HEIGHT) { 108 | throw new Error(`y is out of bounds: ${y}`); 109 | } 110 | return x + WIDTH * y; 111 | }; 112 | 113 | 114 | const setPixel = (pos, colour) => { 115 | pixelBuffer[positionToIdx(pos)] = colour; 116 | }; 117 | 118 | const drawSnake = () => { 119 | setPixel(snake.positions[0], headColour); 120 | _.each(snake.positions.slice(1), (pos) => { 121 | setPixel(pos, snake.colour); 122 | }); 123 | }; 124 | 125 | const moveHead = (head) => { 126 | lastDirection = nextDirection.shift() || lastDirection; 127 | switch(lastDirection) { 128 | case 'up': 129 | return [head[0], head[1] - 1]; 130 | case 'down': 131 | return [head[0], head[1] + 1]; 132 | case 'left': 133 | return [head[0] - 1, head[1]]; 134 | case 'right': 135 | return [head[0] + 1, head[1]]; 136 | case 'stop': 137 | return head; 138 | } 139 | }; 140 | 141 | const oppositeDirection = (direction) => { 142 | switch(direction) { 143 | case 'up': 144 | return 'down'; 145 | case 'down': 146 | return 'up'; 147 | case 'left': 148 | return 'right'; 149 | case 'right': 150 | return 'left'; 151 | case 'stop': 152 | return 'stop'; 153 | } 154 | }; 155 | 156 | const offScreen = (pos) => { 157 | if (pos[0] < 0 || pos[0] >= WIDTH) return true; 158 | if (pos[1] < 0 || pos[1] >= HEIGHT) return true; 159 | return false; 160 | }; 161 | 162 | const displayCross = () => { 163 | senseLeds.setPixels(cross); 164 | }; 165 | 166 | 167 | const pointEquals = (a, b) => { 168 | return a[0] == b[0] && a[1] == b[1]; 169 | }; 170 | 171 | const drawFood = (pos) => { 172 | setPixel(pos, foodColour); 173 | }; 174 | 175 | const drawMaze = () => { 176 | pixelBuffer = _.clone(mazes[mazeOptions[currentMaze]]); 177 | }; 178 | 179 | const isIntersecting = (head, body) => { 180 | const checkCell = (cell) => { 181 | return pointEquals(head, cell); 182 | }; 183 | // Check if the body intersects 184 | if (_.some(body, checkCell)) return true; 185 | 186 | // Check if the maze intersects 187 | if (_.some(mazePoints[currentMaze], checkCell)) return true; 188 | 189 | return false; 190 | }; 191 | 192 | const randomFoodPos = () => { 193 | return [_.random(0, 7), _.random(0, 7)]; 194 | }; 195 | 196 | const setNewFoodPos = () => { 197 | foodPos = randomFoodPos(); 198 | 199 | while (isIntersecting(foodPos, snake.positions)) { 200 | foodPos = randomFoodPos(); 201 | } 202 | }; 203 | 204 | // Setup input callbacks 205 | senseJoystick.getJoystick() 206 | .then((joystick) => { 207 | joystick.on('press', (val) => { 208 | if (val === 'click') { 209 | if (lastDirection === 'stop') { 210 | currentMaze = (currentMaze + 1) % mazeOptions.length; 211 | restartGame(); 212 | } 213 | else { 214 | pauseGame(); 215 | snake.colour = [_.random(40, 255), _.random(40, 255), _.random(40, 255)]; 216 | } 217 | } else { 218 | unpauseGame() 219 | let currentDir = _.last(nextDirection) || lastDirection; 220 | if (val !== currentDir && val !== oppositeDirection(currentDir)) { 221 | nextDirection.push(val); 222 | } 223 | } 224 | }); 225 | }); 226 | 227 | // This function is the brains of our snake game. It will be called periodically 228 | // and update the internal models of the snake and the game, and also will update 229 | // the screen. 230 | const tick = () => { 231 | 232 | // first draw the maze 233 | drawMaze(); 234 | 235 | if ((nextDirection[0] || lastDirection) !== 'stop') { 236 | let newHead = moveHead(snake.positions[0]); 237 | snake.positions = [newHead].concat(snake.positions); 238 | 239 | if (pointEquals(newHead, foodPos)) { 240 | snake.size += 1; 241 | tickDelay -= tickDelayModifier; 242 | startGameLoop() 243 | setNewFoodPos(); 244 | } else { 245 | // If we're not eating 246 | snake.positions.pop(); 247 | } 248 | 249 | // Check that the snake hasn't went off the end of the screen 250 | if (offScreen(newHead) || isIntersecting(newHead, snake.positions.slice(1))) { 251 | // Set the snake back to it's starting position, display a 252 | // cross and then set a timer to restart the game 253 | stopGameLoop(); 254 | 255 | displayCross(); 256 | 257 | setTimeout(() => { 258 | clearScreen(); 259 | senseLeds.showMessage(` ${snake.size - 2} ${snake.size - 2}`, () => { 260 | setTimeout(restartGame, 500); 261 | }); 262 | }, 800); 263 | 264 | return; 265 | } 266 | } 267 | 268 | drawFood(foodPos); 269 | drawSnake(); 270 | senseLeds.setPixels(pixelBuffer); 271 | }; 272 | 273 | const restartGame = () => { 274 | snake.size = 2; 275 | snake.positions = [[4, 4], [4, 5]]; 276 | nextDirection = []; 277 | lastDirection = 'stop'; 278 | tickDelay = tickDelayStart; 279 | setNewFoodPos(); 280 | 281 | startGameLoop(); 282 | }; 283 | 284 | let timerHandle; 285 | const STOPPED = 0; 286 | const PAUSED = 1; 287 | const RUNNING = 2; 288 | let state = STOPPED; 289 | const pauseGame = () => { 290 | if (state === STOPPED) return; 291 | state = PAUSED; 292 | clearInterval(timerHandle); 293 | } 294 | const unpauseGame = () => { 295 | if (state === PAUSED) { 296 | startGameLoop(); 297 | } 298 | } 299 | const startGameLoop = () => { 300 | clearInterval(timerHandle); 301 | timerHandle = setInterval(tick, tickDelay); 302 | state = RUNNING; 303 | } 304 | const stopGameLoop = () => { 305 | state = STOPPED; 306 | clearInterval(timerHandle); 307 | } 308 | 309 | restartGame(); 310 | --------------------------------------------------------------------------------