xxxxxxxxxx
573
// Define variables for the Snake game
// make more food for snakes
let snakes = [];
let savedSnakes = [];
let population = 200;
let foodAmount = 5;
let gridSize = 40;
let trainingSpeed = 1;
let generations = 1;
let displayButton, trainingSpeedSlider;
let toggleDisply
let displayAll = true;
function toggle(){
displayAll = !displayAll;
}
function setup() {
createCanvas(400, 400);
frameRate(10); // Adjust the frame rate as needed
for(let i = 0; i < population; i++){
append(snakes, new Snake());
}
displayButton = createButton('Display all snakes');
displayButton.position(10,height+10);
displayButton.mousePressed(toggle);
trainingSpeedSlider = createSlider(1,500,1)
trainingSpeedSlider.position(10,height+30)
}
function draw() {
//background(0,0,0,5);
background(100);
for(let j = 0; j < trainingSpeedSlider.value(); j++){
for(let i = snakes.length -1; i >= 0; i--){
let snake = snakes[i];
// Check for collisions
if(snake.checkCollision() || snake.timeSinceFood > 500){
savedSnakes.push(snakes.splice(i, 1)[0]);
}
// Update and display the snake
closeFood = snake.findCloseFood();
snake.think(closeFood);
snake.update(closeFood);
if(displayAll){
snake.display(closeFood);
}
snake.eat(closeFood);
}
if(snakes.length == 0){
nextGeneration();
}
if(!displayAll){
let recordHolder = 0;
let r = 0;
for(let i = snakes.length -1; i >= 0; i--){
if(snakes[i].score > r){
r = snakes[i].score;
recordHolder = i;
}
}
closeFood = snakes[recordHolder].findCloseFood();
snakes[recordHolder].display(closeFood);
}
}
// //Display score
// fill(255);
// textSize(16);
// text(snake.body.length, 10, 20);
}
// function restart() {
// for(let snake of snakes){
// snake.body = [];
// snake.body[0] = createVector(floor(width / 2), floor(height / 2));
// snake.xDirection = 0;
// snake.yDirection = -1;
// }
// }
function nextGeneration() {
console.log('next generation ' + generations + ' complete.');
generations++;
calculateFitness();
for (let i = 0; i < population; i++) {
snakes[i] = pickOne();
}
savedBirds = [];
}
function pickOne() {
let index = 0;
let r = random(1);
while (r > 0) {
r = r - savedSnakes[index].fitness;
index++;
}
index--;
let snake = savedSnakes[index];
let child = new Snake(snake.brain);
child.mutate();
return child;
}
function calculateFitness() {
let sum = 0;
for (let snake of savedSnakes) {
// sum += snake.score * (snake.body.length-1/2);
sum += snake.score;
}
for (let snake of savedSnakes) {
// snake.fitness = snake.score * (snake.body.length-1/2) / sum;
snake.fitness = snake.score / sum;
}
}
// function keyPressed() {
// //up
// if (keyCode === 38) {
// snake.turnUp();
// }
// //down
// else if (keyCode === 40) {
// snake.turnDown();
// }
// //left
// else if (keyCode === 37) {
// snake.turnLeft();
// }
// //right
// else if (keyCode === 39) {
// snake.turnRight();
// }
// }
function createFood(snake) {
let foodPos;
let isFoodOnSnake = true;
while (isFoodOnSnake) {
const cols = floor(width / gridSize);
const rows = floor(height / gridSize);
const foodX = floor(random(cols)) * gridSize;
const foodY = floor(random(rows)) * gridSize;
// Check if the food is not on the snake's body
isFoodOnSnake = false;
for (let i = 0; i < snake.body.length; i++) {
const segment = snake.body[i];
if (foodX === segment.x && foodY === segment.y) {
isFoodOnSnake = true;
break;
}
}
if (!isFoodOnSnake) {
foodPos = createVector(foodX, foodY);
}
}
return foodPos;
}
function distancesAroundHead(snake) {
const head = snake.body[snake.body.length - 1]; // Snake's head position
const direction = createVector(snake.xDirection, snake.yDirection); // Direction vector
// Initialize variables
distances = {
ahead: 0,
left: 0,
right: 0,
wallA: 0,
bodyA: 0,
foodA: 0, // Initialize food distance ahead to 0
wallL: 0,
bodyL: 0,
foodL: 0, // Initialize food distance left to 0
wallR: 0,
bodyR: 0,
foodR: 0, // Initialize food distance right to 0
};
let currentPos = head.copy();
// Calculate the distance straight ahead
let foodFoundAhead = false; // Flag to detect food ahead
while (true) {
// Calculate the next position
currentPos.add(direction);
// Check if the next position is out of bounds (wall collision)
if (
currentPos.x < 0 ||
currentPos.x >= width ||
currentPos.y < 0 ||
currentPos.y >= height
) {
distances.wallA++;
break; // Hit a wall, stop
}
// Check if the next position is within the snake's body (self-collision)
for (let i = 0; i < snake.body.length; i++) {
const segment = snake.body[i];
if (currentPos.x === segment.x && currentPos.y === segment.y) {
distances.bodyA++;
break; // Hit the snake's own body, stop
}
}
// Check if the next position contains food
if (currentPos.x === snake.food.x && currentPos.y === snake.food.y) {
distances.foodA = 1; // Food found ahead
foodFoundAhead = true;
break; // Stop checking, we found food
}
// Increment the distance
distances.ahead++;
}
// Calculate the distances to the left and right
const leftDirections = [createVector(-1, 0), createVector(0, -1), createVector(1, 0), createVector(0, 1)]; // Left turns
const rightDirections = [createVector(1, 0), createVector(0, 1), createVector(-1, 0), createVector(0, -1)]; // Right turns
for (let i = 0; i < 4; i++) {
// Initialize variables for left and right directions
let leftPos = head.copy();
let rightPos = head.copy();
// Calculate distances to the left and right
while (true) {
// Calculate the next positions
leftPos.add(leftDirections[i]);
rightPos.add(rightDirections[i]);
// Check if the next positions are out of bounds (wall collision)
if (
leftPos.x < 0 ||
leftPos.x >= width ||
leftPos.y < 0 ||
leftPos.y >= height
) {
distances['wall' + (i === 0 ? 'L' : 'R')]++;
break; // Hit a wall, stop
}
if (
rightPos.x < 0 ||
rightPos.x >= width ||
rightPos.y < 0 ||
rightPos.y >= height
) {
distances['wall' + (i === 0 ? 'R' : 'L')]++;
break; // Hit a wall, stop
}
// Check if the next positions are within the snake's body (self-collision)
for (let j = 0; j < snake.body.length; j++) {
const segment = snake.body[j];
if (leftPos.x === segment.x && leftPos.y === segment.y) {
distances['body' + (i === 0 ? 'L' : 'R')]++;
break; // Hit the snake's own body, stop
}
if (rightPos.x === segment.x && rightPos.y === segment.y) {
distances['body' + (i === 0 ? 'R' : 'L')]++;
break; // Hit the snake's own body, stop
}
}
// Check if the next positions contain food
if (leftPos.x === snake.food.x && leftPos.y === snake.food.y) {
distances['food' + (i === 0 ? 'L' : 'R')] = 1; // Food found in the specified direction
break; // Stop checking, we found food
}
if (rightPos.x === snake.food.x && rightPos.y === snake.food.y) {
distances['food' + (i === 0 ? 'R' : 'L')] = 1; // Food found in the specified direction
break; // Stop checking, we found food
}
}
}
return distances;
}
class Snake {
constructor(brain) {
this.body = [];
this.body[0] = createVector(floor(width / 2), floor(height / 2));
this.xDirection = 1;
this.yDirection = 0;
this.food = [];
for (let i = 0; i <= foodAmount; i++){
append(this.food,createFood(this));
}
this.score = 0;
this.fitness = 0;
this.timeSinceFood = 0;
//(input, hidden, output)
//input head position x and y, snake velocity x and y, food location x and y. Need to add body
//output up, down, left, right
if(brain){
this.brain = brain.copy();
}else{
this.brain = new NeuralNetwork(19, 15, 4);
}
}
findCloseFood() {
let record = 9999;
let recordHolder;
let head = this.body[0];
for(let food of this.food){
let d = dist(food.x,food.y,head.x,head.y);
if(d < record){
record = d;
recordHolder = food;
}
}
return recordHolder;
}
think(food) {
const head = this.body[this.body.length - 1].copy();
const currentXDir = this.xDirection;
const currentYDir = this.yDirection;
// Calculate the inputs for the neural network
let inputs = [];
// inputs[0] = head.x / width;
// inputs[1] = head.y / height;
// inputs[2] = currentXDir;
// inputs[3] = currentYDir;
// inputs[4] = this.food.x / width;
// inputs[5] = this.food.y / height;
// let distances = {
// ahead: 0,
// left: 0,
// right: 0,
// wallA: 0,
// bodyA: 0,
// foodA: 0,
// wallL: 0,
// bodyL: 0,
// foodL: 0,
// wallR: 0,
// bodyR: 0,
// foodR: 0,
// };
distances = distancesAroundHead(this)
inputs[0] = distances.ahead
inputs[1] = distances.left
inputs[2] = distances.right
inputs[3] = distances.wallA
inputs[4] = distances.bodyA
inputs[5] = distances.foodA
inputs[6] = distances.wallL
inputs[7] = distances.bodyL
inputs[8] = distances.foodL
inputs[9] = distances.wallR
inputs[10] = distances.bodyR
inputs[11] = distances.foodR
inputs[12] = this.timeSinceFood;
inputs[13] = currentXDir
inputs[14] = currentYDir
inputs[15] = head.x
inputs[16] = head.y
// maybe make even more food here
inputs[17] = food.x - head.x;
inputs[18] = food.y - head.y;
//inputs[17] = this.body.length
// Get the output from the neural network
let output = this.brain.predict(inputs);
// Determine the new direction based on the neural network's output
let newDirectionX = currentXDir;
let newDirectionY = currentYDir;
if (output[0] > output[1] && output[0] > output[2] && output[0] > output[3]) {
// Keep the current direction
newDirectionX = (currentXDir === 0) ? -1 : currentXDir; // Prevent reversing
newDirectionY = 0;
} else if (output[1] > output[0] && output[1] > output[2] && output[1] > output[3]) {
// Change direction to right
newDirectionX = (currentXDir === 0) ? 1 : currentXDir; // Prevent reversing
newDirectionY = 0;
} else if (output[2] > output[0] && output[2] > output[1] && output[2] > output[3]) {
// Change direction to up
newDirectionX = 0;
newDirectionY = (currentYDir === 0) ? -1 : currentYDir; // Prevent reversing
} else if (output[3] > output[0] && output[3] > output[1] && output[3] > output[2]) {
// Change direction to down
newDirectionX = 0;
newDirectionY = (currentYDir === 0) ? 1 : currentYDir; // Prevent reversing
}
// Set the new direction
if(random(100) < 0.001){
newDirectionX *= random(1) < 0.5 ? -1 : 0
newDirectionY *= random(1) < 0.5 ? -1 : 0
}
this.setDirection(newDirectionX, newDirectionY);
}
mutate() {
this.brain.mutate(0.3);
}
turnLeft(){
if(this.xDirection != 1){
//if(this.xDirection != 1 && this.yDireciton != 0){
this.setDirection(-1, 0);
//}
}
}
turnRight(){
if(this.xDirection != -1){
this.setDirection(1, 0);
}
//if(this.xDirection != -1 && this.yDireciton != 0){
//}
}
turnUp(){
//if(this.xDirection != 0 && this.yDireciton != 1){
if(this.yDirection != 1){
this.setDirection(0, -1);
}
//}
}
turnDown(){
//if(this.xDirection != 0 && this.yDireciton != -1){
if(this.yDirection != -1){
this.setDirection(0, 1);
}
//}
}
setDirection(x, y) {
this.xDirection = x;
this.yDirection = y;
}
update(food) {
const head = this.body[this.body.length - 1].copy();
// reward for getting close to the food
//let d = dist(head.x,head.y,this.food.x,this.food.y)
// this.score += d*(this.body.length-1);
// lets try mapping the reward to zero as time goes on
let d = 1/log(this.timeSinceFood + 2) * dist(head.x,head.y,food.x,food.y) * 0.2 *this.body.length;
this.score += d*(this.body.length-1);
this.timeSinceFood ++;
this.body.shift(); // Remove the tail
head.x += this.xDirection * gridSize;
head.y += this.yDirection * gridSize;
this.body.push(head); // Add a new head
}
grow() {
const head = this.body[this.body.length - 1].copy();
this.body.push(head);
}
eat(food) {
let f = food;
const head = this.body[this.body.length - 1];
if (head.x === f.x && head.y === f.y) {
this.grow();
append(this.food,createFood(this));
this.timeSinceFood = 0;
this.score += 400;
return true;
}
return false;
}
checkCollision() {
const head = this.body[this.body.length - 1];
for (let i = 0; i < this.body.length - 2; i++) {
const segment = this.body[i];
if (head.x === segment.x && head.y === segment.y) {
// Snake collided with itself
return true;
}
}
if (head.x >= width || head.x < 0 || head.y >= height || head.y < 0) {
//console.log('Game Over - Wall Collision');
//restart();
this.score -= 5
return true;
}
}
display(food) {
fill(255, 10, 10);
// Display the food
rect(food.x, food.y, gridSize, gridSize);
// Color the head
//-todo: just make some eyes
fill(0,255,255);
noStroke();
rect(this.body[this.body.length - 1].x, this.body[this.body.length - 1].y, gridSize, gridSize);
// Color the body of the snake
for (let i = 0; i < this.body.length -1; i++) {
fill(0, 100, 0);
noStroke();
rect(this.body[i].x,this.body[i].y,gridSize,gridSize)
//rect(this.body[i].x + 10*noise(this.body[i].x,this.body[i].y,frameCount), this.body[i].y + 10*noise(this.body[i].x,this.body[i].y,frameCount), gridSize, gridSize);
}
}
}