move bot editor to /editor
This commit is contained in:
parent
71d37e758f
commit
64d24c9e3d
3 changed files with 311 additions and 284 deletions
|
@ -6,10 +6,17 @@
|
|||
|
||||
<div class="outer-container">
|
||||
<div class="navbar">
|
||||
<div class="navbar-main">
|
||||
<a href="/">PlanetWars</a>
|
||||
<div class="navbar-left">
|
||||
<div class="navbar-header">
|
||||
<a href="/">PlanetWars</a>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<a href="/editor">Editor</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<UserControls />
|
||||
</div>
|
||||
<UserControls />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -34,13 +41,33 @@
|
|||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.navbar-main {
|
||||
margin: auto 0;
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar-main a {
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar-header {
|
||||
margin: auto 0;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.navbar-header a {
|
||||
font-size: 20px;
|
||||
color: #eee;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.navbar-item {
|
||||
margin: auto 0;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.navbar-item a {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
|
277
web/pw-server/src/routes/editor.svelte
Normal file
277
web/pw-server/src/routes/editor.svelte
Normal file
|
@ -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>
|
|
@ -1,277 +0,0 @@
|
|||
<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>
|
Loading…
Reference in a new issue