Created
November 9, 2024 07:04
-
-
Save codepediair/3ea32facf9ebc8ba93e299ed75b71310 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en" dir="ltr"> | |
<head> | |
<meta charset="utf-8"> | |
<title>Snake ASTAR Auto</title> | |
</head> | |
<style> | |
body { | |
text-align: center; | |
font-family: calibri; | |
} | |
</style> | |
<body> | |
<h2>Snake AI with A* Algorithm</h2> | |
<canvas></canvas> | |
<h1>Score: <span id="score">0</span> | Time: <span id="time">0</span>s</h1> | |
<p><b>Mode: <font color="#eb6f92">Automatic (BOT)</font></b></p> | |
<button type="button" onclick="location.reload()">Refresh</button> | |
<button type="button" id="play">Play</button> | |
<button type="button" id="pause" hidden>Pause</button> | |
<script src="snake.js" charset="utf-8"></script> | |
<script type="text/javascript"> | |
var snake = new Game({ | |
size: 400, | |
blockSize: 15, | |
fps: 25, | |
utils: { | |
showGrid: true, | |
distance: true, | |
distanceCount: true, | |
distancePerpendicular: true | |
}, | |
aStar: true | |
}); | |
var playBtn = document.querySelector("#play"); | |
var pauseBtn = document.querySelector("#pause"); | |
playBtn.addEventListener("click", function(){ | |
this.hidden = true; | |
pauseBtn.hidden = false; | |
snake.play(); | |
}); | |
pauseBtn.addEventListener("click", function(){ | |
this.hidden = true; | |
playBtn.hidden = false; | |
snake.pause(); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Utils(params) { | |
var blockSize = params.blockSize; | |
var ctx = document.querySelector("canvas").getContext("2d"); | |
function drawStroke(start, end, color) { | |
ctx.beginPath(); | |
ctx.setLineDash([5, 3]); | |
ctx.moveTo((start[0] * blockSize) + (blockSize / 2), (start[1] * blockSize) + (blockSize / 2)); | |
ctx.lineTo((end[0] * blockSize) + (blockSize / 2), (end[1] * blockSize) + (blockSize / 2)); | |
ctx.strokeStyle = color; | |
ctx.stroke(); | |
} | |
this.distance = (line1, line2, color) => { | |
drawStroke(line1, line2, color); | |
} | |
this.distanceCount = (line1, line2, color) => { | |
let d = Math.sqrt(Math.abs(line1[1] - line2[1]) + Math.abs(line1[0] - line2[0])); | |
ctx.fillStyle = color; | |
ctx.font = "15px Arial"; | |
ctx.fillText(d, line2[0] * blockSize, (line2[1] * blockSize) - 10); | |
} | |
this.distancePerpendicular = (line1, line2, line3, line4, color) => { | |
ctx.fillStyle = color; | |
ctx.font = "15px Arial"; | |
drawStroke(line1, line2, color); | |
ctx.fillText([line2], line2[0] * blockSize, (line2[1] * blockSize) - 30); | |
drawStroke(line3, line4, color); | |
ctx.fillText([line4], line4[0] * blockSize, (line4[1] * blockSize) + 30); | |
} | |
} | |
function Game(params) { | |
if (typeof params.utils === "object") { | |
var utils = params.utils; | |
} else { | |
var utils = { | |
showGrid: false, | |
distance: false, | |
distancePerependicular: false | |
} | |
} | |
var interval, timeInterval; | |
var aStar = params.aStar; // is run AStar true/false | |
var aStarBlock = []; // Astar recomendation for next move [x, y] | |
var aStarBlockIndex = 0; // Index for looping aStar | |
var move = []; // Snake next move [x, y] | |
var ctx = document.querySelector("canvas").getContext("2d"); // Get canvas | |
var size = params.size; // Size of board px | |
var blockSize = params.blockSize; // Size of block px | |
var totalBlock = Math.floor(size / blockSize); // Total rows and cols block | |
var fps = params.fps; // Framerate | |
var snake = [ | |
[1, 0], | |
[0, 0] | |
]; // Snake body | |
var direction = aStar ? false : 39; // Snake direction in keycode | |
var food = []; // Food position [x, y] | |
var scoreEl = document.querySelector("#score"); // HTML display score | |
var score = 0; // Score | |
// Sound Effect | |
var eatedSound, gameOverSound; | |
function setSound() { | |
eatedSound = new Sound("sound/eated.mp3"); | |
gameOverSound = new Sound("sound/gameover.mp3"); | |
} | |
function Sound(src) { | |
this.sound = document.createElement("audio"); | |
this.sound.src = src; | |
this.sound.setAttribute("preload", "auto"); | |
this.sound.setAttribute("controls", "none"); | |
this.sound.style.display = "none"; | |
document.body.appendChild(this.sound); | |
this.play = function() { | |
this.sound.play(); | |
} | |
this.stop = function() { | |
this.sound.pause(); | |
} | |
} | |
function gameOver() { | |
clearInterval(interval); | |
clearInterval(timeInterval); | |
gameOverSound.play(); | |
} | |
function drawBoard() { | |
ctx.canvas.width = ctx.canvas.height = size; | |
ctx.canvas.style.border = "1px solid #6e6a86"; | |
} | |
function drawGrid() { | |
for (var x = 0; x < totalBlock; x++) { | |
for (var y = 0; y < totalBlock; y++) { | |
ctx.strokeStyle = "#6e6a86"; | |
ctx.strokeRect(x * blockSize, y * blockSize, blockSize, blockSize); | |
} | |
} | |
} | |
function drawRect(x, y, color) { | |
ctx.fillStyle = color; | |
ctx.fillRect(x, y, blockSize, blockSize); | |
} | |
function drawSnake() { | |
for (var i = 1; i < snake.length; i++) { | |
ctx.strokeStyle = "#6e6a86"; | |
ctx.strokeRect(snake[i][0] * blockSize, snake[i][1] * blockSize, blockSize, blockSize); | |
drawRect(snake[i][0] * blockSize, snake[i][1] * blockSize, "#ebbcba"); | |
} | |
drawRect(snake[0][0] * blockSize, snake[0][1] * blockSize, "#31748f"); | |
} | |
function randomFood() { | |
food = [ | |
Math.floor(Math.random() * totalBlock), | |
Math.floor(Math.random() * totalBlock) | |
]; | |
snake.map((x) => { | |
if (JSON.stringify(x) === JSON.stringify(food)) { | |
randomFood(); | |
} | |
}); | |
} | |
function drawFood() { | |
drawRect(food[0] * blockSize, food[1] * blockSize, "#eb6f92"); | |
} | |
function eated() { | |
eatedSound.play(); | |
score += 1; | |
scoreEl.innerHTML = score; | |
snake.push(food); | |
} | |
function snakeMove() { | |
document.onkeydown = function(e) { | |
if (direction - e.keyCode !== 2 && direction - e.keyCode !== -2) { | |
direction = e.keyCode; | |
} | |
} | |
move = [snake[0][0], snake[0][1]]; | |
if (aStar) { | |
if (aStarBlock[0] - move[1] == 1) | |
direction = 40; | |
else if (aStarBlock[0] - move[1] == -1) | |
direction = 38; | |
else if (aStarBlock[1] - move[0] == 1) | |
direction = 39; | |
else if (aStarBlock[1] - move[0] == -1) | |
direction = 37; | |
} | |
switch (direction) { | |
case 37: move[0] -= 1; break; | |
case 38: move[1] -= 1; break; | |
case 39: move[0] += 1; break; | |
case 40: move[1] += 1; break; | |
} | |
if (move[0] > (totalBlock - 1) || move[1] > (totalBlock - 1) || move[0] < 0 || move[1] < 0) { | |
gameOver(); | |
} | |
snake.map((x) => { | |
if (JSON.stringify(move) === JSON.stringify(x)) { | |
gameOver(); | |
} | |
}); | |
if (JSON.stringify(move) === JSON.stringify(food)) { | |
eated(); | |
randomFood(); | |
} | |
snake.unshift(move); | |
snake.pop(); | |
} | |
function runAStar(snakePos) { | |
var board = []; | |
for (var i = 0; i < totalBlock; i++) { | |
board.push([]); | |
} | |
for (var i = 0; i < totalBlock; i++) { | |
for (var j = board[i].length; j < totalBlock; j++) { | |
board[i].push(1); | |
} | |
} | |
for (var i = 0; i < snake.length; i++) { | |
board[snake[i][1]][[snake[i][0]]] = 0; | |
} | |
board = new Graph(board); | |
var start = board.grid[snakePos[1]][snakePos[0]]; | |
var end = board.grid[food[1]][food[0]]; | |
var result = astar.search(board, start, end); | |
if (board.length == 0) { | |
} | |
aStarBlock = result.length > 0 ? aStarBlock = [result[0].x, result[0].y] : [0, 0]; | |
} | |
// INIT | |
setSound(); | |
utils.showGrid && drawGrid(); | |
drawSnake(); | |
randomFood(); | |
drawFood(); | |
runAStar(snake[0]); | |
var utilities = new Utils({ | |
blockSize: blockSize | |
}); // Show utilites | |
function update() { | |
snakeMove(); | |
drawBoard(); | |
utils.showGrid && drawGrid(); | |
drawSnake(); | |
drawFood(); | |
utils.distance && utilities.distance(snake[0], food, "#e0def4"); | |
utils.distanceCount && utilities.distanceCount(snake[0], food, "#e0def4"); | |
utils.distancePerpendicular && utilities.distancePerpendicular(snake[0], [food[0], snake[0][1]], snake[0], [snake[0][0], food[1]], "#e0def4"); | |
aStar && runAStar(move); | |
} | |
this.play = () => { | |
interval = setInterval(update, 1000 / fps); | |
timeInterval = setInterval(function(){ | |
var timeEl = document.querySelector("#time"); | |
var time = (+timeEl.innerHTML+1); | |
timeEl.innerHTML = time; | |
}, 1000); | |
} | |
this.pause = () => { | |
clearInterval(interval); | |
clearInterval(timeInterval); | |
} | |
} | |
// ASTAR ALGORITM | |
(function(definition) { | |
if (typeof module === 'object' && typeof module.exports === 'object') { | |
module.exports = definition(); | |
} else if (typeof define === 'function' && define.amd) { | |
define([], definition); | |
} else { | |
var exports = definition(); | |
window.astar = exports.astar; | |
window.Graph = exports.Graph; | |
} | |
})(function() { | |
function pathTo(node) { | |
var curr = node; | |
var path = []; | |
while (curr.parent) { | |
path.unshift(curr); | |
curr = curr.parent; | |
} | |
return path; | |
} | |
function getHeap() { | |
return new BinaryHeap(function(node) { | |
return node.f; | |
}); | |
} | |
var astar = { | |
search: function(graph, start, end, options) { | |
graph.cleanDirty(); | |
options = options || {}; | |
var heuristic = options.heuristic || astar.heuristics.manhattan, | |
closest = options.closest || false; | |
var openHeap = getHeap(), | |
closestNode = start; | |
var closedList = []; | |
start.h = heuristic(start, end); | |
openHeap.push(start); | |
while (openHeap.size() > 0) { | |
var currentNode = openHeap.pop(); | |
if (currentNode === end) { | |
while (closedList.length > 0) closedList.pop().closed = false; | |
return pathTo(currentNode); | |
} | |
currentNode.closed = true; | |
closedList.push(currentNode); | |
var neighbors = graph.neighbors(currentNode); | |
for (var i = 0, il = neighbors.length; i < il; ++i) { | |
var neighbor = neighbors[i]; | |
if (neighbor.closed || neighbor.isWall()) { | |
continue; | |
} | |
var gScore = currentNode.g + neighbor.getCost(currentNode), | |
beenVisited = neighbor.visited; | |
if (!beenVisited || gScore < neighbor.g) { | |
neighbor.visited = true; | |
neighbor.parent = currentNode; | |
neighbor.h = neighbor.h || heuristic(neighbor, end); | |
neighbor.g = gScore; | |
neighbor.f = neighbor.g + neighbor.h; | |
graph.markDirty(neighbor); | |
if (closest) { | |
if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) { | |
closestNode = neighbor; | |
} | |
} | |
if (!beenVisited) { | |
openHeap.push(neighbor); | |
} else { | |
openHeap.rescoreElement(neighbor); | |
} | |
} | |
} | |
} | |
while (closedList.length > 0) closedList.pop().closed = false; | |
if (closest) { | |
return pathTo(closestNode); | |
} | |
return []; | |
}, | |
heuristics: { | |
manhattan: function(pos0, pos1) { | |
var d1 = Math.abs(pos1.x - pos0.x); | |
var d2 = Math.abs(pos1.y - pos0.y); | |
return d1 + d2; | |
}, | |
diagonal: function(pos0, pos1) { | |
var D = 1; | |
var D2 = Math.sqrt(2); | |
var d1 = Math.abs(pos1.x - pos0.x); | |
var d2 = Math.abs(pos1.y - pos0.y); | |
return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2)); | |
} | |
}, | |
cleanNode: function(node) { | |
node.f = 0; | |
node.g = 0; | |
node.h = 0; | |
node.visited = false; | |
node.closed = false; | |
node.parent = null; | |
} | |
}; | |
function Graph(gridIn, options) { | |
options = options || {}; | |
this.nodes = []; | |
this.diagonal = !!options.diagonal; | |
this.grid = []; | |
for (var x = 0; x < gridIn.length; x++) { | |
this.grid[x] = []; | |
for (var y = 0, row = gridIn[x]; y < row.length; y++) { | |
var node = new GridNode(x, y, row[y]); | |
this.grid[x][y] = node; | |
this.nodes.push(node); | |
} | |
} | |
this.init(); | |
} | |
Graph.prototype.init = function() { | |
this.dirtyNodes = []; | |
for (var i = 0; i < this.nodes.length; i++) { | |
astar.cleanNode(this.nodes[i]); | |
} | |
}; | |
Graph.prototype.cleanDirty = function() { | |
for (var i = 0; i < this.dirtyNodes.length; i++) { | |
astar.cleanNode(this.dirtyNodes[i]); | |
} | |
this.dirtyNodes = []; | |
}; | |
Graph.prototype.markDirty = function(node) { | |
this.dirtyNodes.push(node); | |
}; | |
Graph.prototype.neighbors = function(node) { | |
var ret = []; | |
var x = node.x; | |
var y = node.y; | |
var grid = this.grid; | |
if (grid[x - 1] && grid[x - 1][y]) { | |
ret.push(grid[x - 1][y]); | |
} | |
if (grid[x + 1] && grid[x + 1][y]) { | |
ret.push(grid[x + 1][y]); | |
} | |
if (grid[x] && grid[x][y - 1]) { | |
ret.push(grid[x][y - 1]); | |
} | |
if (grid[x] && grid[x][y + 1]) { | |
ret.push(grid[x][y + 1]); | |
} | |
if (this.diagonal) { | |
if (grid[x - 1] && grid[x - 1][y - 1]) { | |
ret.push(grid[x - 1][y - 1]); | |
} | |
if (grid[x + 1] && grid[x + 1][y - 1]) { | |
ret.push(grid[x + 1][y - 1]); | |
} | |
if (grid[x - 1] && grid[x - 1][y + 1]) { | |
ret.push(grid[x - 1][y + 1]); | |
} | |
if (grid[x + 1] && grid[x + 1][y + 1]) { | |
ret.push(grid[x + 1][y + 1]); | |
} | |
} | |
return ret; | |
}; | |
Graph.prototype.toString = function() { | |
var graphString = []; | |
var nodes = this.grid; | |
for (var x = 0; x < nodes.length; x++) { | |
var rowDebug = []; | |
var row = nodes[x]; | |
for (var y = 0; y < row.length; y++) { | |
rowDebug.push(row[y].weight); | |
} | |
graphString.push(rowDebug.join(" ")); | |
} | |
return graphString.join("\n"); | |
}; | |
function GridNode(x, y, weight) { | |
this.x = x; | |
this.y = y; | |
this.weight = weight; | |
} | |
GridNode.prototype.toString = function() { | |
return "[" + this.x + " " + this.y + "]"; | |
}; | |
GridNode.prototype.getCost = function(fromNeighbor) { | |
if (fromNeighbor && fromNeighbor.x != this.x && fromNeighbor.y != this.y) { | |
return this.weight * 1.41421; | |
} | |
return this.weight; | |
}; | |
GridNode.prototype.isWall = function() { | |
return this.weight === 0; | |
}; | |
function BinaryHeap(scoreFunction) { | |
this.content = []; | |
this.scoreFunction = scoreFunction; | |
} | |
BinaryHeap.prototype = { | |
push: function(element) { | |
this.content.push(element); | |
this.sinkDown(this.content.length - 1); | |
}, | |
pop: function() { | |
var result = this.content[0]; | |
var end = this.content.pop(); | |
if (this.content.length > 0) { | |
this.content[0] = end; | |
this.bubbleUp(0); | |
} | |
return result; | |
}, | |
remove: function(node) { | |
var i = this.content.indexOf(node); | |
var end = this.content.pop(); | |
if (i !== this.content.length - 1) { | |
this.content[i] = end; | |
if (this.scoreFunction(end) < this.scoreFunction(node)) { | |
this.sinkDown(i); | |
} else { | |
this.bubbleUp(i); | |
} | |
} | |
}, | |
size: function() { | |
return this.content.length; | |
}, | |
rescoreElement: function(node) { | |
this.sinkDown(this.content.indexOf(node)); | |
}, | |
sinkDown: function(n) { | |
var element = this.content[n]; | |
while (n > 0) { | |
var parentN = ((n + 1) >> 1) - 1; | |
var parent = this.content[parentN]; | |
if (this.scoreFunction(element) < this.scoreFunction(parent)) { | |
this.content[parentN] = element; | |
this.content[n] = parent; | |
n = parentN; | |
} | |
else { | |
break; | |
} | |
} | |
}, | |
bubbleUp: function(n) { | |
var length = this.content.length; | |
var element = this.content[n]; | |
var elemScore = this.scoreFunction(element); | |
while (true) { | |
var child2N = (n + 1) << 1; | |
var child1N = child2N - 1; | |
var swap = null; | |
var child1Score; | |
if (child1N < length) { | |
var child1 = this.content[child1N]; | |
child1Score = this.scoreFunction(child1); | |
if (child1Score < elemScore) { | |
swap = child1N; | |
} | |
} | |
if (child2N < length) { | |
var child2 = this.content[child2N]; | |
var child2Score = this.scoreFunction(child2); | |
if (child2Score < (swap === null ? elemScore : child1Score)) { | |
swap = child2N; | |
} | |
} | |
if (swap !== null) { | |
this.content[n] = this.content[swap]; | |
this.content[swap] = element; | |
n = swap; | |
} | |
else { | |
break; | |
} | |
} | |
} | |
}; | |
return { | |
astar: astar, | |
Graph: Graph | |
}; | |
}); | |
// END ASTAR ALGORITM |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment