diff options
Diffstat (limited to 'web/pw-server/src/lib')
-rw-r--r-- | web/pw-server/src/lib/api_client.ts | 80 | ||||
-rw-r--r-- | web/pw-server/src/lib/components/Leaderboard.svelte | 17 | ||||
-rw-r--r-- | web/pw-server/src/lib/components/LinkButton.svelte | 7 | ||||
-rw-r--r-- | web/pw-server/src/lib/components/RulesView.svelte | 7 | ||||
-rw-r--r-- | web/pw-server/src/lib/components/SubmitPane.svelte | 55 | ||||
-rw-r--r-- | web/pw-server/src/lib/components/matches/MatchList.svelte | 104 | ||||
-rw-r--r-- | web/pw-server/src/lib/stores/editor_state.ts | 27 | ||||
-rw-r--r-- | web/pw-server/src/lib/urls.ts | 0 | ||||
-rw-r--r-- | web/pw-server/src/lib/utils.ts | 37 |
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); } |