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 playerId: number;
|
||||
export let showStdErr: boolean = true;
|
||||
|
||||
let playerLog: PlayerLog;
|
||||
|
||||
|
@ -66,7 +67,7 @@
|
|||
<div class="bad-command-error">Parse error: {logTurn.action.error}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if logTurn.stderr.length > 0}
|
||||
{#if showStdErr && logTurn.stderr.length > 0}
|
||||
<div class="stderr-header">stderr</div>
|
||||
<div class="stderr-text-box">
|
||||
{#each logTurn.stderr as stdErrMsg}
|
||||
|
|
|
@ -122,13 +122,7 @@
|
|||
</div>
|
||||
<span>Map</span>
|
||||
<div class="map-select">
|
||||
<Select
|
||||
itemId="name"
|
||||
label="name"
|
||||
items={maps}
|
||||
bind:value={$selectedMap}
|
||||
clearable={false}
|
||||
/>
|
||||
<Select itemId="name" label="name" items={maps} bind:value={$selectedMap} clearable={false} />
|
||||
</div>
|
||||
<button class="submit-button play-button" on:click={submitBot}>Play</button>
|
||||
</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']}">
|
||||
{$currentUser["username"]}
|
||||
</a>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="sign-out" on:click={signOut}>Sign out</div>
|
||||
{:else}
|
||||
<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>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="table-placeholder">
|
||||
Nothing here yet
|
||||
</div>
|
||||
<div class="table-placeholder">Nothing here yet</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -115,9 +113,7 @@
|
|||
<LinkButton href={`/matches?bot=${bot["name"]}`}>All matches</LinkButton>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="table-placeholder">
|
||||
No matches played yet
|
||||
</div>
|
||||
<div class="table-placeholder">No matches played yet</div>
|
||||
{/if}
|
||||
</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.
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
|
|
Loading…
Reference in a new issue