mirror of https://github.com/dirtbags/tanks.git
Add overall rankings. Playable now.
This commit is contained in:
parent
d773ffcede
commit
9d3cce9bf6
12
forftanks.c
12
forftanks.c
|
@ -25,7 +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;
|
unsigned int uid;
|
||||||
|
|
||||||
struct forf_stack _prog;
|
struct forf_stack _prog;
|
||||||
struct forf_value _progvals[CSTACK_SIZE];
|
struct forf_value _progvals[CSTACK_SIZE];
|
||||||
|
@ -326,20 +326,20 @@ ft_read_tank(struct forftank *ftank,
|
||||||
|
|
||||||
ftank->path = path;
|
ftank->path = path;
|
||||||
|
|
||||||
/* Store inode */
|
/* Store uid */
|
||||||
{
|
{
|
||||||
struct stat s;
|
struct stat s;
|
||||||
if (-1 == stat(path, &s)) {
|
if (-1 == stat(path, &s)) {
|
||||||
ftank->inode = -1;
|
ftank->uid = 0;
|
||||||
} else {
|
} else {
|
||||||
ftank->inode = s.st_ino;
|
ftank->uid = (unsigned int)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) {
|
||||||
snprintf(ftank->name, sizeof(ftank->name), "i:%lx", ftank->inode);
|
snprintf(ftank->name, sizeof(ftank->name), "i:%x", ftank->uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* What is your quest? */
|
/* What is your quest? */
|
||||||
|
@ -455,7 +455,7 @@ 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, " \"uid\": %d,\n", ftanks[i].uid);
|
||||||
fprintf(f, " \"color\": \"%s\",\n", ftanks[i].color);
|
fprintf(f, " \"color\": \"%s\",\n", ftanks[i].color);
|
||||||
fprintf(f, " \"name\": \"%s\",\n", ftanks[i].name);
|
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);
|
||||||
|
|
25
tanksd.go
25
tanksd.go
|
@ -84,11 +84,18 @@ func (ts *TankState) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TankState) WriteRound(now time.Time, round []byte) error {
|
func (ts *TankState) WriteRound(now time.Time, round []byte) error {
|
||||||
|
// Write new round
|
||||||
|
roundFn := fmt.Sprintf("%016x.json", now.Unix())
|
||||||
|
roundPath := path.Join(ts.roundsdir, roundFn)
|
||||||
|
if err := os.WriteFile(roundPath, round, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up and index all rounds
|
||||||
dents, err := os.ReadDir(ts.roundsdir)
|
dents, err := os.ReadDir(ts.roundsdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for uint(len(dents)) > *maxrounds {
|
for uint(len(dents)) > *maxrounds {
|
||||||
fn := path.Join(ts.roundsdir, dents[0].Name())
|
fn := path.Join(ts.roundsdir, dents[0].Name())
|
||||||
if err := os.Remove(fn); err != nil {
|
if err := os.Remove(fn); err != nil {
|
||||||
|
@ -97,17 +104,15 @@ func (ts *TankState) WriteRound(now time.Time, round []byte) error {
|
||||||
dents = dents[1:]
|
dents = dents[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
roundFn := fmt.Sprintf("%016x.json", now.Unix())
|
rounds := make([]string, 0, len(dents))
|
||||||
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++ {
|
for i := 0; i < len(dents); i++ {
|
||||||
rounds[i] = dents[i].Name()
|
name := dents[i].Name()
|
||||||
|
switch name {
|
||||||
|
case "index.json":
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rounds = append(rounds, name)
|
||||||
}
|
}
|
||||||
rounds[len(dents)] = roundFn
|
|
||||||
|
|
||||||
roundsJs, err := json.Marshal(rounds)
|
roundsJs, err := json.Marshal(rounds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -132,7 +132,7 @@ EOD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
export function init() {
|
||||||
let canvas = document.querySelector("#design")
|
let canvas = document.querySelector("#design")
|
||||||
let ctx = canvas.getContext("2d")
|
let ctx = canvas.getContext("2d")
|
||||||
canvas.width = 200
|
canvas.width = 200
|
||||||
|
@ -184,4 +184,3 @@ function init() {
|
||||||
setInterval(() => update(ctx), Second / FPS)
|
setInterval(() => update(ctx), Second / FPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<title>Tanks</title>
|
<title>Tanks</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="designer.mjs" type="module"></script>
|
<script src="main.mjs" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Tanks</h1>
|
<h1>Tanks</h1>
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
<h2>Upload a tank</h2>
|
<h2>Upload a tank</h2>
|
||||||
<form id="upload">
|
<form id="upload">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><input type="text" name="name" placeholder="My tank name" required></legend>
|
<legend>Tank Designer</legend>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Program</legend>
|
<legend>Program</legend>
|
||||||
<textarea name="program">30 40 set-speed!
|
<textarea name="program">30 40 set-speed!
|
||||||
|
@ -44,8 +44,14 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<label>ID:
|
<label>Token:
|
||||||
<input type="text" name="id" placeholder="ID provided by game owner" required>
|
<input type="text" name="id" placeholder="Token provided by game owner" required>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Name:
|
||||||
|
<input type="text" name="name" placeholder="Tank name" required>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -67,5 +73,59 @@
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>
|
||||||
|
Game Replay
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<select id="game">
|
||||||
|
<option>latest</option>
|
||||||
|
</select>
|
||||||
|
<output id="date">moo</output>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>
|
||||||
|
Statistics
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<table id="stats">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Games</th>
|
||||||
|
<th>Kills</th>
|
||||||
|
<th>Killed</th>
|
||||||
|
<th>Awards</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import * as designer from "./designer.mjs"
|
||||||
|
import * as replay from "./replay.mjs"
|
||||||
|
|
||||||
|
designer.init()
|
||||||
|
replay.init()
|
|
@ -1,7 +1,7 @@
|
||||||
import {Tank} from "./tank.mjs"
|
import {Tank} from "./tank.mjs"
|
||||||
|
|
||||||
const Millisecond = 1
|
const Millisecond = 1
|
||||||
const Second = 1000
|
const Second = 1000 * Millisecond
|
||||||
const FPS = 12
|
const FPS = 12
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
|
@ -75,3 +75,4 @@ export class Player {
|
||||||
this.frameno += 1
|
this.frameno += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,217 @@
|
||||||
|
import {Player} from "./player.mjs"
|
||||||
|
|
||||||
|
const Millisecond = 1
|
||||||
|
const Second = 1000 * Millisecond
|
||||||
|
const Minute = 60 * Second
|
||||||
|
|
||||||
|
const GameTime = new Intl.DateTimeFormat(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
dateStyle: "short",
|
||||||
|
timeStyle: "short",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function dateOfFn(fn) {
|
||||||
|
let ts = parseInt(fn.replace(/\.json$/, ""), 16) * Second
|
||||||
|
let when = new Date(ts)
|
||||||
|
return GameTime.format(when)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Replay {
|
||||||
|
constructor(ctx, select, results, dateOutput, stats) {
|
||||||
|
this.ctx = ctx
|
||||||
|
this.select = select
|
||||||
|
this.results = results
|
||||||
|
this.dateOutput = dateOutput
|
||||||
|
this.stats = stats
|
||||||
|
this.player = new Player(ctx)
|
||||||
|
this.games = {}
|
||||||
|
|
||||||
|
select.addEventListener("change", () => this.reloadPlayer())
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshSingleGame(fn) {
|
||||||
|
if (this.games[fn]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let resp = await fetch(`rounds/${fn}`)
|
||||||
|
let game = await resp.json()
|
||||||
|
this.games[fn] = game
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshGames() {
|
||||||
|
let resp = await fetch("rounds/index.json")
|
||||||
|
let index = await resp.json()
|
||||||
|
let promises = []
|
||||||
|
for (let fn of index) {
|
||||||
|
let p = this.refreshSingleGame(fn)
|
||||||
|
promises.push(p)
|
||||||
|
}
|
||||||
|
for (let fn in this.games) {
|
||||||
|
if (!index.includes(fn)) {
|
||||||
|
delete this.games[fn]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
fns() {
|
||||||
|
return Object.keys(this.games).toSorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
await this.refreshGames()
|
||||||
|
|
||||||
|
let newGame = false
|
||||||
|
let latest = this.select.firstElementChild
|
||||||
|
for (let fn of this.fns()) {
|
||||||
|
let game = this.games[fn]
|
||||||
|
if (!game.element) {
|
||||||
|
game.element = document.createElement("option")
|
||||||
|
newGame = true
|
||||||
|
}
|
||||||
|
game.element.value = fn
|
||||||
|
game.element.textContent = dateOfFn(fn)
|
||||||
|
|
||||||
|
// This moves existing elements in Chrome.
|
||||||
|
// But the MDN document doesn't specify what happens with existing elements.
|
||||||
|
// So this may break somewhere.
|
||||||
|
latest.after(game.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove anything no longer in games
|
||||||
|
for (let option of this.select.querySelectorAll("[value]")) {
|
||||||
|
let fn = option.value
|
||||||
|
if (!(fn in this.games)) {
|
||||||
|
option.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newGame) {
|
||||||
|
this.reloadStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newGame && (this.select.value == "latest")) {
|
||||||
|
this.reloadPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadStats() {
|
||||||
|
let TankNames = {}
|
||||||
|
let TankColors = {}
|
||||||
|
let TotalGames = {}
|
||||||
|
let TotalKills = {}
|
||||||
|
let TotalDeaths = {}
|
||||||
|
let MaxKills = 0
|
||||||
|
let MaxDeaths = 0
|
||||||
|
let ngames = 0
|
||||||
|
for (let fn of this.fns()) {
|
||||||
|
let game = this.games[fn]
|
||||||
|
for (let tank of game.tanks) {
|
||||||
|
TotalGames[tank.uid] = (TotalGames[tank.uid] ?? 0) + 1
|
||||||
|
TotalKills[tank.uid] = (TotalKills[tank.uid] ?? 0) + tank.kills
|
||||||
|
TotalDeaths[tank.uid] = (TotalDeaths[tank.uid] ?? 0) + (tank.killer == -1 ? 0 : 1)
|
||||||
|
TankNames[tank.uid] = tank.name
|
||||||
|
TankColors[tank.uid] = tank.color
|
||||||
|
|
||||||
|
MaxKills = Math.max(MaxKills, TotalKills[tank.uid])
|
||||||
|
MaxDeaths = Math.max(MaxDeaths, TotalDeaths[tank.uid])
|
||||||
|
}
|
||||||
|
ngames++
|
||||||
|
}
|
||||||
|
|
||||||
|
let tbody = this.stats.querySelector("tbody")
|
||||||
|
console.log(this.stats, tbody)
|
||||||
|
tbody.replaceChildren()
|
||||||
|
|
||||||
|
let byKills = Object.keys(TankNames)
|
||||||
|
byKills.sort((a, b) => TotalKills[a] - TotalKills[b])
|
||||||
|
byKills.reverse()
|
||||||
|
|
||||||
|
for (let uid of byKills) {
|
||||||
|
let tr = tbody.appendChild(document.createElement("tr"))
|
||||||
|
|
||||||
|
let tdSwatch = tr.appendChild(document.createElement("td"))
|
||||||
|
tdSwatch.class = "swatch"
|
||||||
|
tdSwatch.style.backgroundColor = TankColors[uid]
|
||||||
|
tdSwatch.textContent = "#"
|
||||||
|
|
||||||
|
tr.appendChild(document.createElement("td")).textContent = TankNames[uid]
|
||||||
|
tr.appendChild(document.createElement("td")).textContent = TotalGames[uid]
|
||||||
|
tr.appendChild(document.createElement("td")).textContent = TotalKills[uid]
|
||||||
|
tr.appendChild(document.createElement("td")).textContent = TotalDeaths[uid]
|
||||||
|
|
||||||
|
let award = []
|
||||||
|
if (TotalGames[uid] < 20) {
|
||||||
|
award.push("noob")
|
||||||
|
}
|
||||||
|
if (TotalKills[uid] == MaxKills) {
|
||||||
|
award.push("ruthless")
|
||||||
|
}
|
||||||
|
if (TotalDeaths[uid] == MaxDeaths) {
|
||||||
|
award.push("punished")
|
||||||
|
}
|
||||||
|
if (TotalDeaths[uid] == 0) {
|
||||||
|
award.push("invincible")
|
||||||
|
}
|
||||||
|
if (TotalDeaths[uid] == ngames) {
|
||||||
|
award.push("wasted")
|
||||||
|
}
|
||||||
|
if (TotalGames[uid] > ngames) {
|
||||||
|
award.push("overachiever")
|
||||||
|
}
|
||||||
|
if (TotalKills[uid] == TotalDeaths[uid]) {
|
||||||
|
award.push("balanced")
|
||||||
|
}
|
||||||
|
tr.appendChild(document.createElement("td")).textContent = award.join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadPlayer() {
|
||||||
|
let fn = this.select.value
|
||||||
|
if (fn == "latest") {
|
||||||
|
let fns = this.fns()
|
||||||
|
fn = fns[fns.length - 1]
|
||||||
|
}
|
||||||
|
let game = this.games[fn]
|
||||||
|
|
||||||
|
this.player.load(game)
|
||||||
|
console.log(fn)
|
||||||
|
|
||||||
|
this.dateOutput.value = dateOfFn(fn)
|
||||||
|
|
||||||
|
let tbody = this.results.querySelector("tbody")
|
||||||
|
tbody.replaceChildren()
|
||||||
|
|
||||||
|
for (let tank of game.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 = game.tanks[tank.killer]?.name
|
||||||
|
tr.appendChild(document.createElement("td")).textContent = `${tank.error} @${tank.errorPos}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
let replay = new Replay(
|
||||||
|
document.querySelector("canvas#battlefield").getContext("2d"),
|
||||||
|
document.querySelector("select#game"),
|
||||||
|
document.querySelector("table#results"),
|
||||||
|
document.querySelector("output#date"),
|
||||||
|
document.querySelector("table#stats"),
|
||||||
|
)
|
||||||
|
replay.refresh()
|
||||||
|
setInterval(() => replay.refresh(), Minute / 5)
|
||||||
|
}
|
|
@ -186,13 +186,13 @@ dd {
|
||||||
border: 2px solid green;
|
border: 2px solid green;
|
||||||
}
|
}
|
||||||
|
|
||||||
table#results {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
table#results td {
|
table td {
|
||||||
padding: 0.4em 1em;
|
padding: 0.4em 1em;
|
||||||
}
|
}
|
||||||
table#results tr:nth-child(even) {
|
table tr:nth-child(even) {
|
||||||
background-color: #343;
|
background-color: #343;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue