diff options
author | Ilion Beyst <ilion.beyst@gmail.com> | 2022-10-24 07:19:52 +0200 |
---|---|---|
committer | Ilion Beyst <ilion.beyst@gmail.com> | 2022-10-24 07:19:52 +0200 |
commit | e3164246e1733c717072b804566a485458769314 (patch) | |
tree | 1149693d8e958073ee24915bfe69cb1317287396 | |
parent | 03cdbbc003b102156194568a7b8fc52d636f87f4 (diff) | |
download | planetwars.dev-e3164246e1733c717072b804566a485458769314.tar.xz planetwars.dev-e3164246e1733c717072b804566a485458769314.zip |
create simple stats page
-rw-r--r-- | web/pw-server/src/routes/bots/[bot_name]/index.svelte (renamed from web/pw-server/src/routes/bots/[bot_name].svelte) | 9 | ||||
-rw-r--r-- | web/pw-server/src/routes/bots/[bot_name]/stats.svelte | 169 |
2 files changed, 175 insertions, 3 deletions
diff --git a/web/pw-server/src/routes/bots/[bot_name].svelte b/web/pw-server/src/routes/bots/[bot_name]/index.svelte index 58e89ad..6e93834 100644 --- a/web/pw-server/src/routes/bots/[bot_name].svelte +++ b/web/pw-server/src/routes/bots/[bot_name]/index.svelte @@ -5,8 +5,10 @@ const apiClient = new ApiClient(fetch); try { - const [botData, matchesPage] = await Promise.all([ - apiClient.get(`/api/bots/${params["bot_name"]}`), + const bot_name = params["bot_name"]; + const [botData, botStats, matchesPage] = await Promise.all([ + apiClient.get(`/api/bots/${bot_name}`), + apiClient.get(`/api/bots/${bot_name}/stats`), apiClient.get("/api/matches", { bot: params["bot_name"], count: "20" }), ]); @@ -19,6 +21,7 @@ bot, owner, versions, + botStats, matches: matchesPage["matches"], }, }; @@ -41,7 +44,7 @@ export let owner: object; export let versions: object[]; export let matches: object[]; - + export let botStats: object; // function last_updated() { // versions.sort() // } diff --git a/web/pw-server/src/routes/bots/[bot_name]/stats.svelte b/web/pw-server/src/routes/bots/[bot_name]/stats.svelte new file mode 100644 index 0000000..6b5a2e1 --- /dev/null +++ b/web/pw-server/src/routes/bots/[bot_name]/stats.svelte @@ -0,0 +1,169 @@ +<script lang="ts" context="module"> + import { ApiClient } from "$lib/api_client"; + + export async function load({ params, fetch }) { + const apiClient = new ApiClient(fetch); + + try { + const bot_name = params["bot_name"]; + const [botData, botStats, leaderboard] = await Promise.all([ + apiClient.get(`/api/bots/${bot_name}`), + apiClient.get(`/api/bots/${bot_name}/stats`), + apiClient.get("/api/leaderboard"), + ]); + + const { bot, owner } = botData; + return { + props: { + bot, + owner, + botStats, + leaderboard, + }, + }; + } catch (error) { + return { + status: error.status, + error: error, + }; + } + } + + function mergedStats(rawStats: object) { + return Object.fromEntries( + Object.entries(rawStats).map(([opponent, ms]) => { + const mapStats = ms as { k: MatchupStats }; + return [opponent, Object.values(mapStats).reduce(mergeStats)]; + }) + ); + } + + type MatchupStats = { + win: number; + tie: number; + loss: number; + }; + + function winRate(stats: MatchupStats) { + return (stats.win + 0.5 * stats.tie) / (stats.win + stats.tie + stats.loss); + } + + function mergeStats(a: MatchupStats, b: MatchupStats): MatchupStats { + return { + win: a.win + b.win, + tie: a.tie + b.tie, + loss: a.loss + b.loss, + }; + } +</script> + +<script lang="ts"> + export let bot: object; + export let owner: object; + export let botStats: object; + export let leaderboard: object[]; + + $: mergedStats = mergedStats(botStats); +</script> + +<div class="container"> + <div class="header"> + <h1 class="bot-name">{bot["name"]}</h1> + {#if owner} + <a class="owner-name" href="/users/{owner['username']}"> + {owner["username"]} + </a> + {/if} + </div> + <h2>Stats</h2> + <table class="leaderboard"> + <tr class="leaderboard-row leaderboard-header"> + <th class="leaderboard-rank">Rank</th> + <th class="leaderboard-rating">Rating</th> + <th class="leaderboard-bot">Bot</th> + <th class="leaderboard-author">Author</th> + <th>Winrate</th> + <th>Matches</th> + </tr> + {#each leaderboard as entry, index} + <tr class="leaderboard-row"> + <td class="leaderboard-rank">{index + 1}</td> + <td class="leaderboard-rating"> + {entry["rating"].toFixed(0)} + </td> + <td class="leaderboard-bot"> + <a class="leaderboard-href" href="/bots/{entry['bot']['name']}" + >{entry["bot"]["name"]} + </a></td + > + <td class="leaderboard-author"> + {#if entry["author"]} + <!-- TODO: remove duplication --> + <a class="leaderboard-href" href="/users/{entry['author']['username']}" + >{entry["author"]["username"]}</a + > + {/if} + </td> + {#if mergedStats[entry["bot"]["name"]]} + <td> + {winRate(mergedStats[entry["bot"]["name"]]).toFixed(2)} + </td> + <td> + <a href={`/matches?bot=${bot["name"]}&opponent=${entry["bot"]["name"]}`}>view matches</a + > + </td> + {:else} + <td /> + <td>no matches yet </td>{/if} + </tr> + {/each} + </table> +</div> + +<style lang="scss"> + .container { + width: 800px; + max-width: 80%; + margin: 50px auto; + } + + .header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 60px; + border-bottom: 1px solid black; + } + + $header-space-above-line: 12px; + + .bot-name { + font-size: 24pt; + margin-bottom: $header-space-above-line; + } + + .owner-name { + font-size: 14pt; + text-decoration: none; + color: #333; + margin-bottom: $header-space-above-line; + } + + .leaderboard { + margin: 18px 10px; + text-align: center; + } + + .leaderboard th, + .leaderboard td { + padding: 8px 16px; + } + .leaderboard-rank { + color: #333; + } + + .leaderboard-href { + text-decoration: none; + color: black; + } +</style> |