//Define 10x20 grid as the board
var grid = [
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
];
//Block shapes
var shapes = {
I: [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]],
J: [[2,0,0], [2,2,2], [0,0,0]],
L: [[0,0,3], [3,3,3], [0,0,0]],
O: [[4,4], [4,4]],
S: [[0,5,5], [5,5,0], [0,0,0]],
T: [[0,6,0], [6,6,6], [0,0,0]],
Z: [[7,7,0], [0,7,7], [0,0,0]]
};
//Block colors
var colors = ["F92338", "C973FF", "1C76BC", "FEE356", "53D504", "36E0FF", "F8931D"];
//Used to help create a seeded generated random number for choosing shapes. makes results deterministic (reproducible) for debugging
var rndSeed = 1;
//BLOCK SHAPES
//coordinates and shape parameter of current block we can update
var currentShape = {x: 0, y: 0, shape: undefined};
//store shape of upcoming block
var upcomingShape;
//stores shapes
var bag = [];
//index for shapes in the bag
var bagIndex = 0;
//GAME VALUES
//Game score
var score = 0;
// game speed
var speed = 500;
// boolean for changing game speed
var changeSpeed = false;
//for storing current state, we can load later
var saveState;
//stores current game state
var roundState;
//list of available game speeds
var speeds = [500,100,1,0];
//inded in game speed array
var speedIndex = 0;
//turn ai on or off
var ai = true;
//drawing game vs updating algorithms
var draw = true;
//how many so far?
var movesTaken = 0;
//max number of moves allowed in a generation
var moveLimit = 500;
//consists of move the 7 move parameters
var moveAlgorithm = {};
//set to highest rate move
var inspectMoveSelection = false;
//GENETIC ALGORITHM VALUES
//stores number of genomes, init at 50
var populationSize = 50;
//stores genomes
var genomes = [];
//index of current genome in genomes array
var currentGenome = -1;
//generation number
var generation = 0;
//stores values for a generation
var archive = {
populationSize: 0,
currentGeneration: 0,
elites: [],
genomes: []
};
//rate of mutation
var mutationRate = 0.05;
//helps calculate mutation
var mutationStep = 0.2;
//main function, called on load
function initialize() {
//init pop size
archive.populationSize = populationSize;
//get the next available shape from the bag
nextShape();
//applies the shape to the grid
applyShape();
//set both save state and current state from the game
saveState = getState();
roundState = getState();
//create an initial population of genomes
createInitialPopulation();
//the game loop
var loop = function(){
//boolean for changing game speed
if (changeSpeed) {
//restart the clock
//stop time
clearInterval(interval);
//set time, like a digital watch
interval = setInterval(loop, speed);
//and don't change it
changeInterval = false;
}
if (speed === 0) {
//no need to draw on screen elements
draw = false;
//updates the game (update fitness, make a move, evaluate next move)
update();
update();
update();
} else {
//draw the elements
draw = true;
}
//update regardless
update();
if (speed === 0) {
//now draw elements
draw = true;
//now update the score
updateScore();
}
};
//timer interval
var interval = setInterval(loop, speed);
}
document.onLoad = initialize();
//key options
window.onkeydown = function (event) {
var characterPressed = String.fromCharCode(event.keyCode);
if (event.keyCode == 38) {
rotateShape();
} else if (event.keyCode == 40) {
moveDown();
} else if (event.keyCode == 37) {
moveLeft();
} else if (event.keyCode == 39) {
moveRight();
} else if (shapes[characterPressed.toUpperCase()] !== undefined) {
removeShape();
currentShape.shape = shapes[characterPressed.toUpperCase()];
applyShape();
} else if (characterPressed.toUpperCase() == "Q") {
saveState = getState();
} else if (characterPressed.toUpperCase() == "W") {
loadState(saveState);
} else if (characterPressed.toUpperCase() == "D") {
//slow down
speedIndex--;
if (speedIndex < 0) {
speedIndex = speeds.length - 1;
}
speed = speeds[speedIndex];
changeSpeed = true;
} else if (characterPressed.toUpperCase() == "E") {
//speed up
speedIndex++;
if (speedIndex >= speeds.length) {
speedIndex = 0;
}
//adjust speed index
speed = speeds[speedIndex];
changeSpeed = true;
//Turn on/off AI
} else if (characterPressed.toUpperCase() == "A") {
ai = !ai;
} else if (characterPressed.toUpperCase() == "R") {
//load saved generation values
loadArchive(prompt("Insert archive:"));
} else if (characterPressed.toUpperCase() == "G") {
if (localStorage.getItem("archive") === null) {
alert("No archive saved. Archives are saved after a generation has passed, and remain across sessions. Try again once a generation has passed");
} else {
prompt("Archive from last generation (including from last session):", localStorage.getItem("archive"));
}
} else if (characterPressed.toUpperCase() == "F") {
//?
inspectMoveSelection = !inspectMoveSelection;
} else {
return true;
}
//outputs game state to the screen (post key press)
output();
return false;
};
/**
* Creates the initial population of genomes, each with random genes.
*/
function createInitialPopulation() {
//inits the array
genomes = [];
//for a given population size
for (var i = 0; i < populationSize; i++) {
//randomly initialize the 7 values that make up a genome
//these are all weight values that are updated through evolution
var genome = {
//unique identifier for a genome
id: Math.random(),
//The weight of each row cleared by the given move. the more rows that are cleared, the more this weight increases
rowsCleared: Math.random() - 0.5,
//the absolute height of the highest column to the power of 1.5
//added so that the algorithm can be able to detect if the blocks are stacking too high
weightedHeight: Math.random() - 0.5,
//The sum of all the column’s heights
cumulativeHeight: Math.random() - 0.5,
//the highest column minus the lowest column
relativeHeight: Math.random() - 0.5,
//the sum of all the empty cells that have a block above them (basically, cells that are unable to be filled)
holes: Math.random() * 0.5,
// the sum of absolute differences between the height of each column
//(for example, if all the shapes on the grid lie completely flat, then the roughness would equal 0).
roughness: Math.random() - 0.5,
};
//add them to the array
genomes.push(genome);
}
evaluateNextGenome();
}
/**
* Evaluates the next genome in the population. If there is none, evolves the population.
*/
function evaluateNextGenome() {
//increment index in genome array
currentGenome++;
//If there is none, evolves the population.
if (currentGenome == genomes.length) {
evolve();
}
//load current gamestate
loadState(roundState);
//reset moves taken
movesTaken = 0;
//and make the next move
makeNextMove();
}
/**
* Evolves the entire population and goes to the next generation.
*/
function evolve() {
console.log("Generation " + generation + " evaluated.");
//reset current genome for new generation
currentGenome = 0;
//increment generation
generation++;
//resets the game
reset();
//gets the current game state
roundState = getState();
//sorts genomes in decreasing order of fitness values
genomes.sort(function(a, b) {
return b.fitness - a.fitness;
});
//add a copy of the fittest genome to the elites list
archive.elites.push(clone(genomes[0]));
console.log("Elite's fitness: " + genomes[0].fitness);
//remove the tail end of genomes, focus on the fittest
while(genomes.length > populationSize / 2) {
genomes.pop();
}
//sum of the fitness for each genome
var totalFitness = 0;
for (var i = 0; i < genomes.length; i++) {
totalFitness += genomes[i].fitness;
}
//get a random index from genome array
function getRandomGenome() {
return genomes[randomWeightedNumBetween(0, genomes.length - 1)];
}
//create children array
var children = [];
//add the fittest genome to array
children.push(clone(genomes[0]));
//add population sized amount of children
while (children.length < populationSize) {
//crossover between two random genomes to make a child
children.push(makeChild(getRandomGenome(), getRandomGenome()));
}
//create new genome array
genomes = [];
//to store all the children in
genomes = genomes.concat(children);
//store this in our archive
archive.genomes = clone(genomes);
//and set current gen
archive.currentGeneration = clone(generation);
console.log(JSON.stringify(archive));
//store archive, thanks JS localstorage! (short term memory)
localStorage.setItem("archive", JSON.stringify(archive));
}
/**
* Creates a child genome from the given parent genomes, and then attempts to mutate the child genome.
* @param {Genome} mum The first parent genome.
* @param {Genome} dad The second parent genome.
* @return {Genome} The child genome.
*/
function makeChild(mum, dad) {
//init the child given two genomes (its 7 parameters + initial fitness value)
var child = {
//unique id
id : Math.random(),
//all these params are randomly selected between the mom and dad genome
rowsCleared: randomChoice(mum.rowsCleared, dad.rowsCleared),
weightedHeight: randomChoice(mum.weightedHeight, dad.weightedHeight),
cumulativeHeight: randomChoice(mum.cumulativeHeight, dad.cumulativeHeight),
relativeHeight: randomChoice(mum.relativeHeight, dad.relativeHeight),
holes: randomChoice(mum.holes, dad.holes),
roughness: randomChoice(mum.roughness, dad.roughness),
//no fitness. yet.
fitness: -1
};
//mutation time!
//we mutate each parameter using our mutationstep
if (Math.random() < mutationRate) {
child.rowsCleared = child.rowsCleared + Math.random() * mutationStep * 2 - mutationStep;
}
if (Math.random() < mutationRate) {
child.weightedHeight = child.weightedHeight + Math.random() * mutationStep * 2 - mutationStep;
}
if (Math.random() < mutationRate) {
child.cumulativeHeight = child.cumulativeHeight + Math.random() * mutationStep * 2 - mutationStep;
}
if (Math.random() < mutationRate) {
child.relativeHeight = child.relativeHeight + Math.random() * mutationStep * 2 - mutationStep;
}
if (Math.random() < mutationRate) {
child.holes = child.holes + Math.random() * mutationStep * 2 - mutationStep;
}
if (Math.random() < mutationRate) {
child.roughness = child.roughness + Math.random() * mutationStep * 2 - mutationStep;
}
return child;
}
/**
* Returns an array of all the possible moves that could occur in the current state, rated by the parameters of the current genome.
* @return {Array} An array of all the possible moves that could occur.
*/
function getAllPossibleMoves() {
var lastState = getState();
var possibleMoves = [];
var possibleMoveRatings = [];
var iterations = 0;
//for each possible rotation
for (var rots = 0; rots < 4; rots++) {
var oldX = [];
//for each iteration
for (var t = -5; t <= 5; t++) {
iterations++;
loadState(lastState);
//rotate shape
for (var j = 0; j < rots; j++) {
rotateShape();
}
//move left
if (t < 0) {
for (var l = 0; l < Math.abs(t); l++) {
moveLeft();
}
//move right
} else if (t > 0) {
for (var r = 0; r < t; r++) {
moveRight();
}
}
//if the shape has moved at all
if (!contains(oldX, currentShape.x)) {
//move it down
var moveDownResults = moveDown();
while (moveDownResults.moved) {
moveDownResults = moveDown();
}
//set the 7 parameters of a genome
var algorithm = {
rowsCleared: moveDownResults.rowsCleared,
weightedHeight: Math.pow(getHeight(), 1.5),
cumulativeHeight: getCumulativeHeight(),
relativeHeight: getRelativeHeight(),
holes: getHoles(),
roughness: getRoughness()
};
//rate each move
var rating = 0;
rating += algorithm.rowsCleared * genomes[currentGenome].rowsCleared;
rating += algorithm.weightedHeight * genomes[currentGenome].weightedHeight;
rating += algorithm.cumulativeHeight * genomes[currentGenome].cumulativeHeight;
rating += algorithm.relativeHeight * genomes[currentGenome].relativeHeight;
rating += algorithm.holes * genomes[currentGenome].holes;
rating += algorithm.roughness * genomes[currentGenome].roughness;
//if the move loses the game, lower its rating
if (moveDownResults.lose) {
rating -= 500;
}
//push all possible moves, with their associated ratings and parameter values to an array
possibleMoves.push({rotations: rots, translation: t, rating: rating, algorithm: algorithm});
//update the position of old X value
oldX.push(currentShape.x);
}
}
}
//get last state
loadState(lastState);
//return array of all possible moves
return possibleMoves;
}
/**
* Returns the highest rated move in the given array of moves.
* @param {Array} moves An array of possible moves to choose from.
* @return {Move} The highest rated move from the moveset.
*/
function getHighestRatedMove(moves) {
//start these values off small
var maxRating = -10000000000000;
var maxMove = -1;
var ties = [];
//iterate through the list of moves
for (var index = 0; index < moves.length; index++) {
//if the current moves rating is higher than our maxrating
if (moves[index].rating > maxRating) {
//update our max values to include this moves values
maxRating = moves[index].rating;
maxMove = index;
//store index of this move
ties = [index];
} else if (moves[index].rating == maxRating) {
//if it ties with the max rating
//add the index to the ties array
ties.push(index);
}
}
//eventually we'll set the highest move value to this move var
var move = moves[ties[0]];
//and set the number of ties
move.algorithm.ties = ties.length;
return move;
}
/**
* Makes a move, which is decided upon using the parameters in the current genome.
*/
function makeNextMove() {
//increment number of moves taken
movesTaken++;
//if its over the limit of moves
if (movesTaken > moveLimit) {
//update this genomes fitness value using the game score
genomes[currentGenome].fitness = clone(score);
//and evaluates the next genome
evaluateNextGenome();
} else {
//time to make a move
//we're going to re-draw, so lets store the old drawing
var oldDraw = clone(draw);
draw = false;
//get all the possible moves
var possibleMoves = getAllPossibleMoves();
//lets store the current state since we will update it
var lastState = getState();
//whats the next shape to play
nextShape();
//for each possible move
for (var i = 0; i < possibleMoves.length; i++) {
//get the best move. so were checking all the possible moves, for each possible move. moveception.
var nextMove = getHighestRatedMove(getAllPossibleMoves());
//add that rating to an array of highest rates moves
possibleMoves[i].rating += nextMove.rating;
}
//load current state
loadState(lastState);
//get the highest rated move ever
var move = getHighestRatedMove(possibleMoves);
//then rotate the shape as it says too
for (var rotations = 0; rotations < move.rotations; rotations++) {
rotateShape();
}
//and move left as it says
if (move.translation < 0) {
for (var lefts = 0; lefts < Math.abs(move.translation); lefts++) {
moveLeft();
}
//and right as it says
} else if (move.translation > 0) {
for (var rights = 0; rights < move.translation; rights++) {
moveRight();
}
}
//update our move algorithm
if (inspectMoveSelection) {
moveAlgorithm = move.algorithm;
}
//and set the old drawing to the current
draw = oldDraw;
//output the state to the screen
output();
//and update the score
updateScore();
}
}
/**
* Updates the game.
*/
function update() {
//if we have our AI turned on and the current genome is nonzero
//make a move
if (ai && currentGenome != -1) {
//move the shape down
var results = moveDown();
//if that didn't do anything
if (!results.moved) {
//if we lost
if (results.lose) {
//update the fitness
genomes[currentGenome].fitness = clone(score);
//move on to the next genome
evaluateNextGenome();
} else {
//if we didnt lose, make the next move
makeNextMove();
}
}
} else {
//else just move down
moveDown();
}
//output the state to the screen
output();
//and update the score
updateScore();
}
/**
* Moves the current shape down if possible.
* @return {Object} The results of the movement of the piece.
*/
function moveDown() {
//array of possibilities
var result = {lose: false, moved: true, rowsCleared: 0};
//remove the shape, because we will draw a new one
removeShape();
//move it down the y axis
currentShape.y++;
//if it collides with the grid
if (collides(grid, currentShape)) {
//update its position
currentShape.y--;
//apply (stick) it to the grid
applyShape();
//move on to the next shape in the bag
nextShape();
//clear rows and get number of rows cleared
result.rowsCleared = clearRows();
//check again if this shape collides with our grid
if (collides(grid, currentShape)) {
//reset
result.lose = true;
if (ai) {
} else {
reset();
}
}
result.moved = false;
}
//apply shape, update the score and output the state to the screen
applyShape();
score++;
updateScore();
output();
return result;
}
/**
* Moves the current shape to the left if possible.
*/
function moveLeft() {
//remove current shape, slide it over, if it collides though, slide it back
removeShape();
currentShape.x--;
if (collides(grid, currentShape)) {
currentShape.x++;
}
//apply the new shape
applyShape();
}
/**
* Moves the current shape to the right if possible.
*/
//same deal
function moveRight() {
removeShape();
currentShape.x++;
if (collides(grid, currentShape)) {
currentShape.x--;
}
applyShape();
}
/**
* Rotates the current shape clockwise if possible.
*/
//slide it if we can, else return to original rotation
function rotateShape() {
removeShape();
currentShape.shape = rotate(currentShape.shape, 1);
if (collides(grid, currentShape)) {
currentShape.shape = rotate(currentShape.shape, 3);
}
applyShape();
}
/**
* Clears any rows that are completely filled.
*/
function clearRows() {
//empty array for rows to clear
var rowsToClear = [];
//for each row in the grid
for (var row = 0; row < grid.length; row++) {
var containsEmptySpace = false;
//for each column
for (var col = 0; col < grid[row].length; col++) {
//if its empty
if (grid[row][col] === 0) {
//set this value to true
containsEmptySpace = true;
}
}
//if none of the columns in the row were empty
if (!containsEmptySpace) {
//add the row to our list, it's completely filled!
rowsToClear.push(row);
}
}
//increase score for up to 4 rows. it maxes out at 12000
if (rowsToClear.length == 1) {
score += 400;
} else if (rowsToClear.length == 2) {
score += 1000;
} else if (rowsToClear.length == 3) {
score += 3000;
} else if (rowsToClear.length >= 4) {
score += 12000;
}
//new array for cleared rows
var rowsCleared = clone(rowsToClear.length);
//for each value
for (var toClear = rowsToClear.length - 1; toClear >= 0; toClear--) {
//remove the row from the grid
grid.splice(rowsToClear[toClear], 1);
}
//shift the other rows
while (grid.length < 20) {
grid.unshift([0,0,0,0,0,0,0,0,0,0]);
}
//return the rows cleared
return rowsCleared;
}
/**
* Applies the current shape to the grid.
*/
function applyShape() {
//for each value in the current shape (row x column)
for (var row = 0; row < currentShape.shape.length; row++) {
for (var col = 0; col < currentShape.shape[row].length; col++) {
//if its non-empty
if (currentShape.shape[row][col] !== 0) {
//set the value in the grid to its value. Stick the shape in the grid!
grid[currentShape.y + row][currentShape.x + col] = currentShape.shape[row][col];
}
}
}
}
/**
* Removes the current shape from the grid.
*/
//same deal but reverse
function removeShape() {
for (var row = 0; row < currentShape.shape.length; row++) {
for (var col = 0; col < currentShape.shape[row].length; col++) {
if (currentShape.shape[row][col] !== 0) {
grid[currentShape.y + row][currentShape.x + col] = 0;
}
}
}
}
/**
* Cycles to the next shape in the bag.
*/
function nextShape() {
//increment the bag index
bagIndex += 1;
//if we're at the start or end of the bag
if (bag.length === 0 || bagIndex == bag.length) {
//generate a new bag of genomes
generateBag();
}
//if almost at end of bag
if (bagIndex == bag.length - 1) {
//store previous seed
var prevSeed = rndSeed;
//generate upcoming shape
upcomingShape = randomProperty(shapes);
//set random seed
rndSeed = prevSeed;
} else {
//get the next shape from our bag
upcomingShape = shapes[bag[bagIndex + 1]];
}
//get our current shape from the bag
currentShape.shape = shapes[bag[bagIndex]];
//define its position
currentShape.x = Math.floor(grid[0].length / 2) - Math.ceil(currentShape.shape[0].length / 2);
currentShape.y = 0;
}
/**
* Generates the bag of shapes.
*/
function generateBag() {
bag = [];
var contents = "";
//7 shapes
for (var i = 0; i < 7; i++) {
//generate shape randomly
var shape = randomKey(shapes);
while(contents.indexOf(shape) != -1) {
shape = randomKey(shapes);
}
//update bag with generated shape
bag[i] = shape;
contents += shape;
}
//reset bag index
bagIndex = 0;
}
/**
* Resets the game.
*/
function reset() {
score = 0;
grid = [[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
];
moves = 0;
generateBag();
nextShape();
}
/**
* Determines if the given grid and shape collide with one another.
* @param {Grid} scene The grid to check.
* @param {Shape} object The shape to check.
* @return {Boolean} Whether the shape and grid collide.
*/
function collides(scene, object) {
//for the size of the shape (row x column)
for (var row = 0; row < object.shape.length; row++) {
for (var col = 0; col < object.shape[row].length; col++) {
//if its not empty
if (object.shape[row][col] !== 0) {
//if it collides, return true
if (scene[object.y + row] === undefined || scene[object.y + row][object.x + col] === undefined || scene[object.y + row][object.x + col] !== 0) {
return true;
}
}
}
}
return false;
}
//for rotating a shape, how many times should we rotate
function rotate(matrix, times) {
//for each time
for (var t = 0; t < times; t++) {
//flip the shape matrix
matrix = transpose(matrix);
//and for the length of the matrix, reverse each column
for (var i = 0; i < matrix.length; i++) {
matrix[i].reverse();
}
}
return matrix;
}
//flip row x column to column x row
function transpose(array) {
return array[0].map(function(col, i) {
return array.map(function(row) {
return row[i];
});
});
}
/**
* Outputs the state to the screen.
*/
function output() {
if (draw) {
var output = document.getElementById("output");
var html = "<h1>TetNet</h1><h5>Evolutionary approach to Tetris AI</h5>var grid = [";
var space = " ";
for (var i = 0; i < grid.length; i++) {
if (i === 0) {
html += "[" + grid[i] + "]";
} else {
html += "<br />" + space + "[" + grid[i] + "]";
}
}
html += "];";
for (var c = 0; c < colors.length; c++) {
html = replaceAll(html, "," + (c + 1), ",<font color=\"" + colors[c] + "\">" + (c + 1) + "</font>");
html = replaceAll(html, (c + 1) + ",", "<font color=\"" + colors[c] + "\">" + (c + 1) + "</font>,");
}
output.innerHTML = html;
}
}
/**
* Updates the side information.
*/
function updateScore() {
if (draw) {
var scoreDetails = document.getElementById("score");
var html = "<br /><br /><h2> </h2><h2>Score: " + score + "</h2>";
html += "<br /><b>--Upcoming--</b>";
for (var i = 0; i < upcomingShape.length; i++) {
var next =replaceAll((upcomingShape[i] + ""), "0", " ");
html += "<br /> " + next;
}
for (var l = 0; l < 4 - upcomingShape.length; l++) {
html += "<br />";
}
for (var c = 0; c < colors.length; c++) {
html = replaceAll(html, "," + (c + 1), ",<font color=\"" + colors[c] + "\">" + (c + 1) + "</font>");
html = replaceAll(html, (c + 1) + ",", "<font color=\"" + colors[c] + "\">" + (c + 1) + "</font>,");
}
html += "<br />Speed: " + speed;
if (ai) {
html += "<br />Moves: " + movesTaken + "/" + moveLimit;
html += "<br />Generation: " + generation;
html += "<br />Individual: " + (currentGenome + 1) + "/" + populationSize;
html += "<br /><pre style=\"font-size:12px\">" + JSON.stringify(genomes[currentGenome], null, 2) + "</pre>";
if (inspectMoveSelection) {
html += "<br /><pre style=\"font-size:12px\">" + JSON.stringify(moveAlgorithm, null, 2) + "</pre>";
}
}
html = replaceAll(replaceAll(replaceAll(html, " ,", " "), ", ", " "), ",", " ");
scoreDetails.innerHTML = html;
}
}
/**
* Returns the current game state in an object.
* @return {State} The current game state.
*/
function getState() {
var state = {
grid: clone(grid),
currentShape: clone(currentShape),
upcomingShape: clone(upcomingShape),
bag: clone(bag),
bagIndex: clone(bagIndex),
rndSeed: clone(rndSeed),
score: clone(score)
};
return state;
}
/**
* Loads the game state from the given state object.
* @param {State} state The state to load.
*/
function loadState(state) {
grid = clone(state.grid);
currentShape = clone(state.currentShape);
upcomingShape = clone(state.upcomingShape);
bag = clone(state.bag);
bagIndex = clone(state.bagIndex);
rndSeed = clone(state.rndSeed);
score = clone(state.score);
output();
updateScore();
}
/**
* Returns the cumulative height of all the columns.
* @return {Number} The cumulative height.
*/
function getCumulativeHeight() {
removeShape();
var peaks = [20,20,20,20,20,20,20,20,20,20];
for (var row = 0; row < grid.length; row++) {
for (var col = 0; col < grid[row].length; col++) {
if (grid[row][col] !== 0 && peaks[col] === 20) {
peaks[col] = row;
}
}
}
var totalHeight = 0;
for (var i = 0; i < peaks.length; i++) {
totalHeight += 20 - peaks[i];
}
applyShape();
return totalHeight;
}
/**
* Returns the number of holes in the grid.
* @return {Number} The number of holes.
*/
function getHoles() {
removeShape();
var peaks = [20,20,20,20,20,20,20,20,20,20];
for (var row = 0; row < grid.length; row++) {
for (var col = 0; col < grid[row].length; col++) {
if (grid[row][col] !== 0 && peaks[col] === 20) {
peaks[col] = row;
}
}
}
var holes = 0;
for (var x = 0; x < peaks.length; x++) {
for (var y = peaks[x]; y < grid.length; y++) {
if (grid[y][x] === 0) {
holes++;
}
}
}
applyShape();
return holes;
}
/**
* Returns an array that replaces all the holes in the grid with -1.
* @return {Array} The modified grid array.
*/
function getHolesArray() {
var array = clone(grid);
removeShape();
var peaks = [20,20,20,20,20,20,20,20,20,20];
for (var row = 0; row < grid.length; row++) {
for (var col = 0; col < grid[row].length; col++) {
if (grid[row][col] !== 0 && peaks[col] === 20) {
peaks[col] = row;
}
}
}
for (var x = 0; x < peaks.length; x++) {
for (var y = peaks[x]; y < grid.length; y++) {
if (grid[y][x] === 0) {
array[y][x] = -1;
}
}
}
applyShape();
return array;
}
/**
* Returns the roughness of the grid.
* @return {Number} The roughness of the grid.
*/
function getRoughness() {
removeShape();
var peaks = [20,20,20,20,20,20,20,20,20,20];
for (var row = 0; row < grid.length; row++) {
for (var col = 0; col < grid[row].length; col++) {
if (grid[row][col] !== 0 && peaks[col] === 20) {
peaks[col] = row;
}
}
}
var roughness = 0;
var differences = [];
for (var i = 0; i < peaks.length - 1; i++) {
roughness += Math.abs(peaks[i] - peaks[i + 1]);
differences[i] = Math.abs(peaks[i] - peaks[i + 1]);
}
applyShape();
return roughness;
}
/**
* Returns the range of heights of the columns on the grid.
* @return {Number} The relative height.
*/
function getRelativeHeight() {
removeShape();
var peaks = [20,20,20,20,20,20,20,20,20,20];
for (var row = 0; row < grid.length; row++) {
for (var col = 0; col < grid[row].length; col++) {
if (grid[row][col] !== 0 && peaks[col] === 20) {
peaks[col] = row;
}
}
}
applyShape();
return Math.max.apply(Math, peaks) - Math.min.apply(Math, peaks);
}
/**
* Returns the height of the biggest column on the grid.
* @return {Number} The absolute height.
*/
function getHeight() {
removeShape();
var peaks = [20,20,20,20,20,20,20,20,20,20];
for (var row = 0; row < grid.length; row++) {
for (var col = 0; col < grid[row].length; col++) {
if (grid[row][col] !== 0 && peaks[col] === 20) {
peaks[col] = row;
}
}
}
applyShape();
return 20 - Math.min.apply(Math, peaks);
}
/**
* Loads the archive given.
* @param {String} archiveString The stringified archive.
*/
function loadArchive(archiveString) {
archive = JSON.parse(archiveString);
genomes = clone(archive.genomes);
populationSize = archive.populationSize;
generation = archive.currentGeneration;
currentGenome = 0;
reset();
roundState = getState();
console.log("Archive loaded!");
}
/**
* Clones an object.
* @param {Object} obj The object to clone.
* @return {Object} The cloned object.
*/
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
/**
* Returns a random property from the given object.
* @param {Object} obj The object to select a property from.
* @return {Property} A random property.
*/
function randomProperty(obj) {
return(obj[randomKey(obj)]);
}
/**
* Returns a random property key from the given object.
* @param {Object} obj The object to select a property key from.
* @return {Property} A random property key.
*/
function randomKey(obj) {
var keys = Object.keys(obj);
var i = seededRandom(0, keys.length);
return keys[i];
}
function replaceAll(target, search, replacement) {
return target.replace(new RegExp(search, 'g'), replacement);
}
/**
* Returns a random number that is determined from a seeded random number generator.
* @param {Number} min The minimum number, inclusive.
* @param {Number} max The maximum number, exclusive.
* @return {Number} The generated random number.
*/
function seededRandom(min, max) {
max = max || 1;
min = min || 0;
rndSeed = (rndSeed * 9301 + 49297) % 233280;
var rnd = rndSeed / 233280;
return Math.floor(min + rnd * (max - min));
}
function randomNumBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomWeightedNumBetween(min, max) {
return Math.floor(Math.pow(Math.random(), 2) * (max - min + 1) + min);
}
function randomChoice(propOne, propTwo) {
if (Math.round(Math.random()) === 0) {
return clone(propOne);
} else {
return clone(propTwo);
}
}
function contains(a, obj) {
var i = a.length;
while (i--) {
if (a[i] === obj) {
return true;
}
}
return false;
}
Comments
0 B
|👍
/👎