aboutsummaryrefslogtreecommitdiff
path: root/web/pw-server/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'web/pw-server/src/lib')
-rw-r--r--web/pw-server/src/lib/api_client.ts80
-rw-r--r--web/pw-server/src/lib/components/Leaderboard.svelte17
-rw-r--r--web/pw-server/src/lib/components/LinkButton.svelte7
-rw-r--r--web/pw-server/src/lib/components/RulesView.svelte7
-rw-r--r--web/pw-server/src/lib/components/SubmitPane.svelte55
-rw-r--r--web/pw-server/src/lib/components/matches/MatchList.svelte104
-rw-r--r--web/pw-server/src/lib/stores/editor_state.ts27
-rw-r--r--web/pw-server/src/lib/urls.ts0
-rw-r--r--web/pw-server/src/lib/utils.ts37
9 files changed, 265 insertions, 69 deletions
diff --git a/web/pw-server/src/lib/api_client.ts b/web/pw-server/src/lib/api_client.ts
new file mode 100644
index 0000000..706d958
--- /dev/null
+++ b/web/pw-server/src/lib/api_client.ts
@@ -0,0 +1,80 @@
+import { browser } from "$app/env";
+import { get_session_token } from "./auth";
+
+export type FetchFn = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
+
+export class ApiError extends Error {
+ constructor(public status: number, message?: string) {
+ super(message);
+ }
+}
+
+export class ApiClient {
+ private fetch_fn: FetchFn;
+ private sessionToken?: string;
+
+ constructor(fetch_fn?: FetchFn) {
+ if (fetch_fn) {
+ this.fetch_fn = fetch_fn;
+ } else if (browser) {
+ this.fetch_fn = fetch.bind(window);
+ }
+
+ // TODO: maybe it is cleaner to pass this as a parameter
+ this.sessionToken = get_session_token();
+ }
+
+ async get(url: string, params?: Record<string, string>): Promise<any> {
+ const response = await this.getRequest(url, params);
+ this.checkResponse(response);
+ return await response.json();
+ }
+
+ async getText(url: string, params?: Record<string, string>): Promise<any> {
+ const response = await this.getRequest(url, params);
+ this.checkResponse(response);
+ return await response.text();
+ }
+
+ async post(url: string, data: any): Promise<any> {
+ const headers = { "Content-Type": "application/json" };
+
+ const token = get_session_token();
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`;
+ }
+
+ const response = await this.fetch_fn(url, {
+ method: "POST",
+ headers,
+ body: JSON.stringify(data),
+ });
+
+ this.checkResponse(response);
+ return await response.json();
+ }
+
+ private async getRequest(url: string, params: Record<string, string>): Promise<Response> {
+ const headers = { "Content-Type": "application/json" };
+
+ if (this.sessionToken) {
+ headers["Authorization"] = `Bearer ${this.sessionToken}`;
+ }
+
+ if (params) {
+ let searchParams = new URLSearchParams(params);
+ url = `${url}?${searchParams}`;
+ }
+
+ return await this.fetch_fn(url, {
+ method: "GET",
+ headers,
+ });
+ }
+
+ private checkResponse(response: Response) {
+ if (!response.ok) {
+ throw new ApiError(response.status, response.statusText);
+ }
+ }
+}
diff --git a/web/pw-server/src/lib/components/Leaderboard.svelte b/web/pw-server/src/lib/components/Leaderboard.svelte
index d29d5d6..ea30384 100644
--- a/web/pw-server/src/lib/components/Leaderboard.svelte
+++ b/web/pw-server/src/lib/components/Leaderboard.svelte
@@ -1,20 +1,5 @@
<script lang="ts">
- import { onMount } from "svelte";
-
- let leaderboard = [];
-
- onMount(async () => {
- const res = await fetch("/api/leaderboard", {
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- if (res.ok) {
- leaderboard = await res.json();
- console.log(leaderboard);
- }
- });
+ export let leaderboard;
function formatRating(entry: object): any {
const rating = entry["rating"];
diff --git a/web/pw-server/src/lib/components/LinkButton.svelte b/web/pw-server/src/lib/components/LinkButton.svelte
new file mode 100644
index 0000000..0d0e7d6
--- /dev/null
+++ b/web/pw-server/src/lib/components/LinkButton.svelte
@@ -0,0 +1,7 @@
+<script lang="ts">
+ export let href: string | null;
+
+ $: isDisabled = !href;
+</script>
+
+<a class="btn" class:btn-disabled={isDisabled} {href}><slot /></a>
diff --git a/web/pw-server/src/lib/components/RulesView.svelte b/web/pw-server/src/lib/components/RulesView.svelte
index 92de37e..c7d4a4a 100644
--- a/web/pw-server/src/lib/components/RulesView.svelte
+++ b/web/pw-server/src/lib/components/RulesView.svelte
@@ -1,11 +1,6 @@
<div class="container">
<div class="game-rules">
- <h2 class="title">Welcome to planetwars!</h2>
-
- <p>
- Planetwars is a game of galactic conquest for busy people. Your goal is to program a bot that
- will conquer the galaxy for you, while you take care of more important stuff.
- </p>
+ <h2 class="title">How to play</h2>
<p>
In every game turn, your bot will receive a json-encoded line on stdin, describing the current
state of the game. Each state will hold a set of planets, and a set of spaceship fleets
diff --git a/web/pw-server/src/lib/components/SubmitPane.svelte b/web/pw-server/src/lib/components/SubmitPane.svelte
index 82f752e..b0f86c8 100644
--- a/web/pw-server/src/lib/components/SubmitPane.svelte
+++ b/web/pw-server/src/lib/components/SubmitPane.svelte
@@ -1,15 +1,20 @@
<script lang="ts">
+ import { ApiClient } from "$lib/api_client";
+
import { get_session_token } from "$lib/auth";
import { getBotName, saveBotName } from "$lib/bot_code";
import { currentUser } from "$lib/stores/current_user";
+ import { selectedOpponent, selectedMap } from "$lib/stores/editor_state";
+
import { createEventDispatcher, onMount } from "svelte";
import Select from "svelte-select";
export let editSession;
let availableBots: object[] = [];
- let selectedOpponent = undefined;
+ let maps: object[] = [];
+
let botName: string | undefined = undefined;
// whether to show the "save succesful" message
let saveSuccesful = false;
@@ -18,24 +23,28 @@
onMount(async () => {
botName = getBotName();
+ const apiClient = new ApiClient();
- const res = await fetch("/api/bots", {
- headers: {
- "Content-Type": "application/json",
- },
- });
+ const [_bots, _maps] = await Promise.all([
+ apiClient.get("/api/bots"),
+ apiClient.get("/api/maps"),
+ ]);
- if (res.ok) {
- availableBots = await res.json();
- selectedOpponent = availableBots.find((b) => b["name"] === "simplebot");
+ availableBots = _bots;
+ maps = _maps;
+
+ if (!$selectedOpponent) {
+ selectedOpponent.set(availableBots.find((b) => b["name"] === "simplebot"));
+ }
+
+ if (!$selectedMap) {
+ selectedMap.set(maps.find((m) => m["name"] === "hex"));
}
});
const dispatch = createEventDispatcher();
async function submitBot() {
- const opponentName = selectedOpponent["name"];
-
let response = await fetch("/api/submit_bot", {
method: "POST",
headers: {
@@ -43,7 +52,8 @@
},
body: JSON.stringify({
code: editSession.getDocument().getValue(),
- opponent_name: opponentName,
+ opponent_name: $selectedOpponent["name"],
+ map_name: $selectedMap["name"],
}),
});
@@ -100,13 +110,23 @@
<div class="submit-pane">
<div class="match-form">
<h4>Play a match</h4>
- <div class="play-text">Select an opponent to test your bot</div>
- <div class="opponentSelect">
+ <div class="play-text">Opponent</div>
+ <div class="opponent-select">
<Select
optionIdentifier="name"
labelIdentifier="name"
items={availableBots}
- bind:value={selectedOpponent}
+ bind:value={$selectedOpponent}
+ isClearable={false}
+ />
+ </div>
+ <span>Map</span>
+ <div class="map-select">
+ <Select
+ optionIdentifier="name"
+ labelIdentifier="name"
+ items={maps}
+ bind:value={$selectedMap}
isClearable={false}
/>
</div>
@@ -145,8 +165,9 @@
margin-bottom: 0.3em;
}
- .opponentSelect {
- margin: 20px 0;
+ .opponent-select,
+ .map-select {
+ margin: 8px 0;
}
.save-form {
diff --git a/web/pw-server/src/lib/components/matches/MatchList.svelte b/web/pw-server/src/lib/components/matches/MatchList.svelte
new file mode 100644
index 0000000..e38543e
--- /dev/null
+++ b/web/pw-server/src/lib/components/matches/MatchList.svelte
@@ -0,0 +1,104 @@
+<script lang="ts">
+ import { goto } from "$app/navigation";
+ import dayjs from "dayjs";
+
+ export let matches: object[];
+
+ function match_url(match: object) {
+ return `/matches/${match["id"]}`;
+ }
+</script>
+
+<table class="matches-table">
+ <tr>
+ <th class="header-timestamp">timestamp</th>
+ <th class="col-player-1">player 1</th>
+ <th />
+ <th />
+ <th class="col-player-2">player 2</th>
+ <th class="col-map">map</th>
+ </tr>
+ {#each matches as match}
+ <tr class="match-table-row" on:click={() => goto(match_url(match))}>
+ <td class="col-timestamp">
+ {dayjs(match["timestamp"]).format("YYYY-MM-DD HH:mm")}
+ </td>
+ <td class="col-player-1">
+ {match["players"][0]["bot_name"]}
+ </td>
+ {#if match["winner"] == null}
+ <td class="col-score-player-1"> TIE </td>
+ <td class="col-score-player-2"> TIE </td>
+ {:else if match["winner"] == 0}
+ <td class="col-score-player-1"> WIN </td>
+ <td class="col-score-player-2"> LOSS </td>
+ {:else if match["winner"] == 1}
+ <td class="col-score-player-1"> LOSS </td>
+ <td class="col-score-player-2"> WIN </td>
+ {/if}
+ <td class="col-player-2">
+ {match["players"][1]["bot_name"]}
+ </td>
+ <td class="col-map">
+ {match["map"]?.name || ""}
+ </td>
+ </tr>
+ {/each}
+</table>
+
+<style lang="scss">
+ .matches-table {
+ width: 100%;
+ }
+ .matches-table td,
+ .matches-table th {
+ padding: 8px 16px;
+ }
+
+ .header-timestamp {
+ text-align: left;
+ }
+
+ .col-timestamp {
+ color: #555;
+ }
+
+ .col-player-1 {
+ text-align: left;
+ }
+
+ .col-player-2 {
+ text-align: right;
+ }
+
+ @mixin col-player-score {
+ text-transform: uppercase;
+ font-weight: 600;
+ font-size: 14px;
+ font-family: "Open Sans", sans-serif;
+ }
+
+ .col-score-player-1 {
+ @include col-player-score;
+ text-align: right;
+ }
+
+ .col-score-player-2 {
+ @include col-player-score;
+ text-align: left;
+ }
+
+ .col-map {
+ text-align: right;
+ }
+
+ .matches-table {
+ margin: 12px auto;
+ border-collapse: collapse;
+ }
+
+ .match-table-row:hover {
+ cursor: pointer;
+ background-color: #eee;
+ }
+</style>
diff --git a/web/pw-server/src/lib/stores/editor_state.ts b/web/pw-server/src/lib/stores/editor_state.ts
new file mode 100644
index 0000000..c0462e1
--- /dev/null
+++ b/web/pw-server/src/lib/stores/editor_state.ts
@@ -0,0 +1,27 @@
+import { writable } from "svelte/store";
+
+const MAX_MATCHES = 100;
+
+function createMatchHistory() {
+ const { subscribe, update } = writable([]);
+
+ function pushMatch(match: object) {
+ update((matches) => {
+ if (matches.length == MAX_MATCHES) {
+ matches.pop();
+ }
+ matches.unshift(match);
+
+ return matches;
+ });
+ }
+
+ return {
+ subscribe,
+ pushMatch,
+ };
+}
+
+export const matchHistory = createMatchHistory();
+export const selectedOpponent = writable(null);
+export const selectedMap = writable(null);
diff --git a/web/pw-server/src/lib/urls.ts b/web/pw-server/src/lib/urls.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/web/pw-server/src/lib/urls.ts
diff --git a/web/pw-server/src/lib/utils.ts b/web/pw-server/src/lib/utils.ts
index 155d952..ab1faa5 100644
--- a/web/pw-server/src/lib/utils.ts
+++ b/web/pw-server/src/lib/utils.ts
@@ -1,4 +1,4 @@
-import { get_session_token } from "./auth";
+import { ApiClient, FetchFn } from "./api_client";
export function debounce(func: Function, timeout: number = 300) {
let timer: ReturnType<typeof setTimeout>;
@@ -10,35 +10,12 @@ export function debounce(func: Function, timeout: number = 300) {
};
}
-export async function get(url: string, fetch_fn: Function = fetch) {
- const headers = { "Content-Type": "application/json" };
-
- const token = get_session_token();
- if (token) {
- headers["Authorization"] = `Bearer ${token}`;
- }
-
- const response = await fetch_fn(url, {
- method: "GET",
- headers,
- });
-
- return JSON.parse(response);
+export async function get(url: string, params?: Record<string, string>, fetch_fn: FetchFn = fetch) {
+ const client = new ApiClient(fetch_fn);
+ return await client.get(url, params);
}
-export async function post(url: string, data: any, fetch_fn: Function = fetch) {
- const headers = { "Content-Type": "application/json" };
-
- const token = get_session_token();
- if (token) {
- headers["Authorization"] = `Bearer ${token}`;
- }
-
- const response = await fetch_fn(url, {
- method: "POST",
- headers,
- body: JSON.stringify(data),
- });
-
- return JSON.parse(response);
+export async function post(url: string, data: any, fetch_fn: FetchFn = fetch) {
+ const client = new ApiClient(fetch_fn);
+ return await client.post(url, data);
}