Skip to content

Instantly share code, notes, and snippets.

@evanthebouncy
Created March 4, 2026 03:41
Show Gist options
  • Select an option

  • Save evanthebouncy/6db689afe182523a30d31cfb466512f3 to your computer and use it in GitHub Desktop.

Select an option

Save evanthebouncy/6db689afe182523a30d31cfb466512f3 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<body>
<div>
<span style="float: left">
<canvas id="myCanvas" width="800" height="800"
style="border:1px solid #d3d3d3;">
Your browser does not support the canvas element.
</canvas>
</span>
<span style="float: right">
<div id="hititle"> current high schore </div>
<div id="hiscore">
STUFF GOES HERE ?
</div>
<div id="allhititle"> all time high schore </div>
<div id="allhi">
STUFF GOES HERE ?
</div>
</span>
</div>
<script src="discrete.js"></script>
<script>
// GAME RULE:
// just like slither.io if you crash yo head you die die
// each snake starts out with 100 hp
// each move takes 1 point
// each food grants 10 hp
// you also die die if hp reach 0 as well
var L = 80;
var FOOD_HP = 20;
var START_HP = 50;
// window size
var W = 5;
var N_GENES = 2;
var DECAY_SCORE = 0.01;
var cv = document.getElementById('myCanvas');
var ctx = cv.getContext('2d');
var cvWidth = cv.width;
var cvHeight = cv.height;
function draw_DDD(ctx, args) {
var x = args[0];
var y = args[1];
var c = args[2];
ctx.beginPath();
ctx.fillStyle = c;
ctx.fillRect(x*10, y*10, 10, 10)
ctx.fill();
ctx.closePath();
}
// ======================= S T U F F S ======================= //
// the board rendering of snakes
var board_render = [
];
// a snake is a list of coordinates of length 10
var snakes = [];
// a food is a list of coordinates of all food on the board
var foods = [];
var best_weights = [];
for (var j = 0; j < N_GENES; j++){
best_weights.push([0,
[randomV(W*W*4+3), randomV(W*W*4+3),randomV(W*W*4+3), randomV(W*W*4+3)]]);
};
// ========================= F U N C T I O N S ====================
function to_board (snakes, foods) {
var new_board_render = [];
snakes.forEach(function(snake) {
var colr = snake.color;
snake.body.forEach(function(coord) {
new_board_render.push([coord[0], coord[1], colr])
});
});
foods.forEach(function(food) {
var colr = food.color;
new_board_render.push([food.position[0], food.position[1], colr])
});
return new_board_render;
}
function move_head(snake, snakes, foods) {
var head = snake.body[0];
var dir = snake.policy[0](snake, snakes, foods);
var xx = head[0];
var yy = head[1];
if (dir == 0) {return [(L + xx - 1) % L, yy];}
if (dir == 1) {return [(L + xx + 1) % L, yy];}
if (dir == 2) {return [xx, (L + yy - 1) % L];}
if (dir == 3) {return [xx, (L + yy + 1) % L];}
}
// check if a snake has crashed BibleThump
function crash(snake, head_coord, other_snakes){
var color = snake.color;
var x = head_coord[0];
var y = head_coord[1];
var cur_snake_crash = false;
other_snakes.forEach(function(other_snake) {
other_snake.body.forEach(function(crd) {
if (crd[0] == x && crd[1] == y && other_snake.color != color) {
cur_snake_crash = true;
}
if (snake.hp <= 0) {
cur_snake_crash = true;
}
});
});
return cur_snake_crash;
}
// makek the snake eat the food and return new list of food
function eat_food(head_coord, foods) {
var new_foods = [];
var x = head_coord[0];
var y = head_coord[1];
foods.forEach(function(food) {
var pos = food.position;
if (!(pos[0] == x && pos[1] == y)) {
new_foods.push(food);
}
});
return new_foods;
}
// takes in the current state of snakes and foods
// produces the next state of snakes and foods
function update(snakes, foods) {
var new_snakes = [];
// we need to update multiple rounds of eating food
var foods = foods;
snakes.forEach(function(snake) {
var snake_head = snake.body[0];
var new_head = move_head(snake, snakes, foods);
var new_body = []
new_body[0] = new_head;
for (i = 1; i < 10; i++) {
new_body[i] = snake.body[i-1];
}
// SNEK HAS EATEN FOOD PogChamp
var new_foods = eat_food(new_head, foods);
if (new_foods.length < foods.length) {
foods = new_foods;
snake.hp = snake.hp + FOOD_HP;
snake.max_score = snake.max_score + 1;
}
// SNEK HAS CRASH AND DED BibleThump
var cur_snake_crash = crash(snake, new_head, snakes);
if (!cur_snake_crash) {
new_snakes.push({color : snake.color,
body : new_body, hp : snake.hp - 1,
max_score : snake.max_score,
policy : snake.policy,
gene_id : snake.gene_id});
} else {
snake.body.forEach(function(bb) {
var bbx = bb[0];
var bby = bb[1];
foods = spawnFood(foods, bbx,bby);
});
}
});
return [new_snakes, foods];
}
// click handler to add random rects
window.addEventListener('click', function(e) {
// console.log(e.clientX);
// console.log(e.clientY);
spawnSnek();
});
function getRandomColor() {
// ignoring color E and F here, well use those for food
var letters = '456789ABCD';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 10)];
}
return color;
}
function randomV(lenn) {
var ret = [];
for (i = 0; i < lenn; i++) {
ret.push(2 * Math.random() - 1.0);
}
return ret;
}
function perturb_weight(w) {
var ret = [];
for (i = 0; i < w.length; i++){
ret.push(w[i] + Math.random() * 0.01);
}
return ret;
}
function getNewPolicy(gene_id) {
if (Math.random() < 0.1){
return randomPolicy();
} else {
var best_weight = best_weights[gene_id][1];
var new_weights = [perturb_weight(best_weight[0]),
perturb_weight(best_weight[1]),
perturb_weight(best_weight[2]),
perturb_weight(best_weight[3])];
return [Policy(new_weights), new_weights];
}
}
function randomPolicy() {
// for each input in sliding window, 4 possible values:
// empty, self, enemy, food
var lenn = W * W * 4 + 3;
var weight0 = randomV(lenn);
var weight1 = randomV(lenn);
var weight2 = randomV(lenn);
var weight3 = randomV(lenn);
var weights = [weight0, weight1, weight2, weight3];
return [Policy(weights), weights];
}
function Policy(weights) {
function policy(snake, snakes, foods) {
var sl_window = sliding_window(snake, snakes, foods);
var dir0_str = dotprod(sl_window, weights[0]);
var dir1_str = dotprod(sl_window, weights[1]);
var dir2_str = dotprod(sl_window, weights[2]);
var dir3_str = dotprod(sl_window, weights[3]);
// var dir = Math.floor(Math.random() * 4);
// var dir = soft_arg_max(dir0_str, dir1_str, dir2_str, dir3_str);
var dir = arg_max(dir0_str, dir1_str, dir2_str, dir3_str);
return dir;
}
return policy;
}
function spawnSnek() {
var randColor = getRandomColor();
var rand_x = Math.floor(Math.random() * L);
var rand_y = Math.floor(Math.random() * L);
var g_id = Math.floor(Math.random() * N_GENES);
snakes.push(
{color : randColor,
body : [[rand_x, rand_y], [rand_x, rand_y],
[rand_x, rand_y], [rand_x, rand_y],
[rand_x, rand_y], [rand_x, rand_y],
[rand_x, rand_y], [rand_x, rand_y],
[rand_x, rand_y], [rand_x, rand_y]],
hp : START_HP,
max_score : 0,
gene_id : g_id,
policy : getNewPolicy(g_id),
}
);
}
function spawnFood(foods, x, y) {
var randColor = "#000000"
foods.push(
{color : randColor,
position : [x, y],
}
);
return foods;
}
function spawnRandFood() {
var rand_x = Math.floor(Math.random() * L);
var rand_y = Math.floor(Math.random() * L);
spawnFood(foods, rand_x, rand_y);
}
// update the best weight(s)
function update_best_weight() {
for (var ii = 0; ii < N_GENES; ii++) {
best_weights[ii][0] -= DECAY_SCORE;
}
snakes.forEach(function(snake) {
var gene_id = snake.gene_id;
console.log(gene_id);
console.log(best_weights[gene_id]);
if (snake.max_score > best_weights[gene_id][0]) {
best_weights[gene_id] = [snake.max_score, snake.policy[1]];
}
});
}
// THE MAIN GAME LOOP ==========================
// animation : always running loop.
function animate() {
setTimeout(function (){
// call again next time we can draw
requestAnimationFrame(animate);
// RENDER BOARD
ctx.clearRect(0, 0, cvWidth, cvHeight);
board_render.forEach(function(o) {
draw_DDD(ctx, o)
});
// UPDATE
// update takes in old snakes, old foods
// and produce new snakes and new foods
var snakes_foods = update(snakes, foods);
snakes = snakes_foods[0];
foods = snakes_foods[1];
board_render = to_board(snakes, foods);
// SPAWN IF LESS THAN DISRED AMOUNT
if (snakes.length < 20) {
spawnSnek();
}
if (foods.length < 0.00 * L * L) {
spawnRandFood();
}
// UPDATE LEADER BOARD
show_leaderboard();
// UPDATE NEW CHAMPION
update_best_weight();
}, 10);
}
animate();
// HALPERS =========================================================
function dotprod(x,y){
var ret = 0.0;
for (i = 0; i < x.length; i++) {
ret += x[i] * y[i];
}
return ret;
}
function arg_max(a,b,c,d) {
var maxxx = Math.max(a,b,c,d);
if (maxxx == a) {
return 0;
}
if (maxxx == b) {
return 1;
}
if (maxxx == c) {
return 2;
}
if (maxxx == d) {
return 3;
}
}
function soft_arg_max(a,b,c,d) {
var aa = Math.exp(a);
var bb = Math.exp(b);
var cc = Math.exp(c);
var dd = Math.exp(d);
var total = aa + bb + cc + dd;
var prob_a = aa / total;
var prob_b = bb / total;
var prob_c = cc / total;
var prob_d = dd / total;
var probabilities = [prob_a, prob_b, prob_c, prob_d];
var disc = SJS.Discrete(probabilities);
return disc.draw();
}
// augment a coordinate for the sliding window on a torus
function aug(x,y) {
var ret = [[x,y],
[x-L,y-L],
[x-L,y+0],
[x-L,y+L],
[x+0,y-L],
[x+0,y+0],
[x+0,y+L],
[x+L,y-L],
[x+L,y+0],
[x+L,y+L],
];
return ret;
}
function nearest_food(headxy, foods){
var headx = headxy[0];
var heady = headxy[1];
var min_x = 0;
var min_y = 0;
var min_dist = 9999;
foods.forEach(function(food){
var foodx = food.position[0];
var foody = food.position[1];
var foodxy_aug = aug(foodx, foody);
foodxy_aug.forEach(function(augxy) {
var augx = augxy[0];
var augy = augxy[1];
var xydist = Math.abs(augx - headx) + Math.abs(augy - heady);
if (xydist < min_dist) {
min_dist = xydist
min_x = augx;
min_y = augy;
}
}
);
});
var diffx = min_x - headx;
var diffy = min_y - heady;
var totall = Math.abs(diffx) + Math.abs(diffy) + 0.1;
return [diffx / totall, diffy / totall];
}
// coordinate transform in aug torus space to sliding window
// return [x, y] if has transform, return [] if there is no transform
function win_xform(Ox, Oy, x, y) {
// sliding window bounds
var halfW = Math.floor(W/2);
var x_l = Ox - halfW;
var x_u = Ox + halfW;
var y_l = Oy - halfW;
var y_u = Oy + halfW;
var aug_xy = aug(x,y);
var ret = [];
aug_xy.forEach(function(xy) {
aug_x = xy[0];
aug_y = xy[1];
if (x_l <= aug_x && aug_x <= x_u && y_l <= aug_y && aug_y <= y_u) {
ret.push(aug_x - Ox + halfW);
ret.push(aug_y - Oy + halfW);
}
});
return ret ;
}
// get the sliding window of the snake head and put into a feature space
function sliding_window(snake, snakes, foods) {
var Ox = snake.body[0][0];
var Oy = snake.body[0][1];
var matrix = [];
for(var i=0; i<W; i++) {
matrix[i] = new Array(W);
}
// add the foods
foods.forEach(function(food) {
var fxfy = win_xform(Ox, Oy, food.position[0], food.position[1]);
if (fxfy.length > 0) {
var fx = fxfy[0];
var fy = fxfy[1];
matrix[fx][fy] = 1;
}
});
// add the enemies
snakes.forEach(function(other_snake){
other_snake.body.forEach(function(seg) {
var fxfy = win_xform(Ox, Oy, seg[0], seg[1]);
if (fxfy.length > 0) {
var fx = fxfy[0];
var fy = fxfy[1];
matrix[fx][fy] = 2;
}
});
});
// add the selfs
snake.body.forEach(function(selfseg) {
var fxfy = win_xform(Ox, Oy, selfseg[0], selfseg[1]);
if (fxfy.length > 0) {
var fx = fxfy[0];
var fy = fxfy[1];
matrix[fx][fy] = 3;
}
});
var transformed_mat = new Array(W * W * 4);
for (ii = 0; ii < W * W * 4; ii++) {
transformed_mat[ii] = 0.0;
}
for(var i=0; i<W; i++) {
for(var j=0; j<W; j++) {
transformed_mat[i * W * 4 + j * 4 + matrix[i][j]] = 1.0;
}
}
var food_vect = nearest_food([Ox, Oy], foods);
// return food_vect;
return transformed_mat.concat(food_vect).concat([1.0 / (snake.hp + 1)]);
}
// Calculate the leader board
function show_leaderboard() {
var div = document.getElementById("hititle");
div.style.fontSize = "xx-large";
div.innerHTML = "Current High Score";
var div = document.getElementById("allhititle");
div.style.fontSize = "xx-large";
div.innerHTML = "ALL TIME High Score";
document.getElementById("hiscore").innerHTML="";
snakes.forEach(function(snake) {
var div = document.createElement("div");
div.style.fontSize = "xx-large";
div.innerHTML = String(snake.max_score) + " (" + String(snake.hp) + ")";
// div.innerHTML = snake.hp;
div.style.color = snake.color;
document.getElementById("hiscore").appendChild(div);
});
var div = document.getElementById("allhi");
div.style.fontSize = "xx-large";
var all_time = " ";
for (var iii = 0; iii < N_GENES; iii ++) {
all_time += String(Math.floor(best_weights[iii][0])) + " " ;
}
div.innerHTML = all_time;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment