From 554d341d6420eec65bf8815bf7d29060f98f2c62 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 26 Nov 2024 17:52:30 -0700 Subject: [PATCH] Add round visualizer --- forftanks.c | 63 +++++++++++++++++++++---------------------- www/designer.mjs | 35 ++++++++++++++++++------ www/index.html | 2 +- www/player.mjs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ www/round.html | 30 +++++++++++++++++++++ www/round.mjs | 41 ++++++++++++++++++++++++++++ www/style.css | 18 +++++-------- www/tank.mjs | 25 ++++++++++------- 8 files changed, 220 insertions(+), 64 deletions(-) create mode 100644 www/player.mjs create mode 100644 www/round.html create mode 100644 www/round.mjs diff --git a/forftanks.c b/forftanks.c index 61f69e1..b415fbb 100644 --- a/forftanks.c +++ b/forftanks.c @@ -1,20 +1,5 @@ -/* - * This software has been authored by an employee or employees of Los - * Alamos National Security, LLC, operator of the Los Alamos National - * Laboratory (LANL) under Contract No. DE-AC52-06NA25396 with the U.S. - * Department of Energy. The U.S. Government has rights to use, - * reproduce, and distribute this software. The public may copy, - * distribute, prepare derivative works and publicly display this - * software without charge, provided that this Notice and any statement - * of authorship are reproduced on all copies. Neither the Government - * nor LANS makes any warranty, express or implied, or assumes any - * liability or responsibility for the use of this software. If - * software is modified to produce derivative works, such modified - * software should be clearly marked, so as not to confuse it with the - * version available from LANL. - */ - #include +#include #include #include #include @@ -40,6 +25,7 @@ struct forftank { char color[8]; /* "#ff0088" */ char name[50]; char *path; + ino_t inode; struct forf_stack _prog; struct forf_value _progvals[CSTACK_SIZE]; @@ -57,18 +43,18 @@ void forf_print_val(struct forf_value *val) { switch (val->type) { - case forf_type_number: - printf("%ld", val->v.i); - break; - case forf_type_proc: - printf("[proc %p]", val->v.p); - break; - case forf_type_stack_begin: - printf("{"); - break; - case forf_type_stack_end: - printf("}"); - break; + case forf_type_number: + printf("%ld", val->v.i); + break; + case forf_type_proc: + printf("[proc %p]", val->v.p); + break; + case forf_type_stack_begin: + printf("{"); + break; + case forf_type_stack_end: + printf("}"); + break; } } @@ -174,8 +160,8 @@ forf_proc_random(struct forf_env *env) long max = forf_pop_num(env); if (max < 1) { - forf_push_num(env, 0); - return; + forf_push_num(env, 0); + return; } forf_push_num(env, rand() % max); @@ -244,7 +230,7 @@ ft_run_tank(struct tank *tank, struct forftank *ftank) ret = forf_eval(&ftank->env); if (! ret) { fprintf(stderr, "Error in %s: %s\n", - ftank->name, + ftank->path, forf_error_str[ftank->env.error]); } } @@ -340,10 +326,20 @@ ft_read_tank(struct forftank *ftank, ftank->path = path; + /* Store inode */ + { + struct stat s; + if (-1 == stat(path, &s)) { + ftank->inode = -1; + } else { + ftank->inode = s.st_ino; + } + } + /* What is your name? */ ret = ft_read_file(ftank->name, sizeof(ftank->name), path, "name"); if (! ret) { - strncpy(ftank->name, path, sizeof(ftank->name)); + snprintf(ftank->name, sizeof(ftank->name), "i:%lx", ftank->inode); } /* What is your quest? */ @@ -459,8 +455,9 @@ print_standings(FILE *f, fprintf(f, ",\n"); } fprintf(f, " {\n"); + fprintf(f, " \"inode\": %ld,\n", ftanks[i].inode); fprintf(f, " \"color\": \"%s\",\n", ftanks[i].color); - fprintf(f, " \"path\": \"%s\",\n", ftanks[i].path); + fprintf(f, " \"name\": \"%s\",\n", ftanks[i].name); fprintf(f, " \"death\": \"%s\",\n", tanks[i].cause_death); fprintf(f, " \"killer\": %d,\n", killer); fprintf(f, " \"kills\": %d,\n", kills); diff --git a/www/designer.mjs b/www/designer.mjs index 14c547b..cbafc76 100644 --- a/www/designer.mjs +++ b/www/designer.mjs @@ -6,6 +6,7 @@ const Second = 1000 * Millisecond const Minute = 60 * Second const TankRPM = 1 const TurretRPM = 4 +const FPS = 12 function deg2rad(angle) { return angle*Math.TAU/360; @@ -21,12 +22,12 @@ function update(ctx) { let width = document.querySelector(`[name=s${i}w]`).value let turret = document.querySelector(`[name=s${i}t]`).checked - sensors[i] = [ - Math.min(range, 100), - deg2rad(angle % 360), - deg2rad(width % 360), - turret, - ] + sensors[i] = { + range: Math.min(range, 100), + angle: deg2rad(angle % 360), + width: deg2rad(width % 360), + turret: turret, + } } let tankRevs = -TankRPM * (Date.now() / Minute) @@ -81,16 +82,34 @@ function formSubmit(event) { } // Upload files + let pending = 0 + let errors = 0 + let begin = performance.now() for (let k in files) { let url = new URL(k, apiURL) let opts = { method: "PUT", body: files[k], } + pending += 1 fetch(url, opts) .then(resp => { + pending -= 1 if (!resp.ok) { - console.error("OH NO") + errors += 1 + } + if (pending == 0) { + let duration = (performance.now() - begin).toPrecision(2) + let debug = document.querySelector("#debug") + if (debug) { + let msg = `tank uploaded in ${duration}ms` + if (errors > 0) { + msg = msg + `; ${errors} errors` + } + debug.textContent = msg + + setTimeout(() => debug.textContent = "", 2 * Second) + } } }) } @@ -145,7 +164,7 @@ function init() { } update(ctx) - setInterval(() => update(ctx), Second / 20) + setInterval(() => update(ctx), Second / FPS) } init() diff --git a/www/index.html b/www/index.html index 2db500f..f693f1c 100644 --- a/www/index.html +++ b/www/index.html @@ -59,7 +59,7 @@ - +
diff --git a/www/player.mjs b/www/player.mjs new file mode 100644 index 0000000..b60791e --- /dev/null +++ b/www/player.mjs @@ -0,0 +1,70 @@ +import {Tank} from "./tank.mjs" + +const Millisecond = 1 +const Second = 1000 +const FPS = 12 + +export class Player { + constructor(ctx) { + this.ctx = ctx + } + + load(game) { + this.ctx.canvas.width = game.field[0] + this.ctx.canvas.height = game.field[1] + + this.tanks = [] + for (let tankDef of game.tanks) { + let tank = new Tank(this.ctx, tankDef.color, tankDef.sensors) + this.tanks.push(tank) + } + + this.rounds = game.rounds + + this.start() + } + + start(frameno = 0) { + if (!this.loop_id) { + this.loop_id = setInterval( + () => this.update(), + Second / FPS, + ) + } + this.frameno = frameno + } + + stop() { + if (this.loop_id) { + clearInterval(this.loop_id) + } + } + + + update() { + let frame = this.rounds[this.frameno] + if (!frame) { + this.stop() + return + } + + this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height) + + // Update and draw craters first + for (let i in frame) { + let tank = this.tanks[i] + tank.set_state(...frame[i]) + tank.draw_crater() + } + // Then sensors + for (let tank of this.tanks) { + tank.draw_wrap_sensors() + } + // Then tanks + for (let tank of this.tanks) { + tank.draw_tank() + } + + this.frameno += 1 + } +} diff --git a/www/round.html b/www/round.html new file mode 100644 index 0000000..2c11729 --- /dev/null +++ b/www/round.html @@ -0,0 +1,30 @@ + + + + Tank Round + + + + +

Tank Round

+ +
+ +
+ + + + + + + + + + + + + +
NameScoreDeathKillerError
+ + + diff --git a/www/round.mjs b/www/round.mjs new file mode 100644 index 0000000..e562dac --- /dev/null +++ b/www/round.mjs @@ -0,0 +1,41 @@ +import {Player} from "./player.mjs" + +function results(round) { + let tbody = document.querySelector("#results tbody") + + for (let tank of round.tanks) { + let tr = tbody.appendChild(document.createElement("tr")) + + let tdSwatch = tr.appendChild(document.createElement("td")) + tdSwatch.class = "swatch" + tdSwatch.style.backgroundColor = tank.color + tdSwatch.textContent = "#" + + let tdName = tr.appendChild(document.createElement("td")) + tdName.textContent = tank.name + + tr.appendChild(document.createElement("td")).textContent = tank.kills + tr.appendChild(document.createElement("td")).textContent = tank.death + tr.appendChild(document.createElement("td")).textContent = round.tanks[tank.killer]?.name + tr.appendChild(document.createElement("td")).textContent = `${tank.error} @${tank.errorPos}` + } +} + +async function init() { + let canvas = document.querySelector("#battlefield") + let ctx = canvas.getContext("2d") + let player = new Player(ctx) + + let indexResp = await fetch("rounds/index.json") + let index = await indexResp.json() + + let recentFn = index[index.length - 1] + console.log(recentFn) + + let roundResp = await fetch(`rounds/${recentFn}`) + let round = await roundResp.json() + player.load(round) + results(round) +} + +init() diff --git a/www/style.css b/www/style.css index 429ec9f..19719f6 100644 --- a/www/style.css +++ b/www/style.css @@ -185,20 +185,14 @@ dd { border: 2px solid green; } -.solved { - text-decoration: line-through; +table#results { + border-collapse: collapse; } - -table.pollster { - margin-left: 5em; +table#results td { + padding: 0.4em 1em; } - -table.pollster td { - padding: 2px 1em 2px 5px; -} - -table.pollster thead { - font-weight: bold; +table#results tr:nth-child(even) { + background-color: #343; } .swatch { diff --git a/www/tank.mjs b/www/tank.mjs index 34b7b58..acf7f33 100644 --- a/www/tank.mjs +++ b/www/tank.mjs @@ -15,15 +15,16 @@ export class Tank { if (! s) { this.sensors[i] = [0,0,0,0] } else { + let r = s.range // r, angle, width, turret - this.sensors[i] = [ - s[0], // Center angle - s[1] - s[2]/2, // Left border angle - s[1] + s[2]/2, // Right border angle - s[3]?1:0, // On turret? - ] - if (s[0] > this.maxlen) { - this.maxlen = s[0] + this.sensors[i] = { + range: s.range, + beg: s.angle - s.width/2, + end: s.angle + s.width/2, + turret: s.turret, + } + if (s.range > this.maxlen) { + this.maxlen = s.range } } } @@ -92,8 +93,11 @@ export class Tank { this.ctx.lineWidth = 1 for (let i in this.sensors) { var s = this.sensors[i] - var adj = this.turret * s[3] + this.ctx.save() + if (s.turret) { + this.ctx.rotate(this.turret) + } if (this.sensor_state & (1 << i)) { // Sensor is triggered this.ctx.strokeStyle = "#000" @@ -102,9 +106,10 @@ export class Tank { } this.ctx.beginPath() this.ctx.moveTo(0, 0) - this.ctx.arc(0, 0, s[0], s[1] + adj, s[2] + adj, false) + this.ctx.arc(0, 0, s.range, s.beg, s.end, false) this.ctx.closePath() this.ctx.stroke() + this.ctx.restore() } this.ctx.restore()