aboutsummaryrefslogtreecommitdiff
path: root/web/pw-server/src/routes/editor.svelte
diff options
context:
space:
mode:
authorIlion Beyst <ilion.beyst@gmail.com>2022-08-21 21:06:10 +0200
committerIlion Beyst <ilion.beyst@gmail.com>2022-08-21 21:06:10 +0200
commit64d24c9e3d7e744788644b00cdb2ec44a999d9cd (patch)
tree7b92cc3e3b4d7d008f4de75a41d585dfb2bcaf34 /web/pw-server/src/routes/editor.svelte
parent71d37e758fbc155a14f13989e7ed05e7719c2159 (diff)
downloadplanetwars.dev-64d24c9e3d7e744788644b00cdb2ec44a999d9cd.tar.xz
planetwars.dev-64d24c9e3d7e744788644b00cdb2ec44a999d9cd.zip
move bot editor to /editor
Diffstat (limited to 'web/pw-server/src/routes/editor.svelte')
-rw-r--r--web/pw-server/src/routes/editor.svelte277
1 files changed, 277 insertions, 0 deletions
diff --git a/web/pw-server/src/routes/editor.svelte b/web/pw-server/src/routes/editor.svelte
new file mode 100644
index 0000000..85c2454
--- /dev/null
+++ b/web/pw-server/src/routes/editor.svelte
@@ -0,0 +1,277 @@
+<script lang="ts">
+ import Visualizer from "$lib/components/Visualizer.svelte";
+ import EditorView from "$lib/components/EditorView.svelte";
+ import { onMount } from "svelte";
+
+ import { DateTime } from "luxon";
+
+ import type { Ace } from "ace-builds";
+ import ace from "ace-builds/src-noconflict/ace?client";
+ import * as AcePythonMode from "ace-builds/src-noconflict/mode-python?client";
+ import { getBotCode, saveBotCode, hasBotCode } from "$lib/bot_code";
+ import { debounce } from "$lib/utils";
+ import SubmitPane from "$lib/components/SubmitPane.svelte";
+ import OutputPane from "$lib/components/OutputPane.svelte";
+ import RulesView from "$lib/components/RulesView.svelte";
+ import Leaderboard from "$lib/components/Leaderboard.svelte";
+
+ enum ViewMode {
+ Editor,
+ MatchVisualizer,
+ Rules,
+ Leaderboard,
+ }
+
+ let matches = [];
+
+ let viewMode = ViewMode.Editor;
+ let selectedMatchId: string | undefined = undefined;
+ let selectedMatchLog: string | undefined = undefined;
+
+ let editSession: Ace.EditSession;
+
+ onMount(() => {
+ if (!hasBotCode()) {
+ viewMode = ViewMode.Rules;
+ }
+ init_editor();
+ });
+
+ function init_editor() {
+ editSession = new ace.EditSession(getBotCode());
+ editSession.setMode(new AcePythonMode.Mode());
+
+ const saveCode = () => {
+ const code = editSession.getDocument().getValue();
+ saveBotCode(code);
+ };
+
+ // cast to any because the type annotations are wrong here
+ (editSession as any).on("change", debounce(saveCode, 2000));
+ }
+
+ async function onMatchCreated(e: CustomEvent) {
+ const matchData = e.detail["match"];
+ matches.unshift(matchData);
+ matches = matches;
+ await selectMatch(matchData["id"]);
+ }
+
+ async function selectMatch(matchId: string) {
+ selectedMatchId = matchId;
+ selectedMatchLog = null;
+ fetchSelectedMatchLog(matchId);
+
+ viewMode = ViewMode.MatchVisualizer;
+ }
+
+ async function fetchSelectedMatchLog(matchId: string) {
+ if (matchId !== selectedMatchId) {
+ return;
+ }
+
+ let matchLog = await getMatchLog(matchId);
+
+ if (matchLog) {
+ selectedMatchLog = matchLog;
+ } else {
+ // try again in 1 second
+ setTimeout(fetchSelectedMatchLog, 1000, matchId);
+ }
+ }
+
+ async function getMatchData(matchId: string) {
+ let response = await fetch(`/api/matches/${matchId}`, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw Error(response.statusText);
+ }
+
+ let matchData = await response.json();
+ return matchData;
+ }
+
+ async function getMatchLog(matchId: string) {
+ const matchData = await getMatchData(matchId);
+ console.log(matchData);
+ if (matchData["state"] !== "Finished") {
+ // log is not available yet
+ return null;
+ }
+
+ const res = await fetch(`/api/matches/${matchId}/log`, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ let log = await res.text();
+ return log;
+ }
+
+ function setViewMode(viewMode_: ViewMode) {
+ selectedMatchId = undefined;
+ selectedMatchLog = undefined;
+ viewMode = viewMode_;
+ }
+
+ function selectRules() {
+ selectedMatchId = undefined;
+ selectedMatchLog = undefined;
+ viewMode = ViewMode.Rules;
+ }
+
+ function formatMatchTimestamp(timestampString: string): string {
+ let timestamp = DateTime.fromISO(timestampString, { zone: "utc" }).toLocal();
+ if (timestamp.startOf("day").equals(DateTime.now().startOf("day"))) {
+ return timestamp.toFormat("HH:mm");
+ } else {
+ return timestamp.toFormat("dd/MM");
+ }
+ }
+
+ $: selectedMatch = matches.find((m) => m["id"] === selectedMatchId);
+</script>
+
+<div class="container">
+ <div class="sidebar-left">
+ <div
+ class="editor-button sidebar-item"
+ class:selected={viewMode === ViewMode.Editor}
+ on:click={() => setViewMode(ViewMode.Editor)}
+ >
+ Editor
+ </div>
+ <div
+ class="rules-button sidebar-item"
+ class:selected={viewMode === ViewMode.Rules}
+ on:click={() => setViewMode(ViewMode.Rules)}
+ >
+ Rules
+ </div>
+ <div
+ class="sidebar-item"
+ class:selected={viewMode === ViewMode.Leaderboard}
+ on:click={() => setViewMode(ViewMode.Leaderboard)}
+ >
+ Leaderboard
+ </div>
+ <div class="sidebar-header">match history</div>
+ <ul class="match-list">
+ {#each matches as match}
+ <li
+ class="match-card sidebar-item"
+ on:click={() => selectMatch(match.id)}
+ class:selected={match.id === selectedMatchId}
+ >
+ <span class="match-timestamp">{formatMatchTimestamp(match.timestamp)}</span>
+ <!-- hex is hardcoded for now, don't show map name -->
+ <!-- <span class="match-mapname">hex</span> -->
+ <!-- ugly temporary hardcode -->
+ <span class="match-opponent">{match["players"][1]["bot_name"]}</span>
+ </li>
+ {/each}
+ </ul>
+ </div>
+ <div class="editor-container">
+ {#if viewMode === ViewMode.MatchVisualizer}
+ <Visualizer matchData={selectedMatch} matchLog={selectedMatchLog} />
+ {:else if viewMode === ViewMode.Editor}
+ <EditorView {editSession} />
+ {:else if viewMode === ViewMode.Rules}
+ <RulesView />
+ {:else if viewMode === ViewMode.Leaderboard}
+ <Leaderboard />
+ {/if}
+ </div>
+ <div class="sidebar-right">
+ {#if viewMode === ViewMode.MatchVisualizer}
+ <OutputPane matchLog={selectedMatchLog} />
+ {:else if viewMode === ViewMode.Editor}
+ <SubmitPane {editSession} on:matchCreated={onMatchCreated} />
+ {/if}
+ </div>
+</div>
+
+<style lang="scss">
+ @import "src/styles/variables.scss";
+
+ .container {
+ display: flex;
+ flex-grow: 1;
+ min-height: 0;
+ }
+
+ .sidebar-left {
+ width: 240px;
+ background-color: $bg-color;
+ display: flex;
+ flex-direction: column;
+ }
+ .sidebar-right {
+ width: 400px;
+ background-color: white;
+ border-left: 1px solid;
+ padding: 0;
+ display: flex;
+ overflow: hidden;
+ }
+ .editor-container {
+ flex-grow: 1;
+ flex-shrink: 1;
+ overflow: hidden;
+ background-color: white;
+ }
+
+ .editor-container {
+ height: 100%;
+ }
+
+ .sidebar-item {
+ color: #eee;
+ padding: 15px;
+ }
+
+ .sidebar-item:hover {
+ background-color: #333;
+ }
+
+ .sidebar-item.selected {
+ background-color: #333;
+ }
+
+ .match-list {
+ list-style: none;
+ color: #eee;
+ padding-top: 15px;
+ overflow-y: scroll;
+ padding-left: 0px;
+ }
+
+ .match-card {
+ padding: 10px 15px;
+ font-size: 11pt;
+ }
+
+ .match-timestamp {
+ color: #ccc;
+ }
+
+ .match-opponent {
+ padding: 0 0.5em;
+ }
+
+ .sidebar-header {
+ margin-top: 2em;
+ text-transform: uppercase;
+ font-weight: 600;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 14px;
+ font-family: "Open Sans", sans-serif;
+ padding-left: 14px;
+ }
+</style>