tanks

Blow up enemy tanks using code
git clone https://git.woozle.org/neale/tanks.git

tanks / www
Neale Pickett  ·  2024-12-05

replay.mjs

  1import {Player} from "./player.mjs"
  2
  3const Millisecond = 1
  4const Second = 1000 * Millisecond
  5const Minute = 60 * Second
  6
  7const GameTime = new Intl.DateTimeFormat(
  8    undefined,
  9    {
 10        dateStyle: "short",
 11        timeStyle: "short",
 12    },
 13)
 14
 15function dateOfFn(fn) {
 16    let ts = parseInt(fn.replace(/\.json$/, ""), 16) * Second
 17    let when = new Date(ts)
 18    return GameTime.format(when)
 19}
 20
 21class Replay {
 22    constructor(ctx, select, results, dateOutput, stats) {
 23        this.ctx = ctx
 24        this.select = select
 25        this.results = results
 26        this.dateOutput = dateOutput
 27        this.stats = stats
 28        this.player = new Player(ctx)
 29        this.games = {}
 30
 31        select.addEventListener("change", () => this.reloadPlayer())
 32    }
 33
 34    async refreshSingleGame(fn) {
 35        if (this.games[fn]) {
 36            return
 37        }
 38        let resp = await fetch(`rounds/${fn}`)
 39        let game = await resp.json()
 40        this.games[fn] = game
 41    }
 42
 43    async refreshGames() {
 44        let resp = await fetch("rounds/index.json")
 45        let index = await resp.json()
 46        let promises = []
 47        for (let fn of index) {
 48            let p = this.refreshSingleGame(fn)
 49            promises.push(p)
 50        }
 51        for (let fn in this.games) {
 52            if (!index.includes(fn)) {
 53                delete this.games[fn]
 54            }
 55        }
 56        await Promise.all(promises)
 57    }
 58
 59    fns() {
 60        return Object.keys(this.games).toSorted()
 61    }
 62
 63    async refresh() {
 64        await this.refreshGames()
 65
 66        let newGame = false
 67        let latest = this.select.firstElementChild
 68        for (let fn of this.fns()) {
 69            let game = this.games[fn]
 70            if (!game.element) {
 71                game.element = document.createElement("option")
 72                newGame = true
 73            }
 74            game.element.value = fn
 75            game.element.textContent = dateOfFn(fn)
 76
 77            // This moves existing elements in Chrome.
 78            // But the MDN document doesn't specify what happens with existing elements.
 79            // So this may break somewhere.
 80            latest.after(game.element)
 81        }
 82
 83        // Remove anything no longer in games
 84        for (let option of this.select.querySelectorAll("[value]")) {
 85            let fn = option.value
 86            if (!(fn in this.games)) {
 87                option.remove()
 88            }
 89        }
 90
 91        if (newGame) {
 92            this.reloadStats()
 93        }
 94
 95        if (newGame && (this.select.value == "latest")) {
 96            this.reloadPlayer()
 97        }
 98    }
 99
100    reloadStats() {
101        let TankNames = {}
102        let TankColors = {}
103        let TotalGames = {}
104        let TotalKills = {}
105        let TotalDeaths = {}
106        let MaxKills = 0
107        let MaxDeaths = 0
108        let ngames = 0
109        for (let fn of this.fns()) {
110            let game = this.games[fn]
111            for (let tank of game.tanks) {
112                TotalGames[tank.uid] = (TotalGames[tank.uid] ?? 0) + 1
113                TotalKills[tank.uid] = (TotalKills[tank.uid] ?? 0) + tank.kills
114                TotalDeaths[tank.uid] = (TotalDeaths[tank.uid] ?? 0) + (tank.killer == -1 ? 0 : 1)
115                TankNames[tank.uid] = tank.name
116                TankColors[tank.uid] = tank.color
117
118                MaxKills = Math.max(MaxKills, TotalKills[tank.uid])
119                MaxDeaths = Math.max(MaxDeaths, TotalDeaths[tank.uid])
120            }
121            ngames++
122        }
123
124        let tbody = this.stats.querySelector("tbody")
125        tbody.replaceChildren()
126
127        let byKills = Object.keys(TankNames)
128        byKills.sort((a, b) => TotalKills[a] - TotalKills[b])
129        byKills.reverse()
130
131        for (let uid of byKills) {
132            let tr = tbody.appendChild(document.createElement("tr"))
133
134            let tdSwatch = tr.appendChild(document.createElement("td"))
135            tdSwatch.class = "swatch"
136            tdSwatch.style.backgroundColor = TankColors[uid]
137            tdSwatch.textContent = "#"
138
139            tr.appendChild(document.createElement("td")).textContent = TankNames[uid]
140            tr.appendChild(document.createElement("td")).textContent = TotalGames[uid]
141            tr.appendChild(document.createElement("td")).textContent = TotalKills[uid]
142            tr.appendChild(document.createElement("td")).textContent = TotalDeaths[uid]
143
144            let award = []
145            if (TotalGames[uid] < 20) {
146                award.push("noob")
147            }
148            if (TotalKills[uid] == MaxKills) {
149                award.push("ruthless")
150            }
151            if (TotalDeaths[uid] == MaxDeaths) {
152                award.push("punished")
153            }
154            if (TotalDeaths[uid] == 0) {
155                award.push("invincible")
156            }
157            if (TotalDeaths[uid] < ngames*0.33) {
158                award.push("stealthy")
159            }
160            if (TotalDeaths[uid] == ngames) {
161                award.push("wasted")
162            }
163            if (TotalGames[uid] > ngames) {
164                award.push("overachiever")
165            }
166            if (TotalKills[uid] == TotalDeaths[uid]) {
167                award.push("balanced")
168            }
169            tr.appendChild(document.createElement("td")).textContent = award.join(", ")
170        }
171    }
172
173    async reloadPlayer() {
174        let fn = this.select.value
175        if (fn == "latest") {
176            let fns = this.fns()
177            fn = fns[fns.length - 1]
178        }
179        let game = this.games[fn]
180
181        this.player.load(game)
182
183        this.dateOutput.value = dateOfFn(fn)
184
185        let tbody = this.results.querySelector("tbody")
186        tbody.replaceChildren()
187
188        let byKills = Object.keys(game.tanks)
189        byKills.sort((a, b) => game.tanks[a].kills - game.tanks[b].kills)
190        byKills.reverse()
191
192        for (let i of byKills) {
193            let tank = game.tanks[i]
194            let tr = tbody.appendChild(document.createElement("tr"))
195
196            let tdSwatch = tr.appendChild(document.createElement("td"))
197            tdSwatch.class = "swatch"
198            tdSwatch.style.backgroundColor = tank.color
199            tdSwatch.textContent = "#"
200
201            let tdName = tr.appendChild(document.createElement("td"))
202            tdName.textContent = tank.name
203            
204            tr.appendChild(document.createElement("td")).textContent = tank.kills
205            tr.appendChild(document.createElement("td")).textContent = tank.death
206            tr.appendChild(document.createElement("td")).textContent = game.tanks[tank.killer]?.name
207            tr.appendChild(document.createElement("td")).textContent = `${tank.error} @${tank.errorPos}`
208        }
209    }
210}
211
212
213export function init() {
214    let replay = new Replay(
215        document.querySelector("canvas#battlefield").getContext("2d"),
216        document.querySelector("select#game"),
217        document.querySelector("table#results"),
218        document.querySelector("output#date"),
219        document.querySelector("table#stats"),
220    )
221    replay.refresh()
222    setInterval(() => replay.refresh(), Minute / 5)
223}