design new BotMatch view

This commit is contained in:
Ilion Beyst 2022-10-30 14:37:38 +01:00
parent 67c8a2780c
commit 00d31df58d
10 changed files with 269 additions and 15 deletions

View file

@ -0,0 +1,20 @@
export type Match = {
id: number;
timestamp: string;
state: string;
players: MatchPlayer[];
winner: number;
map: Map;
};
export type MatchPlayer = {
bot_id: number;
bot_version_id: number;
bot_name: string;
owner_id?: number;
had_errors?: boolean;
};
export type Map = {
name: string;
};

View file

@ -3,6 +3,7 @@
export let matchLog: string; export let matchLog: string;
export let playerId: number; export let playerId: number;
export let showStdErr: boolean = true;
let playerLog: PlayerLog; let playerLog: PlayerLog;
@ -66,7 +67,7 @@
<div class="bad-command-error">Parse error: {logTurn.action.error}</div> <div class="bad-command-error">Parse error: {logTurn.action.error}</div>
</div> </div>
{/if} {/if}
{#if logTurn.stderr.length > 0} {#if showStdErr && logTurn.stderr.length > 0}
<div class="stderr-header">stderr</div> <div class="stderr-header">stderr</div>
<div class="stderr-text-box"> <div class="stderr-text-box">
{#each logTurn.stderr as stdErrMsg} {#each logTurn.stderr as stdErrMsg}

View file

@ -122,13 +122,7 @@
</div> </div>
<span>Map</span> <span>Map</span>
<div class="map-select"> <div class="map-select">
<Select <Select itemId="name" label="name" items={maps} bind:value={$selectedMap} clearable={false} />
itemId="name"
label="name"
items={maps}
bind:value={$selectedMap}
clearable={false}
/>
</div> </div>
<button class="submit-button play-button" on:click={submitBot}>Play</button> <button class="submit-button play-button" on:click={submitBot}>Play</button>
</div> </div>

View file

@ -0,0 +1,104 @@
<script lang="ts">
import type { BotMatch } from "$lib/matches";
import dayjs from "dayjs";
export let botMatch: BotMatch;
</script>
<a class="bot-match-card-wrapper" href={`/matches/${botMatch.id}`}>
<div class="bot-match-card">
<div class="bot-match-outcome">
{botMatch.outcome}
</div>
<div class="bot-match-card-main">
<div class="opponent-name">
<a class="bot-link" href={`/bots/${botMatch.opponent.bot_name}`}
>{botMatch.opponent.bot_name}</a
>
</div>
<div class="map-name">
{botMatch.map.name}
</div>
</div>
<div class="bot-card-right">
<div class="match-timestamp">
{dayjs(botMatch.timestamp).format("YYYY-MM-DD HH:mm")}
</div>
<div class="match-errors">
{#if botMatch.hadErrors}
! Had errors
{/if}
</div>
</div>
</div>
</a>
<style lang="scss">
@use "src/styles/variables";
.bot-match-card {
margin: 4px;
padding: 12px;
display: flex;
border: 1px solid variables.$light-grey;
}
.bot-match-outcome {
font-size: 24pt;
font-weight: 600;
font-family: "Open Sans", sans-serif;
text-transform: uppercase;
color: #333;
width: 75px;
display: flex;
justify-content: center;
margin-top: -4px;
}
.bot-match-card-main {
flex-grow: 1;
padding: 0 25px;
}
.bot-card-right {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
}
.opponent-name {
font-size: 18pt;
color: variables.$blue-primary;
padding-bottom: 4px;
}
.match-errors {
color: red;
font-weight: 600;
text-align: right;
}
.match-timestamp {
text-align: right;
}
.bot-link {
color: variables.$blue-primary;
text-decoration: none;
}
.bot-link:hover {
text-decoration: underline;
}
.bot-match-card:hover {
background-color: rgb(246, 248, 250);
}
.bot-match-card-wrapper {
text-decoration: none;
color: black;
display: block;
}
</style>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import type { BotMatch } from "$lib/matches";
import BotMatchCard from "./BotMatchCard.svelte";
export let botMatches: BotMatch[];
function match_url(match: object) {
return `/matches/${match["id"]}`;
}
</script>
<div>
{#each botMatches as botMatch}
<BotMatchCard {botMatch} />
{/each}
</div>
<style lang="scss">
</style>

View file

@ -39,6 +39,7 @@
<a class="current-user-name" href="/users/{$currentUser['username']}"> <a class="current-user-name" href="/users/{$currentUser['username']}">
{$currentUser["username"]} {$currentUser["username"]}
</a> </a>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="sign-out" on:click={signOut}>Sign out</div> <div class="sign-out" on:click={signOut}>Sign out</div>
{:else} {:else}
<a class="account-href" href="/login">Sign in</a> <a class="account-href" href="/login">Sign in</a>

View file

@ -0,0 +1,55 @@
import type { Match as ApiMatch, MatchPlayer as ApiMatchPlayer, Map } from "./api_types";
// match from the perspective of a bot
export type BotMatch = {
id: number;
opponent: BotMatchOpponent;
outcome: BotMatchOutcome;
timestamp: string;
map: Map;
hadErrors?: boolean;
};
export type BotMatchOutcome = "win" | "loss" | "tie";
export type BotMatchOpponent = {
bot_id: number;
bot_name: string;
owner_id?: number;
};
export function apiMatchtoBotMatch(bot_name: string, apiMatch: ApiMatch): BotMatch {
let player: ApiMatchPlayer;
let playerIndex: number;
let opponent: ApiMatchPlayer;
apiMatch.players.forEach((matchPlayer, index) => {
if (matchPlayer.bot_name === bot_name) {
player = matchPlayer;
playerIndex = index;
} else {
opponent = matchPlayer;
}
});
if (player === undefined || opponent === undefined || playerIndex === undefined) {
throw "could not assign player and opponent";
}
let outcome: BotMatchOutcome;
if (apiMatch.winner === playerIndex) {
outcome = "win";
} else if (apiMatch.winner) {
outcome = "loss";
} else {
outcome = "tie";
}
return {
id: apiMatch.id,
opponent,
outcome,
timestamp: apiMatch.timestamp,
map: apiMatch.map,
hadErrors: player.had_errors,
};
}

View file

@ -100,9 +100,7 @@
<LinkButton href={`/matches?bot=${bot["name"]}&had_errors=true`}>View all</LinkButton> <LinkButton href={`/matches?bot=${bot["name"]}&had_errors=true`}>View all</LinkButton>
</div> </div>
{:else} {:else}
<div class="table-placeholder"> <div class="table-placeholder">Nothing here yet</div>
Nothing here yet
</div>
{/if} {/if}
</div> </div>
{/if} {/if}
@ -115,9 +113,7 @@
<LinkButton href={`/matches?bot=${bot["name"]}`}>All matches</LinkButton> <LinkButton href={`/matches?bot=${bot["name"]}`}>All matches</LinkButton>
</div> </div>
{:else} {:else}
<div class="table-placeholder"> <div class="table-placeholder">No matches played yet</div>
No matches played yet
</div>
{/if} {/if}
</div> </div>
</div> </div>

View file

@ -0,0 +1,64 @@
<script lang="ts" context="module">
import { ApiClient } from "$lib/api_client";
import type { Match } from "$lib/api_types";
const PAGE_SIZE = "50";
export async function load({ params, fetch }) {
try {
const apiClient = new ApiClient(fetch);
const botName = params["bot_name"];
let { matches, has_next } = await apiClient.get("/api/matches", { bot: botName });
// TODO: should this be done client-side?
// if (query["after"]) {
// matches = matches.reverse();
// }
return {
props: {
matches,
botName,
hasNext: has_next,
},
};
} catch (error) {
return {
status: error.status,
error: new Error("failed to load matches"),
};
}
}
</script>
<script lang="ts">
import LinkButton from "$lib/components/LinkButton.svelte";
import BotMatchList from "$lib/components/matches/BotMatchList.svelte";
import { apiMatchtoBotMatch } from "$lib/matches";
export let matches: Match[];
export let botName: string | null;
// whether a next page exists in the current iteration direction (before/after)
// export let hasNext: boolean;
$: botMatches = matches.map((match) => apiMatchtoBotMatch(botName, match));
</script>
<div class="container">
<BotMatchList {botMatches} />
</div>
<style lang="scss">
.container {
max-width: 800px;
margin: 0 auto;
width: 100%;
}
.page-controls {
display: flex;
justify-content: center;
margin: 24px 0;
}
</style>

View file

@ -65,9 +65,9 @@ Example command:
} }
] ]
} }
```
You can dispatch as many expeditions as you like. You can dispatch as many expeditions as you like.
```
## Rules ## Rules