From 0b62a1ca25760bfbaa9e168feaf046d18548629b Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 26 Nov 2024 15:35:25 -0700 Subject: [PATCH] Go-based daemon w/HTTP; new designer --- .gitignore | 14 +-- ctanks.c | 16 --- forftanks.c | 5 + run-tanks | 73 ----------- tanksd.go | 177 ++++++++++++++++++++++++++ upload.cgi.c | 318 ----------------------------------------------- www/designer.mjs | 151 ++++++++++++++++++++++ www/index.html | 99 +++++++++++++++ www/style.css | 33 +++-- www/tank.mjs | 166 +++++++++++++++++++++++++ www/tanks.js | 310 --------------------------------------------- 11 files changed, 623 insertions(+), 739 deletions(-) delete mode 100755 run-tanks create mode 100644 tanksd.go delete mode 100644 upload.cgi.c create mode 100644 www/designer.mjs create mode 100644 www/index.html create mode 100644 www/tank.mjs delete mode 100644 www/tanks.js diff --git a/.gitignore b/.gitignore index cfe294b..ca6c6d5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,16 +2,6 @@ *# *.o -round-*.html -next-round -points forftanks - -designer.cgi -upload.cgi - -forf.html -summary.html -designer.html -intro.html -procs.html +rounds +tanks diff --git a/ctanks.c b/ctanks.c index 76574d8..47d6bd9 100644 --- a/ctanks.c +++ b/ctanks.c @@ -1,19 +1,3 @@ -/* - * 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 diff --git a/forftanks.c b/forftanks.c index 489021a..61f69e1 100644 --- a/forftanks.c +++ b/forftanks.c @@ -445,10 +445,14 @@ print_standings(FILE *f, fprintf(f, " \"tanks\": [\n"); for (int i = 0; i < ntanks; i += 1) { int killer = -1; + int kills = 0; for (int j = 0; j < ntanks; j += 1) { if (tanks[i].killer == &(tanks[j])) { killer = j; } + if (tanks[j].killer == &(tanks[i])) { + kills += 1; + } } if (i > 0) { @@ -459,6 +463,7 @@ print_standings(FILE *f, fprintf(f, " \"path\": \"%s\",\n", ftanks[i].path); fprintf(f, " \"death\": \"%s\",\n", tanks[i].cause_death); fprintf(f, " \"killer\": %d,\n", killer); + fprintf(f, " \"kills\": %d,\n", kills); fprintf(f, " \"errorPos\": %d,\n", ftanks[i].error_pos); fprintf(f, " \"error\": \"%s\",\n", forf_error_str[ftanks[i].env.error]); fprintf(f, " \"sensors\": [\n"); diff --git a/run-tanks b/run-tanks deleted file mode 100755 index da7b5bc..0000000 --- a/run-tanks +++ /dev/null @@ -1,73 +0,0 @@ -#! /bin/sh - -if [ "$#" -gt 0 ]; then - tanks="$@" -else - echo "Usage: $0 tank1 tank2 [...]" - echo "Writes ./next-round and ./summary.html" - exit 1 -fi - -TANKS_GAME=${TANKS_GAME:-forftanks} -NAV_HTML_INC=${NAV_HTML_INC:-./nav.html.inc} export NAV_HTML_INC - -# Add wherever this lives to the search path -PATH=$PATH:$(dirname $0) - -if [ -f next-round ]; then - next=$(cat next-round) -else - next=0 -fi -expr $next + 1 > next-round - -fn=$(printf "round-%04d.html" $next) -rfn=results$$.txt - -# Clean up old games -ofn=$(printf "round-%04d.html" $(expr $next - 720)) -echo "Removing $ofn" -rm -f $ofn - - -echo -n "Running round $next... " -cat <$fn - - - - Tanks Round $next - - - - - - - - -

Tanks Round $next

-
-

0 fps

-EOF -rank.awk $rfn >>$fn -rm -f $rfn -cat $NAV_HTML_INC >>$fn -cat <>$fn - - -EOF - -summary.awk $tanks > summary.html.$$ && mv summary.html.$$ summary.html - -echo "done." - diff --git a/tanksd.go b/tanksd.go new file mode 100644 index 0000000..208f3eb --- /dev/null +++ b/tanksd.go @@ -0,0 +1,177 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path" + "time" +) + +var forftanksPath = flag.String("forftanks", "./forftanks", "path to forftanks executable") +var wwwDir = flag.String("www", "www", "path to www http content (ro)") +var tanksDir = flag.String("tanks", "tanks", "path to tanks state directories (rw)") +var roundsDir = flag.String("rounds", "rounds", "path to rounds storage (rw)") +var maxrounds = flag.Uint("maxrounds", 200, "number of rounds to store") +var maxSize = flag.Uint("maxsize", 8000 , "maximum uploaded file size") +var listenAddr = flag.String("listen", ":8080", "where to listen for incoming HTTP connections") +var roundDuration = flag.Duration("round", 1 * time.Minute, "Time to wait between each round") + +type TankState struct { + dir string + roundsdir string +} + +var validFilenames = []string{ + "author", + "name", + "color", + "program", + "sensor0", + "sensor1", + "sensor2", + "sensor3", + "sensor4", + "sensor5", + "sensor6", + "sensor7", + "sensor8", + "sensor9", +} +func (ts *TankState) ServeHTTP(w http.ResponseWriter, req *http.Request) { + id := req.PathValue("id") + name := req.PathValue("name") + + if req.ContentLength < 0 { + http.Error(w, "Length required", http.StatusLengthRequired) + return + } + if uint(req.ContentLength) > *maxSize { + http.Error(w, "Too large", http.StatusRequestEntityTooLarge) + return + } + + tankDir := path.Join(ts.dir, id) + if tankDir == ts.dir { + http.Error(w, "Invalid tank ID", http.StatusBadRequest) + return + } + + filename := path.Join(tankDir, name) + f, err := os.Create(filename) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer f.Close() + if _, err := io.Copy(f, req.Body); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fmt.Fprintln(w, "file written") +} + +func (ts *TankState) WriteRound(now time.Time, round []byte) error { + dents, err := os.ReadDir(ts.roundsdir) + if err != nil { + return err + } + + for uint(len(dents)) > *maxrounds { + fn := path.Join(ts.roundsdir, dents[0].Name()) + if err := os.Remove(fn); err != nil { + return err + } + dents = dents[1:] + } + + roundFn := fmt.Sprintf("%016x.json", now.Unix()) + roundPath := path.Join(ts.roundsdir, roundFn) + if err := os.WriteFile(roundPath, round, 0644); err != nil { + return err + } + + rounds := make([]string, len(dents) + 1) + for i := 0; i < len(dents); i++ { + rounds[i] = dents[i].Name() + } + rounds[len(dents)] = roundFn + + roundsJs, err := json.Marshal(rounds) + if err != nil { + return err + } + idxFn := path.Join(ts.roundsdir, "index.json") + if err := os.WriteFile(idxFn, roundsJs, 0644); err != nil { + return err + } + + return nil +} + +func (ts *TankState) RunRound(now time.Time) error { + dents, err := os.ReadDir(ts.dir) + if err != nil { + return err + } + + args := make([]string, 0, len(dents)) + for _, dent := range dents { + if dent.IsDir() { + tankPath := path.Join(ts.dir, dent.Name()) + args = append(args, tankPath) + } + } + + if len(args) < 2 { + return fmt.Errorf("Not enough tanks for a round") + } + + cmd := exec.Command(*forftanksPath, args...) + out, err := cmd.Output() + if err != nil { + return err + } + + if err := ts.WriteRound(now, out); err != nil { + return err + } + + return nil +} + +func (ts *TankState) RunForever() { + if err := ts.RunRound(time.Now()); err != nil { + log.Println(err) + } + + for now := range time.Tick(*roundDuration) { + if err := ts.RunRound(now); err != nil { + log.Println(err) + } + } +} + +func main() { + flag.Parse() + + ts := &TankState{ + dir: *tanksDir, + roundsdir: *roundsDir, + } + + http.Handle("GET /", http.FileServer(http.Dir(*wwwDir))) + http.Handle("GET /rounds/", http.StripPrefix("/rounds/", http.FileServer(http.Dir(*roundsDir)))) + http.Handle("PUT /tanks/{id}/{name}", ts) + + go ts.RunForever() + + log.Println("Listening on", *listenAddr) + http.ListenAndServe(*listenAddr, nil) +} diff --git a/upload.cgi.c b/upload.cgi.c deleted file mode 100644 index d3e4a70..0000000 --- a/upload.cgi.c +++ /dev/null @@ -1,318 +0,0 @@ -/* - * 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 -#include -#include - -char *BASE_PATH = ""; - -struct { - char *name; - size_t size; -} entries[] = { - { - "name", 20}, { - "author", 80}, { - "color", 10}, { - "sensor0", 16}, { - "sensor1", 16}, { - "sensor2", 16}, { - "sensor3", 16}, { - "sensor4", 16}, { - "sensor5", 16}, { - "sensor6", 16}, { - "sensor7", 16}, { - "sensor8", 16}, { - "sensor9", 16}, { - "program", 16384}, { - NULL, 0} -}; - - - -size_t inlen; - -int -read_char() -{ - if (inlen) { - inlen -= 1; - return getchar(); - } - return EOF; -} - -char -tonum(int c) -{ - if ((c >= '0') && (c <= '9')) { - return c - '0'; - } - if ((c >= 'a') && (c <= 'f')) { - return 10 + c - 'a'; - } - if ((c >= 'A') && (c <= 'F')) { - return 10 + c - 'A'; - } - return 0; -} - -char -read_hex() -{ - int a = read_char(); - int b = read_char(); - - return tonum(a) * 16 + tonum(b); -} - -/* - * Read a key or a value. Since & and = aren't supposed to appear outside of boundaries, we can use the same function for both. - */ -size_t -read_item(char *str, size_t maxlen) -{ - int c; - size_t pos = 0; - - while (1) { - c = read_char(); - switch (c) { - case EOF: - case '=': - case '&': - str[pos] = '\0'; - return pos; - case '%': - c = read_hex(); - break; - case '+': - c = ' '; - break; - } - if (pos < maxlen - 1) { - str[pos] = c; - pos += 1; - } - } -} - -size_t -copy_item(char *filename, size_t maxlen) -{ - FILE *f; - char path[132]; - int c; - size_t pos = 0; - - snprintf(path, sizeof(path), "%s%05d.%s", BASE_PATH, getpid(), filename); - f = fopen(path, "w"); - if (!f) { - /* - * Just send it to the bit bucket - */ - maxlen = 0; - } - - while (1) { - c = read_char(); - switch (c) { - case EOF: - case '=': - case '&': - if (f) - fclose(f); - return pos; - case '%': - c = read_hex(); - break; - case '+': - c = ' '; - break; - } - if (pos < maxlen) { - fputc(c, f); - pos += 1; - } - } -} - -int -croak(int code, char *msg) -{ - int i; - char path[132]; - - for (i = 0; entries[i].name; i += 1) { - snprintf(path, sizeof(path), "%s%05d.%s", BASE_PATH, getpid(), entries[i].name); - unlink(path); - } - - printf("Status: %d %s\n", code, msg); - printf("Content-type: text/plain\n"); - printf("\n"); - printf("Error: %s\n", msg); - - return 0; -} - -int -main(int argc, char *argv[]) -{ - int sensor[10][5]; - char key[20]; - char token[40]; - size_t len; - - memset(sensor, 0, sizeof(sensor)); - token[0] = '\0'; - - BASE_PATH = getenv("BASE_PATH"); - if (!BASE_PATH) { - BASE_PATH = ""; - } - - { - char *rm = getenv("REQUEST_METHOD"); - - if (!(rm && (0 == strcmp(rm, "POST")))) { - printf("405 Method not allowed\n"); - printf("Allow: POST\n"); - printf("Content-type: text/plain\n"); - printf("\n"); - printf("Error: I only speak POST\n"); - return 0; - } - - inlen = atoi(getenv("CONTENT_LENGTH")); - } - - while (inlen) { - len = read_item(key, sizeof(key)); - if (0 == strcmp(key, "token")) { - read_item(token, sizeof(token)); - } else if ((3 == len) && ('s' == key[0])) { - /* - * sensor dealie, key = "s[0-9][rawt]" - */ - char val[5]; - int n = key[1] - '0'; - int i; - int p; - - read_item(val, sizeof(val)); - - if (!(n >= 0) && (n <= 9)) { - break; - } - i = atoi(val); - - sensor[n][0] = 1; - switch (key[2]) { - case 'r': - p = 1; - break; - case 'a': - p = 2; - break; - case 'w': - p = 3; - break; - default: - p = 4; - i = (val[0] != '\0'); - break; - } - - sensor[n][p] = i; - } else { - int i; - - for (i = 0; entries[i].name; i += 1) { - if (0 == strcmp(key, entries[i].name)) { - len = copy_item(key, entries[i].size); - break; - } - } - } - } - - /* - * Sanitize token - */ - { - char *p = token; - - while (*p) { - if (!isalnum(*p)) { - *p = '_'; - } - p += 1; - } - - if ('\0' == token[0]) { - token[0] = '_'; - token[1] = '\0'; - } - } - - /* - * Move files into their directory - */ - { - char path[132]; - char dest[132]; - struct stat st; - int i; - - snprintf(path, sizeof(path), "%s%s/", BASE_PATH, token); - if (-1 == stat(path, &st)) - return croak(422, "Invalid token"); - if (!S_ISDIR(st.st_mode)) - return croak(422, "Invalid token"); - for (i = 0; entries[i].name; i += 1) { - snprintf(path, sizeof(path), "%s%05d.%s", BASE_PATH, getpid(), entries[i].name); - snprintf(dest, sizeof(dest), "%s%s/%s", BASE_PATH, token, entries[i].name); - rename(path, dest); - } - - for (i = 0; i < 10; i += 1) { - FILE *f; - - if (sensor[i][0]) { - snprintf(dest, sizeof(dest), "%s%s/sensor%d", BASE_PATH, token, i); - f = fopen(dest, "w"); - if (!f) { - break; - } - - fprintf(f, "%d %d %d %d\n", sensor[i][1], sensor[i][2], sensor[i][3], sensor[i][4]); - fclose(f); - } - } - } - - printf("Content-type: text/plain\n"); - printf("\n"); - printf("OK: Tank submitted!\n"); - - return 0; -} diff --git a/www/designer.mjs b/www/designer.mjs new file mode 100644 index 0000000..14c547b --- /dev/null +++ b/www/designer.mjs @@ -0,0 +1,151 @@ +import {Tank} from "./tank.mjs" + +Math.TAU = Math.PI * 2 +const Millisecond = 1 +const Second = 1000 * Millisecond +const Minute = 60 * Second +const TankRPM = 1 +const TurretRPM = 4 + +function deg2rad(angle) { + return angle*Math.TAU/360; +} + +function update(ctx) { + let color = document.querySelector("[name=color]").value + let sensors = [] + + for (let i = 0; i < 10; i += 1) { + let range = document.querySelector(`[name=s${i}r]`).value + let angle = document.querySelector(`[name=s${i}a]`).value + 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, + ] + } + + let tankRevs = -TankRPM * (Date.now() / Minute) + let turretRevs = TurretRPM * (Date.now() / Minute) + let tank = new Tank(ctx, color, sensors); + tank.set_state( + 100, 100, + (tankRevs * Math.TAU) % Math.TAU, + (turretRevs * Math.TAU) % Math.TAU, + 0, + 0, + ) + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) + tank.draw_sensors() + tank.draw_tank() +} + +function formSubmit(event) { + event.preventDefault() + + let formData = new FormData(event.target) + for (let [k, v] of formData.entries()) { + localStorage.setItem(k, v) + } + + let files = { + name: formData.get("name"), + color: formData.get("color"), + author: formData.get("author"), + program: formData.get("program"), + } + for (let i = 0; i < 10; i++) { + let r = formData.get(`s${i}r`) || 0 + let a = formData.get(`s${i}a`) || 0 + let w = formData.get(`s${i}w`) || 0 + let t = (formData.get(`s${i}t`) == "on") ? 1 : 0 + files[`sensor${i}`] = `${r} ${a} ${w} ${t}` + } + + let id = formData.get("id") + let apiURL = new URL(`tanks/${id}/`, location) + + // Fill slots + for (let k in files) { + let v = files[k] + for (let e of document.querySelectorAll(`slot[name="${k}"]`)) { + e.textContent = v + } + } + for (let e of document.querySelectorAll("slot[name=apiurl]")) { + e.textContent = apiURL + } + + // Upload files + for (let k in files) { + let url = new URL(k, apiURL) + let opts = { + method: "PUT", + body: files[k], + } + fetch(url, opts) + .then(resp => { + if (!resp.ok) { + console.error("OH NO") + } + }) + } +} + +function init() { + let canvas = document.querySelector("#design") + let ctx = canvas.getContext("2d") + canvas.width = 200 + canvas.height = 200 + + let form = document.querySelector("form#upload") + form.addEventListener("submit", formSubmit) + + let tbody = document.querySelector("#sensors tbody") + for (let i = 0; i < 10; i++) { + let tr = tbody.appendChild(document.createElement("tr")) + + tr.appendChild(document.createElement("td")).textContent = i + + let range = tr.appendChild(document.createElement("td")).appendChild(document.createElement("input")) + range.name = `s${i}r` + range.type = "number" + range.min = 0 + range.max = 100 + range.value = 0 + + let angle = tr.appendChild(document.createElement("td")).appendChild(document.createElement("input")) + angle.name = `s${i}a` + angle.type = "number" + angle.min = -360 + angle.max = 360 + + let width = tr.appendChild(document.createElement("td")).appendChild(document.createElement("input")) + width.name = `s${i}w` + width.type = "number" + width.min = -360 + width.max = 360 + + let turret = tr.appendChild(document.createElement("td")).appendChild(document.createElement("input")) + turret.name = `s${i}t` + turret.type = "checkbox" + } + + // Load in previous values + for (let e of form.querySelectorAll("[name]")) { + let v = localStorage.getItem(e.name) + if (v !== undefined) { + e.checked = (v == "on") + e.value = v + } + } + + update(ctx) + setInterval(() => update(ctx), Second / 20) +} + +init() diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..2db500f --- /dev/null +++ b/www/index.html @@ -0,0 +1,99 @@ + + + + Tanks + + + + +

Tanks

+ +

Upload a tank

+
+
+ +
+ Program + +
+ +
+ Sensors + + + + + + + + + + + +
RangeAngleWidthTurret?
+
+ +
+ Preview +
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+ +
+
+ + +

Upload using curl

+

+ You can use curl to upload your tank, + if you want. +

+

+ The server does no syntax checking on what you upload. + You'll have to wait until the next round to see if you made a mistake. +

+
+#! /bin/sh
+
+curl -X PUT -d 'fred the tank' https://server/tanks/a1b2c3d4/name
+curl -X PUT -d '#ff0000' https://server/tanks/a1b2c3d4/color
+curl -X PUT -d '50 0 10 1' https://server/tanks/a1b2c3d4/sensor0
+curl -X PUT -d '360 0 5 0' https://server/tanks/a1b2c3d4/sensor1
+curl -X PUT -d '0 0 0 0' https://server/tanks/a1b2c3d4/sensor2
+curl -X PUT -d '0 0 0 0' https://server/tanks/a1b2c3d4/sensor3
+curl -X PUT -d '0 0 0 0' https://server/tanks/a1b2c3d4/sensor4
+curl -X PUT -d '0 0 0 0' https://server/tanks/a1b2c3d4/sensor5
+curl -X PUT -d '0 0 0 0' https://server/tanks/a1b2c3d4/sensor6
+curl -X PUT -d '0 0 0 0' https://server/tanks/a1b2c3d4/sensor7
+curl -X PUT -d '0 0 0 0' https://server/tanks/a1b2c3d4/sensor8
+curl -X PUT -d '0 0 0 0' https://server/tanks/a1b2c3d4/sensor9
+curl -X PUT --data-binary @- https://server/tanks/a1b2c3d4/program <<'EOD'
+20 30 set-speed!
+EOD
+    
+ diff --git a/www/style.css b/www/style.css index 5b526ae..429ec9f 100644 --- a/www/style.css +++ b/www/style.css @@ -7,9 +7,7 @@ html { body { font-family: sans-serif; color: #fff; - margin: 50px 0 0 110px; padding: 10px; - max-width: 700px; } /**** heading ****/ @@ -17,15 +15,8 @@ body { h1:first-child { text-transform: lowercase; font-size: 1.6em; - padding: 3px; color: #2a2; - margin: 0 0 1em 70px; -} - -h1:first-child:before { - color: #fff; - letter-spacing: -0.1em; - content: "Dirtbags: "; + border-bottom: #2a2 solid 2px; } /*** left side bar ***/ @@ -75,6 +66,28 @@ nav li .active { border-right: 4px solid #444; } +/**** designer ****/ +#upload fieldset { + display: flex; + flex-wrap: wrap; + gap: 2em; + align-items: flex-start; +} + +#upload [name="program"] { + min-width: 25em; + min-height: 15em; +} + +#upload fieldset fieldset { + border-color: #666; +} + +#preview { + background: #333; + display: flex; + justify-content: center; +} /**** body ****/ diff --git a/www/tank.mjs b/www/tank.mjs new file mode 100644 index 0000000..34b7b58 --- /dev/null +++ b/www/tank.mjs @@ -0,0 +1,166 @@ +const craterPoints = 7 +const craterAngle = Math.PI / craterPoints + +export class Tank { + constructor(ctx, color, sensors) { + this.ctx = ctx + this.color = color + + // Do all the yucky math up front + this.maxlen = 0 + this.sensors = [] + for (let i in sensors) { + let s = sensors[i] + + if (! s) { + this.sensors[i] = [0,0,0,0] + } else { + // 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.set_state(0, 0, 0, 0, 0) + } + + // Set up our state, for later interleaved draw requests + set_state(x, y, rotation, turret, flags, sensor_state) { + this.x = x + this.y = y + this.rotation = rotation + this.turret = turret + if (flags & 1) { + this.fire = 5 + } + this.led = flags & 2 + this.dead = flags & 4 + this.sensor_state = sensor_state + } + + draw_crater() { + if (!this.dead) { + return + } + + this.ctx.save() + this.ctx.translate(this.x, this.y) + this.ctx.rotate(this.rotation) + + if (this.fire == 5) { + this.ctx.save() + this.ctx.rotate(this.turret) + // one frame of cannon fire + this.draw_cannon() + this.fire = 0 + this.ctx.restore() + } + + this.ctx.lineWidth = 2 + this.ctx.strokeStyle = `rgb(from ${this.color} r g b / 50%)` + this.ctx.fillStyle = `rgb(from ${this.color} r g b / 20%)` + this.ctx.beginPath() + this.ctx.moveTo(12, 0) + for (let i = 0; i < craterPoints; i += 1) { + this.ctx.rotate(craterAngle) + this.ctx.lineTo(6, 0) + this.ctx.rotate(craterAngle) + this.ctx.lineTo(12, 0) + } + this.ctx.closePath() + this.ctx.stroke() + this.ctx.fill() + + this.ctx.restore() + } + + draw_sensors() { + if (this.dead) { + return + } + this.ctx.save() + this.ctx.translate(this.x, this.y) + this.ctx.rotate(this.rotation) + + this.ctx.lineWidth = 1 + for (let i in this.sensors) { + var s = this.sensors[i] + var adj = this.turret * s[3] + + if (this.sensor_state & (1 << i)) { + // Sensor is triggered + this.ctx.strokeStyle = "#000" + } else { + this.ctx.strokeStyle = `rgb(from ${this.color} r g b / 40%)` + } + this.ctx.beginPath() + this.ctx.moveTo(0, 0) + this.ctx.arc(0, 0, s[0], s[1] + adj, s[2] + adj, false) + this.ctx.closePath() + this.ctx.stroke() + } + + this.ctx.restore() + } + + draw_tank() { + if (this.dead) { + return + } + this.ctx.save() + this.ctx.translate(this.x, this.y) + this.ctx.rotate(this.rotation) + + this.ctx.fillStyle = this.color + this.ctx.fillRect(-5, -4, 10, 8) + this.ctx.fillStyle = "#777" + this.ctx.fillRect(-7, -9, 15, 5) + this.ctx.fillRect(-7, 4, 15, 5) + this.ctx.rotate(this.turret) + if (this.fire) { + this.draw_cannon() + this.fire -= 1 + } else { + if (this.led) { + this.ctx.fillStyle = "#f00" + } else { + this.ctx.fillStyle = "#000" + } + this.ctx.fillRect(0, -1, 10, 2) + } + + this.ctx.restore() + } + + draw_cannon() { + this.ctx.fillStyle = ("hsl(0, 100%, 100%, " + this.fire/5 + ")") + this.ctx.fillRect(0, -1, 45, 2) + } + + draw_wrap_sensors() { + let width = this.ctx.canvas.width + let height = this.ctx.canvas.height + let orig_x = this.x + let orig_y = this.y + for (let x = this.x - width; x < width + this.maxlen; x += width) { + for (let y = this.y - height; y < height + this.maxlen; y += height) { + if ((-this.maxlen < x) && (x < width + this.maxlen) && + (-this.maxlen < y) && (y < height + this.maxlen)) { + this.x = x + this.y = y + this.draw_sensors() + } + } + } + this.x = orig_x + this.y = orig_y + } +} + diff --git a/www/tanks.js b/www/tanks.js deleted file mode 100644 index 45fb827..0000000 --- a/www/tanks.js +++ /dev/null @@ -1,310 +0,0 @@ -function dbg(o) { - e = document.getElementById("debug"); - e.innerHTML = o; -} - -function torgba(color, alpha) { - var r = parseInt(color.substring(1,3), 16); - var g = parseInt(color.substring(3,5), 16); - var b = parseInt(color.substring(5,7), 16); - - return "rgba(" + r + "," + g + "," + b + "," + alpha + ")"; -} - -function Tank(ctx, width, height, color, sensors) { - var craterStroke = torgba(color, 0.5); - var craterFill = torgba(color, 0.2); - var sensorStroke = torgba(color, 0.4); - var maxlen = 0; - - this.x = 0; - this.y = 0; - this.rotation = 0; - this.turret = 0; - - this.dead = 0; - - // Do all the yucky math up front - this.sensors = new Array(); - for (i in sensors) { - var s = sensors[i]; - - if (! s) { - this.sensors[i] = [0,0,0,0]; - } else { - // r, angle, width, turret - this.sensors[i] = new Array(); - this.sensors[i][0] = s[0]; - this.sensors[i][1] = s[1] - s[2]/2; - this.sensors[i][2] = s[1] + s[2]/2; - this.sensors[i][3] = s[3]?1:0; - if (s[0] > maxlen) { - maxlen = s[0]; - } - } - } - - // Set up our state, for later interleaved draw requests - this.set_state = function(x, y, rotation, turret, flags, sensor_state) { - this.x = x; - this.y = y; - this.rotation = rotation; - this.turret = turret; - if (flags & 1) { - this.fire = 5; - } - this.led = flags & 2; - this.dead = flags & 4; - this.sensor_state = sensor_state; - } - - this.draw_crater = function() { - if (!this.dead) { - return; - } - - var points = 7; - var angle = Math.PI / points; - - ctx.save(); - ctx.translate(this.x, this.y); - ctx.rotate(this.rotation); - - if (this.fire == 5) { - ctx.save(); - ctx.rotate(this.turret); - // one frame of cannon fire - this.draw_cannon(); - this.fire = 0; - ctx.restore(); - } - - ctx.lineWidth = 2; - ctx.strokeStyle = craterStroke; - ctx.fillStyle = craterFill; - ctx.beginPath(); - ctx.moveTo(12, 0); - for (i = 0; i < points; i += 1) { - ctx.rotate(angle); - ctx.lineTo(6, 0); - ctx.rotate(angle); - ctx.lineTo(12, 0); - } - ctx.closePath() - ctx.stroke(); - ctx.fill(); - - ctx.restore(); - } - - this.draw_sensors = function() { - if (this.dead) { - return; - } - ctx.save(); - ctx.translate(this.x, this.y); - ctx.rotate(this.rotation); - - ctx.lineWidth = 1; - for (i in this.sensors) { - var s = this.sensors[i]; - var adj = this.turret * s[3]; - - if (this.sensor_state & (1 << i)) { - // Sensor is triggered - ctx.strokeStyle = "#000"; - } else { - ctx.strokeStyle = sensorStroke; - } - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.arc(0, 0, s[0], s[1] + adj, s[2] + adj, false); - ctx.closePath(); - ctx.stroke(); - } - - ctx.restore(); - } - - this.draw_tank = function() { - if (this.dead) { - return; - } - ctx.save(); - ctx.translate(this.x, this.y); - ctx.rotate(this.rotation); - - ctx.fillStyle = color; - ctx.fillRect(-5, -4, 10, 8); - ctx.fillStyle = "#777"; - ctx.fillRect(-7, -9, 15, 5); - ctx.fillRect(-7, 4, 15, 5); - ctx.rotate(this.turret); - if (this.fire) { - this.draw_cannon(); - this.fire -= 1; - } else { - if (this.led) { - ctx.fillStyle = "#f00"; - } else { - ctx.fillStyle = "#000"; - } - ctx.fillRect(0, -1, 10, 2); - } - - ctx.restore(); - } - - this.draw_cannon = function() { - ctx.fillStyle = ("rgba(255,255,64," + this.fire/5 + ")"); - ctx.fillRect(0, -1, 45, 2); - } - - this.draw_wrap_sensors = function() { - var orig_x = this.x; - var orig_y = this.y; - for (x = this.x - width; x < width + maxlen; x += width) { - for (y = this.y - height; y < height + maxlen; y += height) { - if ((-maxlen < x) && (x < width + maxlen) && - (-maxlen < y) && (y < height + maxlen)) { - this.x = x; - this.y = y; - this.draw_sensors(); - } - } - } - this.x = orig_x; - this.y = orig_y; - } -} - -var loop_id; -var updateFunc = null; -function togglePlayback() { - if ($("#playing").prop("checked")) { - loop_id = setInterval(updateFunc, 66); - } else { - clearInterval(loop_id); - loop_id = null; - } - $("#pauselabel").toggleClass("ui-icon-play ui-icon-pause"); -} - -function start(id, game) { - var canvas = document.getElementById(id); - var ctx = canvas.getContext('2d'); - - canvas.width = game[0][0]; - canvas.height = game[0][1]; - // game[2] is tank descriptions - var turns = game[2]; - - // Set up tanks - var tanks = new Array(); - for (i in game[1]) { - var desc = game[1][i]; - tanks[i] = new Tank(ctx, game[0][0], game[0][1], desc[0], desc[1]); - } - - var frame = 0; - var lastframe = 0; - var fps = document.getElementById('fps'); - - function update_fps() { - fps.innerHTML = (frame - lastframe); - lastframe = frame; - } - - function drawFrame(idx) { - canvas.width = canvas.width; - turn = turns[idx]; - - // Update and draw craters first - for (i in turn) { - t = turn[i]; - if (!t) { - // old data, force-kill it - tanks[i].fire = 0; - tanks[i].dead = 5; - } else { - tanks[i].set_state(t[0], t[1], t[2], t[3], t[4], t[5]); - } - tanks[i].draw_crater(); - } - // Then sensors - for (i in turn) { - tanks[i].draw_wrap_sensors(); - } - // Then tanks - for (i in turn) { - tanks[i].draw_tank() - } - - document.getElementById('frameid').innerHTML = idx; - } - - function update() { - var idx = frame % (turns.length + 20); - var turn; - - frame += 1; - if (idx >= turns.length) { - return; - } - - drawFrame(idx); - - $('#seekslider').slider('value', idx); - } - - function seekToFrame(newidx) { - var idx = frame % (turns.length + 20); - if (idx !== newidx) { - frame = newidx; - drawFrame(newidx); - } - // make sure we're paused - if ($("#playing").prop("checked")) { - $("#playing").prop("checked", false); - togglePlayback(); - } - } - - updateFunc = update; - loop_id = setInterval(update, 66); - //loop_id = setInterval(update, 400); - if (fps) { - setInterval(update_fps, 1000); - } - - if (id === "battlefield") { - $("#game_box").append('

0

'); - $('#playing').button(); - var slider = $('#seekslider'); - slider.slider({ max: turns.length-1, slide: function(event, ui) { seekToFrame(ui.value); } }); - - var spacing = 100 / turns.length; - var deaths = []; - for (i in turns[0]) { - deaths.push(false); - } - var percent = 0; - for (var f = 0; f < turns.length; f++) { - var turn = turns[f]; - if (percent < (spacing * f)) { - percent = spacing * f; - } - for (var i = 0; i < turn.length; i++) { - if (deaths[i]) { continue; } - if (!turn[i] || (turn[i][4] & 4)) { - deaths[i] = true; - // http://stackoverflow.com/questions/8648963/add-tick-marks-to-jquery-slider - $('').css('left', percent + '%').css('background-color', game[1][i][0]).appendTo(slider); - percent++; - break; - } - } - } - } -} -