Add round visualizer

This commit is contained in:
Neale Pickett 2024-11-26 17:52:30 -07:00
parent 4441520048
commit 554d341d64
8 changed files with 220 additions and 64 deletions

View File

@ -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 <sys/types.h> #include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
@ -40,6 +25,7 @@ struct forftank {
char color[8]; /* "#ff0088" */ char color[8]; /* "#ff0088" */
char name[50]; char name[50];
char *path; char *path;
ino_t inode;
struct forf_stack _prog; struct forf_stack _prog;
struct forf_value _progvals[CSTACK_SIZE]; struct forf_value _progvals[CSTACK_SIZE];
@ -244,7 +230,7 @@ ft_run_tank(struct tank *tank, struct forftank *ftank)
ret = forf_eval(&ftank->env); ret = forf_eval(&ftank->env);
if (! ret) { if (! ret) {
fprintf(stderr, "Error in %s: %s\n", fprintf(stderr, "Error in %s: %s\n",
ftank->name, ftank->path,
forf_error_str[ftank->env.error]); forf_error_str[ftank->env.error]);
} }
} }
@ -340,10 +326,20 @@ ft_read_tank(struct forftank *ftank,
ftank->path = path; 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? */ /* What is your name? */
ret = ft_read_file(ftank->name, sizeof(ftank->name), path, "name"); ret = ft_read_file(ftank->name, sizeof(ftank->name), path, "name");
if (! ret) { if (! ret) {
strncpy(ftank->name, path, sizeof(ftank->name)); snprintf(ftank->name, sizeof(ftank->name), "i:%lx", ftank->inode);
} }
/* What is your quest? */ /* What is your quest? */
@ -459,8 +455,9 @@ print_standings(FILE *f,
fprintf(f, ",\n"); fprintf(f, ",\n");
} }
fprintf(f, " {\n"); fprintf(f, " {\n");
fprintf(f, " \"inode\": %ld,\n", ftanks[i].inode);
fprintf(f, " \"color\": \"%s\",\n", ftanks[i].color); 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, " \"death\": \"%s\",\n", tanks[i].cause_death);
fprintf(f, " \"killer\": %d,\n", killer); fprintf(f, " \"killer\": %d,\n", killer);
fprintf(f, " \"kills\": %d,\n", kills); fprintf(f, " \"kills\": %d,\n", kills);

View File

@ -6,6 +6,7 @@ const Second = 1000 * Millisecond
const Minute = 60 * Second const Minute = 60 * Second
const TankRPM = 1 const TankRPM = 1
const TurretRPM = 4 const TurretRPM = 4
const FPS = 12
function deg2rad(angle) { function deg2rad(angle) {
return angle*Math.TAU/360; return angle*Math.TAU/360;
@ -21,12 +22,12 @@ function update(ctx) {
let width = document.querySelector(`[name=s${i}w]`).value let width = document.querySelector(`[name=s${i}w]`).value
let turret = document.querySelector(`[name=s${i}t]`).checked let turret = document.querySelector(`[name=s${i}t]`).checked
sensors[i] = [ sensors[i] = {
Math.min(range, 100), range: Math.min(range, 100),
deg2rad(angle % 360), angle: deg2rad(angle % 360),
deg2rad(width % 360), width: deg2rad(width % 360),
turret, turret: turret,
] }
} }
let tankRevs = -TankRPM * (Date.now() / Minute) let tankRevs = -TankRPM * (Date.now() / Minute)
@ -81,16 +82,34 @@ function formSubmit(event) {
} }
// Upload files // Upload files
let pending = 0
let errors = 0
let begin = performance.now()
for (let k in files) { for (let k in files) {
let url = new URL(k, apiURL) let url = new URL(k, apiURL)
let opts = { let opts = {
method: "PUT", method: "PUT",
body: files[k], body: files[k],
} }
pending += 1
fetch(url, opts) fetch(url, opts)
.then(resp => { .then(resp => {
pending -= 1
if (!resp.ok) { 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) update(ctx)
setInterval(() => update(ctx), Second / 20) setInterval(() => update(ctx), Second / FPS)
} }
init() init()

View File

@ -59,7 +59,7 @@
<input type="color" name="color" value="#cccccc" required> <input type="color" name="color" value="#cccccc" required>
</div> </div>
<input type="submit" value="Submit"> <input type="submit" value="Upload">
<div id="debug"></div> <div id="debug"></div>
</div> </div>

70
www/player.mjs Normal file
View File

@ -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
}
}

30
www/round.html Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>Tank Round</title>
<link rel="stylesheet" href="style.css">
<script src="round.mjs" type="module"></script>
</head>
<body>
<h1>Tank Round</h1>
<div id="game_box">
<canvas id="battlefield"></canvas>
</div>
<table id="results">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Score</th>
<th>Death</th>
<th>Killer</th>
<th>Error</th>
</tr>
</thead>
<tbody></tbody>
</table>
</body>
</html>

41
www/round.mjs Normal file
View File

@ -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()

View File

@ -185,20 +185,14 @@ dd {
border: 2px solid green; border: 2px solid green;
} }
.solved { table#results {
text-decoration: line-through; border-collapse: collapse;
} }
table#results td {
table.pollster { padding: 0.4em 1em;
margin-left: 5em;
} }
table#results tr:nth-child(even) {
table.pollster td { background-color: #343;
padding: 2px 1em 2px 5px;
}
table.pollster thead {
font-weight: bold;
} }
.swatch { .swatch {

View File

@ -15,15 +15,16 @@ export class Tank {
if (! s) { if (! s) {
this.sensors[i] = [0,0,0,0] this.sensors[i] = [0,0,0,0]
} else { } else {
let r = s.range
// r, angle, width, turret // r, angle, width, turret
this.sensors[i] = [ this.sensors[i] = {
s[0], // Center angle range: s.range,
s[1] - s[2]/2, // Left border angle beg: s.angle - s.width/2,
s[1] + s[2]/2, // Right border angle end: s.angle + s.width/2,
s[3]?1:0, // On turret? turret: s.turret,
] }
if (s[0] > this.maxlen) { if (s.range > this.maxlen) {
this.maxlen = s[0] this.maxlen = s.range
} }
} }
} }
@ -92,8 +93,11 @@ export class Tank {
this.ctx.lineWidth = 1 this.ctx.lineWidth = 1
for (let i in this.sensors) { for (let i in this.sensors) {
var s = this.sensors[i] 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)) { if (this.sensor_state & (1 << i)) {
// Sensor is triggered // Sensor is triggered
this.ctx.strokeStyle = "#000" this.ctx.strokeStyle = "#000"
@ -102,9 +106,10 @@ export class Tank {
} }
this.ctx.beginPath() this.ctx.beginPath()
this.ctx.moveTo(0, 0) 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.closePath()
this.ctx.stroke() this.ctx.stroke()
this.ctx.restore()
} }
this.ctx.restore() this.ctx.restore()