Created
March 4, 2026 03:41
-
-
Save evanthebouncy/6db689afe182523a30d31cfb466512f3 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> | |
| <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