diff --git a/index.html b/index.html index e7b82cb..a72e5af 100644 --- a/index.html +++ b/index.html @@ -26,6 +26,12 @@

Right Paddle Height:

View Code +
+

Snake

+ + + View Code +
diff --git a/js/snake.js b/js/snake.js new file mode 100644 index 0000000..94815e2 --- /dev/null +++ b/js/snake.js @@ -0,0 +1,220 @@ +/* + bits: + 0 = snake + 1 = food + 2 = checked (for searching algorithms) +*/ + +// Bit functions for ease +function isBitSet(num, bit){ + return ((num >> bit) % 2 != 0) +} + +function bitSet(num, bit){ + return num | 1 << bit; +} + +function bitClear(num, bit){ + return num & ~(1 << bit); +} + +class OrderedPair { + constructor(x, y) { + this.x = x; + this.y = y; + } +} + +class SnakeCore { + constructor() { + // TODO: Add CSS stuff for page + this.canvas = document.getElementById('snake'); + this.context = this.canvas.getContext('2d'); + this.grid = 25; // size of grid squares + this.speed = 10; // speed in ms + this.width = 25; + this.height = 15; + this.board = []; + for (let i = 0; i < this.height; i++) { + this.board.push(new Array(this.width)); + for (let j = 0; j < this.width; j++) { + this.board[i][j] = 0; + } + } + this.body = []; + this.gameover = false; + this.foodAte = true; + this.body.push(new OrderedPair(12, 8)); + this.board[8][12] = 1; + } + + reset() { + snake.gameover = false; + snake.foodAte = true; + for (let i = 0; i < this.height; i++) { + for (let j = 0; j < this.width; j++) { + this.board[i][j] = 0; + } + } + } + + foodRegen() { + if (!this.foodAte) + return; + this.foodAte = false; + while (true) { + let tmp = new OrderedPair(Math.floor(Math.random() * this.width), Math.floor(Math.random() * this.height)); + if (isBitSet(this.board[tmp.y][tmp.x], 0)) + continue; + this.board[tmp.y][tmp.x] = bitSet(this.board[tmp.y][tmp.x], 1); + return; + } + } +} + +class Bot { + constructor() { + this.pathUntrimmed = []; + this.path = []; + } + + bfs() { + var search = [snake.body.at(0)]; + while (search.length !== 0) { + let current = search.pop(); + if (isBitSet(snake.board[current.y][current.x], 2)) + continue; + this.pathUntrimmed.push(current); + let locals = []; + for (var i = 0; i < 4; i++) + locals[i] = new OrderedPair(current.x, current.y); + locals[0].y += 1; + locals[1].x += 1; + locals[2].y -= 1; + locals[3].x -= 1; + for (const local of locals) { + if (local.x < 0 || local.x > snake.width - 1 || local.y < 0 || local.y > snake.height - 1) { + continue; + } + let value = snake.board[local.y][local.x]; + if (isBitSet(value, 1)) { + this.pathUntrimmed.push(local); + return; + } + if (isBitSet(value, 2)) + continue; + if (isBitSet(value, 0)) + continue; + search.push(local); + } + snake.board[current.y][current.x] = bitSet(snake.board[current.y][current.x], 2); + } + } + + // TODO: Fix trim function + trim() { + let reachedSnake = false; + this.path.push(this.pathUntrimmed.pop()); // Push food location + while (this.pathUntrimmed.length !== 0) { + if (!reachedSnake) { + let location = this.pathUntrimmed.shift(); + if (isBitSet(snake.board[location.y][location.x], 0)) + reachedSnake = true; + var delta = new OrderedPair; + let connection = this.path.at(0); + delta.x = location.x - connection.x; + delta.y = location.y - connection.y; + if ((Math.abs(delta.x) + Math.abs(delta.y)) === 1) + this.path.push(location); + } else + this.pathUntrimmed.shift(); + } + } + + unvisit() { + for (let i = 0; i < snake.height; i++) { + for (let j = 0; j < snake.width; j++) { + snake.board[i][j] = bitClear(snake.board[i][j], 2); + } + } + } + + /* + values: + 0 = left + 1 = up + 2 = right + 4 = down + */ + autoplay() { + if (this.path.length !== 0) + return; + this.bfs(); + //this.trim(); + this.unvisit(); + /* + let delta = this.headPos - this.path.pop(); + if (delta.x > 0) + return 0; + if (delta.y > 0) + return 1; + if (delta.x < 0) + return 2; + if (delta.y < 0) + return 4; + return 0; // Safety guard if above doesn't return + */ + } +} + +const snake = new SnakeCore(); // Singleton for snake game +const bot = new Bot(); // Singleton for bot playing + +function snakeloop() { + snake.context.clearRect(0, 0, snake.canvas.width, snake.canvas.height); + // Reset of needed + if (snake.gameover) + snake.reset(); + // Input + bot.autoplay(); + + // Move snake + // TODO: Fix teleporting snake + let next = bot.pathUntrimmed.shift(); + + if (next.x < 0 || next.x > snake.width || next.y < 0 || next.y > snake.height) + snake.gameover = true; + if (isBitSet(snake.board[next.y][next.x], 0) && (snake.body.length > 1)) + snake.gameover = true; // Game should end (Snake touching snake) + + snake.board[next.y][next.x] = bitSet(snake.board[next.y][next.x], 0); + snake.body.push(next); + if (!isBitSet(snake.board[next.y][next.x], 1)) { + let old = snake.body.shift(); + snake.board[old.y][old.x] = bitClear(snake.board[old.y][old.x], 0); + } else { + snake.board[next.y][next.x] = bitClear(snake.board[next.y][next.x], 1); + snake.foodAte = true; + while (bot.path.length > 0) + bot.path.pop(); + } + + // Regenerate food if needed + snake.foodRegen(); + + // Draw game + for (let i = 0; i < snake.height; i++) { + for (let j = 0; j < snake.width; j++) { + if (isBitSet(snake.board[i][j], 0)) + snake.context.fillStyle = "green"; + else if (isBitSet(snake.board[i][j], 1)) + snake.context.fillStyle = "red"; + else + snake.context.fillStyle = "black"; + snake.context.fillRect(j * snake.grid, i * snake.grid, snake.grid, snake.grid); + } + } +} + +// start the game +setInterval(snakeloop, snake.speed);