From 5ce2371dc1bb1e0158db9fb82c56db16df55439e Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:58:10 -0700 Subject: [PATCH] docs --- rust/minimax/src/agents/rhai.rs | 2 +- webui/src/components/Playground.tsx | 141 ++++++++++++---------- webui/src/components/ui/SidePanel.tsx | 57 +++++++++ webui/src/styles/SidePanel.module.css | 166 ++++++++++++++++++++++++++ 4 files changed, 303 insertions(+), 63 deletions(-) create mode 100644 webui/src/components/ui/SidePanel.tsx create mode 100644 webui/src/styles/SidePanel.module.css diff --git a/rust/minimax/src/agents/rhai.rs b/rust/minimax/src/agents/rhai.rs index abfb948..ed68aac 100644 --- a/rust/minimax/src/agents/rhai.rs +++ b/rust/minimax/src/agents/rhai.rs @@ -153,7 +153,7 @@ impl RhaiAgent { }) .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(); diff --git a/webui/src/components/Playground.tsx b/webui/src/components/Playground.tsx index 2051a14..db25c20 100644 --- a/webui/src/components/Playground.tsx +++ b/webui/src/components/Playground.tsx @@ -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(null); const resultRef = useRef(null); @@ -184,15 +181,6 @@ export default function Playground() {
- ({ - text: item.text, - onClick: () => {}, - }))} - /> - - -

What is Rhai?

-

- - Rhai - {" "} - is an embedded scripting language and - evaluation engine for Rust that gives a - safe and easy way to add scripting to - any application. -

-

Hotkeys

-

- You can run the script by pressing{" "} - Ctrl + Enter when - focused in the editor. -

-
- - - Rhai Playground - {" "} - version: 0.1.0 - -
- - compiled with Rhai (placeholder) - -
-
- } - /> + @@ -293,6 +243,73 @@ export default function Playground() { + + setIsHelpOpen(false)}> +

Game Rules

+ + +

How to Play

+
    +
  1. Click Run 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.
  2. +
  3. Click Bulk Run to collect statistics from a many games.
  4. +
+ + +

Overview

+
    +
  • step_min() is called once per turn with the {"board's"} current state. This function must return an Action that aims to minimize the total value of the board.
  • +
  • step_max() is just like step_min, but should aim to maximize the value of the board.
  • +
  • Agent code may not be edited between games. Start a new game to use new code.
  • +
  • If your agent takes more than 5 seconds to compute a move, the script will exit with an error.
  • +
+ +

Available Functions

+
    +
  • Action(symbol, position) - Creates a new action that places symbol at position. Valid symbols are 01234567890+-/*. Both 0 and {"\"0\""} are valid symbols.
  • +
  • board.can_play(action) - Checks if an action is valid. Returns a boolean.
  • +
  • board.size() - Return the total number of spots on this board.
  • +
  • board.free_spots() - Count the number of free spots on the board.
  • +
  • board.play(action) - Apply the given action on this board. This mutates the board, but does NOT make the move in the game. The only way to commit to an action is to return it from step_min or step_max. + This method lets you compute potential values of a board when used with board.evaluate(). +
  • +
  • board.ith_free_slot(idx) - Returns the index of the nth free slot on this board. Returns -1 if no such slot exists.
  • +
  • board.contains(symbol) - Checks if this board contains the given symbol. Returns a boolean.
  • +
  • board.evaluate() - Return the value of a board if it can be computed. Returns () otherwise.
  • +
  • board.free_spots_idx(action) - Checks if an action is valid. Returns a boolean.
  • +
  • for i in board {"{ ... }"} - Iterate over all slots on this board. Items are returned as strings, empty slots are the empty string ({"\"\""})
  • +
  • is_op(symbol) - Returns true if symbol is one of +-*/
  • + +
  • rand_symb() - Returns a random symbol (number or operation)
  • +
  • rand_op() - Returns a random operator symbol (one of +-*/)
  • +
  • rand_action() - Returns a random Action
  • +
  • rand_int(min, max) - Returns a random integer between min and max, NOT including max.
  • +
  • rand_bool(probability) - Return true with the given probability. Otherwise return false.
  • +
  • rand_shuffle(array) - Shuffle the given array
  • +
  • for p in permutations(array, 5) {"{}"} - Iterate over all permutations of 5 elements of the given array.
  • +
+ +

Rhai basics

+

+ Agents are written in Rhai, a wonderful embedded scripting language powered by Rust. + Basic language features are outlined below. +

+
    +
  • All statements must be followed by a ;
  • +
  • Use return to return a value from a function.
  • +
  • print(any type) - Prints a message to the Output panel. Prefer this over debug.
  • +
  • debug(any type) - Prints a message to the Output panel, with extra debug info.
  • +
  • () is the {"\"none\""} type, returned by some methods above.
  • +
  • for i in 0..5 {"{}"} will iterate five times, with i = 0, 1, 2, 3, 4
  • +
  • for i in 0..=5 {"{}"} will iterate six times, with i = 0, 1, 2, 3, 4, 5
  • +
  • let a = []; initializes an empty array.
  • +
  • a.push(value) adds a value to the end of an array
  • +
  • a.pop() removes a value from the end of an array and returns it
  • +
  • a[0] returns the first item of an array
  • +
  • a[1] returns the second item of an array
  • +
  • Refer to the Rhai book for more details.
  • +
+ +
); } diff --git a/webui/src/components/ui/SidePanel.tsx b/webui/src/components/ui/SidePanel.tsx new file mode 100644 index 0000000..1dfb907 --- /dev/null +++ b/webui/src/components/ui/SidePanel.tsx @@ -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 ( + <> +
+
+ +
{children}
+
+ + ); +} \ No newline at end of file diff --git a/webui/src/styles/SidePanel.module.css b/webui/src/styles/SidePanel.module.css new file mode 100644 index 0000000..205cc0f --- /dev/null +++ b/webui/src/styles/SidePanel.module.css @@ -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%; + } +} \ No newline at end of file