docs
This commit is contained in:
@@ -153,7 +153,7 @@ impl<R: Rng + 'static> RhaiAgent<R> {
|
||||
})
|
||||
.register_fn("rand_bool", {
|
||||
let rng = rng.clone();
|
||||
move |p: f32| rng.lock().gen_bool(p as f64)
|
||||
move |p: f32| rng.lock().gen_bool((p as f64).clamp(0.0, 1.0))
|
||||
})
|
||||
.register_fn("rand_symb", {
|
||||
let rng = rng.clone();
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 {
|
||||
@@ -20,34 +21,30 @@ fn random_action(board) {
|
||||
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
|
||||
return action;
|
||||
}
|
||||
|
||||
fn step_min(board) {
|
||||
random_action(board)
|
||||
return random_action(board);
|
||||
}
|
||||
|
||||
fn step_max(board) {
|
||||
random_action(board)
|
||||
return random_action(board);
|
||||
}
|
||||
`;
|
||||
|
||||
const exampleScriptList = [
|
||||
{ value: "./hello.rhai", text: "hello.rhai" },
|
||||
{ value: "./fibonacci.rhai", text: "fibonacci.rhai" },
|
||||
{ value: "./arrays.rhai", text: "arrays.rhai" },
|
||||
];
|
||||
|
||||
export default function Playground() {
|
||||
const [isScriptRunning, setIsScriptRunning] = useState(false);
|
||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||
const [fontSize, setFontSize] = useState(14);
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
const resultRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -184,15 +181,6 @@ export default function Playground() {
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonGroup}>
|
||||
<Dropdown
|
||||
trigger="Example Scripts"
|
||||
disabled={isScriptRunning}
|
||||
items={exampleScriptList.map((item) => ({
|
||||
text: item.text,
|
||||
onClick: () => {},
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
trigger="Config"
|
||||
align="right"
|
||||
@@ -211,50 +199,12 @@ export default function Playground() {
|
||||
}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
triggerIcon="help-circle"
|
||||
align="right"
|
||||
customContent={
|
||||
<div className={styles.helpPanel}>
|
||||
<h1>What is Rhai?</h1>
|
||||
<p>
|
||||
<a
|
||||
href="https://rhai.rs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Rhai
|
||||
</a>{" "}
|
||||
is an embedded scripting language and
|
||||
evaluation engine for Rust that gives a
|
||||
safe and easy way to add scripting to
|
||||
any application.
|
||||
</p>
|
||||
<h1>Hotkeys</h1>
|
||||
<p>
|
||||
You can run the script by pressing{" "}
|
||||
<kbd>Ctrl</kbd> + <kbd>Enter</kbd> when
|
||||
focused in the editor.
|
||||
</p>
|
||||
<div className={styles.footer}>
|
||||
<span>
|
||||
<a
|
||||
href="https://github.com/rhaiscript/playground"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Rhai Playground
|
||||
</a>{" "}
|
||||
version: 0.1.0
|
||||
</span>
|
||||
<br />
|
||||
<span>
|
||||
compiled with Rhai (placeholder)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
iconLeft="help-circle"
|
||||
onClick={() => setIsHelpOpen(true)}
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -293,6 +243,73 @@ export default function Playground() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SidePanel isOpen={isHelpOpen} onClose={() => setIsHelpOpen(false)}>
|
||||
<h2>Game Rules</h2>
|
||||
|
||||
|
||||
<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>Available 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, NOT including max.</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>
|
||||
|
||||
<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(any type)</code> - Prints a message to the Output panel. Prefer this over <code>debug</code>.</li>
|
||||
<li><code>debug(any type)</code> - Prints a message to the Output panel, with 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>
|
||||
|
||||
</SidePanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
57
webui/src/components/ui/SidePanel.tsx
Normal file
57
webui/src/components/ui/SidePanel.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import styles from "@/styles/SidePanel.module.css";
|
||||
|
||||
interface SidePanelProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function SidePanel({ isOpen, onClose, children }: SidePanelProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsVisible(true);
|
||||
// Small delay to trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
setIsAnimating(true);
|
||||
});
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
// Wait for animation to complete before hiding
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
}, 300); // Match animation duration
|
||||
document.body.style.overflow = "";
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${styles.overlay} ${isAnimating ? styles.overlayVisible : ""}`}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className={`${styles.sidePanel} ${isAnimating ? styles.sidePanelOpen : ""}`}>
|
||||
<button className={styles.closeButton} onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
166
webui/src/styles/SidePanel.module.css
Normal file
166
webui/src/styles/SidePanel.module.css
Normal file
@@ -0,0 +1,166 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 1000;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.overlayVisible {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.sidePanel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
background: #1e1e1e;
|
||||
z-index: 1001;
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidePanelOpen {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
padding: 4px 12px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease, background 0.2s ease;
|
||||
border-radius: 4px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
font-size: 28px;
|
||||
margin: 0 0 24px 0;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
font-size: 20px;
|
||||
margin: 32px 0 16px 0;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content h3 {
|
||||
font-size: 16px;
|
||||
margin: 24px 0 12px 0;
|
||||
color: #e0e0e0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content p {
|
||||
line-height: 1.6;
|
||||
margin: 0 0 16px 0;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.content ul,
|
||||
.content ol {
|
||||
margin: 0 0 16px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.content li {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.content code {
|
||||
background: #2d2d30;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
background: #2d2d30;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.content pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content a {
|
||||
color: #4fc3f7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content kbd {
|
||||
background: #2d2d30;
|
||||
border: 1px solid #444;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
color: #e0e0e0;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidePanel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user