design new BotMatch view
This commit is contained in:
parent
67c8a2780c
commit
00d31df58d
10 changed files with 269 additions and 15 deletions
20
web/pw-server/src/lib/api_types.ts
Normal file
20
web/pw-server/src/lib/api_types.ts
Normal 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;
|
||||||
|
};
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
104
web/pw-server/src/lib/components/matches/BotMatchCard.svelte
Normal file
104
web/pw-server/src/lib/components/matches/BotMatchCard.svelte
Normal 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>
|
19
web/pw-server/src/lib/components/matches/BotMatchList.svelte
Normal file
19
web/pw-server/src/lib/components/matches/BotMatchList.svelte
Normal 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>
|
|
@ -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>
|
||||||
|
|
55
web/pw-server/src/lib/matches.ts
Normal file
55
web/pw-server/src/lib/matches.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
64
web/pw-server/src/routes/bots/[bot_name]/matches.svelte
Normal file
64
web/pw-server/src/routes/bots/[bot_name]/matches.svelte
Normal 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>
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue