aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIlion Beyst <ilion.beyst@gmail.com>2022-10-24 07:19:52 +0200
committerIlion Beyst <ilion.beyst@gmail.com>2022-10-24 07:19:52 +0200
commite3164246e1733c717072b804566a485458769314 (patch)
tree1149693d8e958073ee24915bfe69cb1317287396
parent03cdbbc003b102156194568a7b8fc52d636f87f4 (diff)
downloadplanetwars.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.svelte169
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>