Add initial webui
This commit is contained in:
357
webui/src/components/Playground.tsx
Normal file
357
webui/src/components/Playground.tsx
Normal file
@@ -0,0 +1,357 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Dropdown } from "@/components/ui/Dropdown";
|
||||
import { Slider } from "@/components/ui/Slider";
|
||||
import { SidePanel } from "@/components/ui/SidePanel";
|
||||
import { Editor } from "@/components/Editor";
|
||||
import { Terminal, TerminalRef } from "@/components/Terminal";
|
||||
import {
|
||||
sendDataToScript,
|
||||
startScript,
|
||||
startScriptBulk,
|
||||
stopScript,
|
||||
} from "@/lib/runner";
|
||||
import styles from "@/styles/Playground.module.css";
|
||||
|
||||
const initialCode = `fn random_action(board) {
|
||||
let symb = rand_symb();
|
||||
let pos = rand_int(0, 10);
|
||||
let action = Action(symb, pos);
|
||||
|
||||
// If this action is invalid, randomly select a new one.
|
||||
while !board.can_play(action) {
|
||||
let symb = rand_symb();
|
||||
let pos = rand_int(0, 10);
|
||||
action = Action(symb, pos);
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
fn step_min(board) {
|
||||
return random_action(board);
|
||||
}
|
||||
|
||||
fn step_max(board) {
|
||||
return random_action(board);
|
||||
}`;
|
||||
|
||||
export default function Playground() {
|
||||
const [isScriptRunning, setIsScriptRunning] = useState(false);
|
||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||
const [fontSize, setFontSize] = useState(14);
|
||||
const [bulkRounds, setBulkRounds] = useState(1000);
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
const resultRef = useRef<HTMLTextAreaElement>(null);
|
||||
const terminalRef = useRef<TerminalRef>(null);
|
||||
|
||||
const runDisabled = isScriptRunning || !isEditorReady;
|
||||
const stopDisabled = !isScriptRunning;
|
||||
|
||||
const runHuman = useCallback(async () => {
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value = "";
|
||||
}
|
||||
|
||||
if (runDisabled || !editorRef.current) return;
|
||||
|
||||
setIsScriptRunning(true);
|
||||
|
||||
try {
|
||||
terminalRef.current?.clear();
|
||||
terminalRef.current?.focus();
|
||||
|
||||
await startScript(
|
||||
editorRef.current.getValue(),
|
||||
(line: string) => {
|
||||
if (resultRef.current) {
|
||||
let v = resultRef.current.value + line + "\n";
|
||||
if (v.length > 10000) {
|
||||
v = v.substring(v.length - 10000);
|
||||
}
|
||||
resultRef.current.value = v;
|
||||
resultRef.current.scrollTop =
|
||||
resultRef.current.scrollHeight -
|
||||
resultRef.current.clientHeight;
|
||||
}
|
||||
},
|
||||
|
||||
(line: string) => {
|
||||
terminalRef.current?.write(line);
|
||||
}
|
||||
);
|
||||
} catch (ex) {
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
|
||||
}
|
||||
terminalRef.current?.write(
|
||||
"\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" + String(ex).replace("\n", "\n\r") + "\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
setIsScriptRunning(false);
|
||||
}, [runDisabled]);
|
||||
|
||||
const runBulk = useCallback(async () => {
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value = "";
|
||||
}
|
||||
|
||||
if (runDisabled || !editorRef.current) return;
|
||||
|
||||
setIsScriptRunning(true);
|
||||
|
||||
try {
|
||||
terminalRef.current?.clear();
|
||||
terminalRef.current?.focus();
|
||||
|
||||
await startScriptBulk(
|
||||
editorRef.current.getValue(),
|
||||
(line: string) => {
|
||||
if (resultRef.current) {
|
||||
let v = resultRef.current.value + line + "\n";
|
||||
if (v.length > 10000) {
|
||||
v = v.substring(v.length - 10000);
|
||||
}
|
||||
resultRef.current.value = v;
|
||||
resultRef.current.scrollTop =
|
||||
resultRef.current.scrollHeight -
|
||||
resultRef.current.clientHeight;
|
||||
}
|
||||
},
|
||||
|
||||
(line: string) => {
|
||||
terminalRef.current?.write(line);
|
||||
},
|
||||
bulkRounds
|
||||
);
|
||||
} catch (ex) {
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
|
||||
}
|
||||
terminalRef.current?.write(
|
||||
"\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" + String(ex).replace("\n", "\n\r") + "\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
setIsScriptRunning(false);
|
||||
}, [runDisabled, bulkRounds]);
|
||||
|
||||
const stopScriptHandler = useCallback(() => {
|
||||
stopScript();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.playgroundRoot}>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerField}>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
variant="success"
|
||||
iconLeft="play"
|
||||
onClick={runHuman}
|
||||
loading={isScriptRunning}
|
||||
disabled={runDisabled}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="success"
|
||||
iconLeft="play"
|
||||
onClick={runBulk}
|
||||
loading={isScriptRunning}
|
||||
disabled={runDisabled}
|
||||
>
|
||||
Bulk Run
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="danger"
|
||||
iconLeft="stop"
|
||||
onClick={stopScriptHandler}
|
||||
disabled={stopDisabled}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonGroup}>
|
||||
<Dropdown
|
||||
trigger="Config"
|
||||
align="right"
|
||||
customContent={
|
||||
<div className={styles.configPanel}>
|
||||
<Slider
|
||||
label="Font Size"
|
||||
value={fontSize}
|
||||
min={10}
|
||||
max={24}
|
||||
step={1}
|
||||
onChange={setFontSize}
|
||||
unit="px"
|
||||
/>
|
||||
<Slider
|
||||
label="Bulk Rounds"
|
||||
value={bulkRounds}
|
||||
min={100}
|
||||
max={10000}
|
||||
step={100}
|
||||
onChange={setBulkRounds}
|
||||
unit=""
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
iconLeft="help-circle"
|
||||
onClick={() => setIsHelpOpen(true)}
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className={styles.mainContent}>
|
||||
<div className={styles.leftPanel}>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
initialValue={initialCode}
|
||||
onChange={() => {}}
|
||||
onReady={() => setIsEditorReady(true)}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.rightPanel}>
|
||||
<div className={styles.terminalPanel}>
|
||||
<div className={styles.panelHeader}>Terminal</div>
|
||||
<div className={styles.terminalContainer}>
|
||||
<Terminal
|
||||
ref={terminalRef}
|
||||
onData={sendDataToScript}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.outputPanel}>
|
||||
<div className={styles.panelHeader}>Output</div>
|
||||
<textarea
|
||||
ref={resultRef}
|
||||
className={styles.result}
|
||||
readOnly
|
||||
autoComplete="off"
|
||||
placeholder="Use print() to produce output"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SidePanel isOpen={isHelpOpen} onClose={() => setIsHelpOpen(false)}>
|
||||
<h2>Game Rules</h2>
|
||||
|
||||
<p>
|
||||
This game is played in two rounds,
|
||||
on an empty eleven-space board.
|
||||
The first round is played on {"Red's"} board, the second is played on {"Blue's"}.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
On {"Red's"} board, {"Red's"} goal is to maximize the value of the expression.
|
||||
{" Blue's"} goal is to minimize it.
|
||||
|
||||
Players take turns placing the fourteen symbols <code>0123456789+-×÷</code>
|
||||
on the board, with the maximizing player taking the first move.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A {"board's"} syntax must always be valid, and
|
||||
the following rules are enforced:
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li>Each symbol may only be used once</li>
|
||||
<li>The binary operators <code>+-×÷</code> may not be next to one another, and may not be at the end slots.</li>
|
||||
<li>The unary operator <code>-</code> (negative) must have a number as an argument. Therefore, it cannot be left of an operator (like <code>-×</code>), and it may not be in the rightmost slot.</li>
|
||||
<li>Unary <code>+</code> may not be used.</li>
|
||||
<li> <code>0</code> may not follow <code>÷</code>.
|
||||
This prevents most cases of zero-division, but {"isn't perfect"}.
|
||||
<code>÷-0</code> will result in an invalid board (causing a draw),
|
||||
and <code>÷0_+</code> is forbidden despite being valid syntax once the empty slot is filled.
|
||||
This is done to simplyify game logic, and might be improved later.
|
||||
</li>
|
||||
<li>Division by zero results in a draw.</li>
|
||||
<li>An incomplete board with no valid moves results in a draw.</li>
|
||||
</ol>
|
||||
|
||||
<h2>How to Play</h2>
|
||||
<ol>
|
||||
<li>Click <strong>Run</strong> to start a single game. Play against your agent in the terminal. Use your arrow keys (up, down, left, right) to select a symbol. Use enter or space to make a move.</li>
|
||||
<li>Click <strong>Bulk Run</strong> to collect statistics from a many games.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
<h2>Overview</h2>
|
||||
<ul>
|
||||
<li><code>step_min()</code> is called once per turn with the {"board's"} current state. This function must return an <code>Action</code> that aims to minimize the total value of the board.</li>
|
||||
<li><code>step_max()</code> is just like <code>step_min</code>, but should aim to maximize the value of the board. </li>
|
||||
<li>Agent code may not be edited between games. Start a new game to use new code.</li>
|
||||
<li>If your agent takes more than 5 seconds to compute a move, the script will exit with an error.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Rhai basics</h2>
|
||||
<p>
|
||||
Agents are written in <a href="https://rhai.rs">Rhai</a>, a wonderful embedded scripting language powered by Rust.
|
||||
Basic language features are outlined below.
|
||||
</p>
|
||||
<ul>
|
||||
<li>All statements must be followed by a <code>;</code></li>
|
||||
<li>Use <code>return</code> to return a value from a function.</li>
|
||||
<li><code>print(anything)</code> - Prints to the output panel. Prefer this over <code>debug</code>.</li>
|
||||
<li><code>debug(anything)</code> - Prints to the output panel. Includes extra debug info.</li>
|
||||
<li><code>()</code> is the {"\"none\""} type, returned by some methods above.</li>
|
||||
<li><code>for i in 0..5 {"{}"}</code> will iterate five times, with <code>i = 0, 1, 2, 3, 4</code></li>
|
||||
<li><code>for i in 0..=5 {"{}"}</code> will iterate six times, with <code>i = 0, 1, 2, 3, 4, 5</code></li>
|
||||
<li><code>let a = [];</code> initializes an empty array.</li>
|
||||
<li><code>a.push(value)</code> adds a value to the end of an array</li>
|
||||
<li><code>a.pop()</code> removes a value from the end of an array and returns it</li>
|
||||
<li><code>a[0]</code> returns the first item of an array</li>
|
||||
<li><code>a[1]</code> returns the second item of an array</li>
|
||||
<li>Refer to <a href="https://rhai.rs/book/language/values-and-types.html">the Rhai book</a> for more details.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Notable Functions</h2>
|
||||
<ul>
|
||||
<li><code>Action(symbol, position)</code> - Creates a new action that places <code>symbol</code> at <code>position</code>. Valid symbols are <code>01234567890+-/*</code>. Both <code>0</code> and <code>{"\"0\""}</code> are valid symbols.</li>
|
||||
<li><code>board.can_play(action)</code> - Checks if an action is valid. Returns a boolean.</li>
|
||||
<li><code>board.size()</code> - Return the total number of spots on this board.</li>
|
||||
<li><code>board.free_spots()</code> - Count the number of free spots on the board.</li>
|
||||
<li><code>board.play(action)</code> - Apply the given action on this board. This mutates the <code>board</code>, but does NOT make the move in the game. The only way to commit to an action is to return it from <code>step_min</code> or <code>step_max</code>.
|
||||
This method lets you compute potential values of a board when used with <code>board.evaluate()</code>.
|
||||
</li>
|
||||
<li><code>board.ith_free_slot(idx)</code> - Returns the index of the <code>n</code>th free slot on this board. Returns <code>-1</code> if no such slot exists.</li>
|
||||
<li><code>board.contains(symbol)</code> - Checks if this board contains the given symbol. Returns a boolean.</li>
|
||||
<li><code>board.evaluate()</code> - Return the value of a board if it can be computed. Returns <code>()</code> otherwise.</li>
|
||||
<li><code>board.free_spots_idx(action)</code> - Checks if an action is valid. Returns a boolean.</li>
|
||||
<li><code>for i in board {"{ ... }"}</code> - Iterate over all slots on this board. Items are returned as strings, empty slots are the empty string (<code>{"\"\""}</code>)</li>
|
||||
<li><code>is_op(symbol)</code> - Returns <code>true</code> if <code>symbol</code> is one of <code>+-*/</code></li>
|
||||
|
||||
<li><code>rand_symb()</code> - Returns a random symbol (number or operation)</li>
|
||||
<li><code>rand_op()</code> - Returns a random operator symbol (one of <code>+-*/</code>)</li>
|
||||
<li><code>rand_action()</code> - Returns a random <code>Action</code></li>
|
||||
<li><code>rand_int(min, max)</code> - Returns a random integer between min and max, including both endpoints.</li>
|
||||
<li><code>rand_bool(probability)</code> - Return <code>true</code> with the given probability. Otherwise return <code>false</code>.</li>
|
||||
<li><code>rand_shuffle(array)</code> - Shuffle the given array</li>
|
||||
<li><code>for p in permutations(array, 5) {"{}"}</code> - Iterate over all permutations of 5 elements of the given array.</li>
|
||||
</ul>
|
||||
|
||||
</SidePanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user