755 lines
20 KiB
TypeScript
755 lines
20 KiB
TypeScript
"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 { AgentSelector } from "@/components/ui/AgentSelector";
|
||
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);
|
||
}`;
|
||
|
||
const AGENTS = {
|
||
// special-cased below
|
||
Self: undefined,
|
||
|
||
Random: `fn random_action(board) {
|
||
let symb = rand_symb();
|
||
let pos = rand_int(0, 10);
|
||
let action = Action(symb, pos);
|
||
|
||
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 [selectedAgent, setSelectedAgent] = useState("Random");
|
||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||
const [scriptName, setScriptName] = useState("");
|
||
const [saveSecret, setSaveSecret] = useState("");
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
const [availableAgents, setAvailableAgents] = useState<string[]>([]);
|
||
const [savedScripts, setSavedScripts] = useState<Record<string, string>>(
|
||
{}
|
||
);
|
||
|
||
const editorRef = useRef<any>(null);
|
||
const resultRef = useRef<HTMLTextAreaElement>(null);
|
||
const terminalRef = useRef<TerminalRef>(null);
|
||
|
||
const runDisabled = isScriptRunning || !isEditorReady;
|
||
const stopDisabled = !isScriptRunning;
|
||
|
||
// Fetch saved scripts and update available agents
|
||
const loadSavedScripts = useCallback(async () => {
|
||
try {
|
||
const response = await fetch("/api/list-scripts");
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
const scripts = data.scripts || [];
|
||
const scriptContents: Record<string, string> = {};
|
||
|
||
// Fetch content for each saved script
|
||
await Promise.all(
|
||
scripts.map(async (scriptName: string) => {
|
||
try {
|
||
const scriptResponse = await fetch(
|
||
`/api/get-script?name=${encodeURIComponent(
|
||
scriptName
|
||
)}`
|
||
);
|
||
const scriptData = await scriptResponse.json();
|
||
|
||
if (scriptResponse.ok) {
|
||
scriptContents[scriptName] = scriptData.content;
|
||
}
|
||
} catch (error) {
|
||
console.error(
|
||
`Failed to load script ${scriptName}:`,
|
||
error
|
||
);
|
||
}
|
||
})
|
||
);
|
||
|
||
setSavedScripts(scriptContents);
|
||
|
||
// Combine hardcoded agents with saved scripts, ensuring Self and Random are first
|
||
const combinedAgents = [
|
||
"Self",
|
||
"Random",
|
||
...Object.keys(AGENTS).filter(
|
||
(key) => key !== "Self" && key !== "Random"
|
||
),
|
||
...scripts,
|
||
];
|
||
|
||
setAvailableAgents(combinedAgents);
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load saved scripts:", error);
|
||
// Fallback to hardcoded agents only
|
||
setAvailableAgents(Object.keys(AGENTS));
|
||
}
|
||
}, []);
|
||
|
||
// Load saved scripts on component mount
|
||
useEffect(() => {
|
||
loadSavedScripts();
|
||
}, [loadSavedScripts]);
|
||
|
||
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) {
|
||
console.error(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) return;
|
||
|
||
setIsScriptRunning(true);
|
||
|
||
try {
|
||
terminalRef.current?.clear();
|
||
terminalRef.current?.focus();
|
||
|
||
// Get script content from either hardcoded agents or saved scripts
|
||
const hardcodedAgent = AGENTS[selectedAgent as keyof typeof AGENTS];
|
||
const savedScript = savedScripts[selectedAgent];
|
||
|
||
const agentScript = hardcodedAgent || savedScript;
|
||
const blueScript =
|
||
agentScript || (editorRef.current?.getValue() ?? "");
|
||
const redScript = editorRef.current?.getValue() ?? "";
|
||
const opponentName = agentScript ? selectedAgent : "script";
|
||
|
||
await startScriptBulk(
|
||
redScript,
|
||
blueScript,
|
||
opponentName,
|
||
(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) {
|
||
console.error(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, selectedAgent, savedScripts]);
|
||
|
||
const stopScriptHandler = useCallback(() => {
|
||
stopScript();
|
||
}, []);
|
||
|
||
const saveScript = useCallback(async () => {
|
||
if (!scriptName.trim()) {
|
||
alert("Please enter a script name");
|
||
return;
|
||
}
|
||
|
||
if (!saveSecret.trim()) {
|
||
alert("Please enter the save secret");
|
||
return;
|
||
}
|
||
|
||
if (!editorRef.current) {
|
||
alert("No script content to save");
|
||
return;
|
||
}
|
||
|
||
setIsSaving(true);
|
||
|
||
try {
|
||
const response = await fetch("/api/save-script", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
name: scriptName.trim(),
|
||
content: editorRef.current.getValue(),
|
||
secret: saveSecret.trim(),
|
||
}),
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
alert(result.message);
|
||
setScriptName("");
|
||
// Reload saved scripts to include the new one
|
||
loadSavedScripts();
|
||
} else {
|
||
alert(result.error || "Failed to save script");
|
||
}
|
||
} catch (error) {
|
||
console.error("Save error:", error);
|
||
alert("Network error: Failed to save script");
|
||
}
|
||
|
||
setIsSaving(false);
|
||
}, [scriptName, saveSecret, loadSavedScripts]);
|
||
|
||
const copyScriptToEditor = useCallback(() => {
|
||
if (!selectedAgent || selectedAgent === "Self") {
|
||
alert("Please select a script to copy to the editor");
|
||
return;
|
||
}
|
||
|
||
// Get the script content
|
||
const hardcodedAgent = AGENTS[selectedAgent as keyof typeof AGENTS];
|
||
const savedScript = savedScripts[selectedAgent];
|
||
const scriptContent = hardcodedAgent || savedScript;
|
||
|
||
if (!scriptContent) {
|
||
alert("No script content available for the selected agent");
|
||
return;
|
||
}
|
||
|
||
// Warn user about losing current content
|
||
const confirmed = confirm(
|
||
`This will replace your current script with "${selectedAgent}". Your current work will be lost. Continue?`
|
||
);
|
||
|
||
if (confirmed && editorRef.current) {
|
||
editorRef.current.setValue(scriptContent);
|
||
}
|
||
}, [selectedAgent, savedScripts]);
|
||
|
||
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 className={styles.configField}>
|
||
<label>Bulk opponent</label>
|
||
<AgentSelector
|
||
agents={availableAgents}
|
||
selectedAgent={selectedAgent}
|
||
onSelect={setSelectedAgent}
|
||
placeholder="Select an agent..."
|
||
/>
|
||
</div>
|
||
|
||
<div className={styles.configField}>
|
||
<Button
|
||
variant="info"
|
||
onClick={copyScriptToEditor}
|
||
disabled={
|
||
!selectedAgent ||
|
||
selectedAgent === "Self"
|
||
}
|
||
>
|
||
Copy to Editor
|
||
</Button>
|
||
</div>
|
||
|
||
<div className={styles.saveSection}>
|
||
<div className={styles.configField}>
|
||
<label>Script name</label>
|
||
<input
|
||
type="text"
|
||
className={styles.saveInput}
|
||
placeholder="Enter script name..."
|
||
value={scriptName}
|
||
onChange={(e) =>
|
||
setScriptName(
|
||
e.target.value
|
||
)
|
||
}
|
||
maxLength={32}
|
||
/>
|
||
</div>
|
||
|
||
<div className={styles.configField}>
|
||
<label>Save secret</label>
|
||
<input
|
||
type="password"
|
||
className={styles.saveInput}
|
||
placeholder="Enter save secret..."
|
||
value={saveSecret}
|
||
onChange={(e) =>
|
||
setSaveSecret(
|
||
e.target.value
|
||
)
|
||
}
|
||
/>
|
||
</div>
|
||
|
||
<div className={styles.configField}>
|
||
<Button
|
||
variant="primary"
|
||
onClick={saveScript}
|
||
loading={isSaving}
|
||
disabled={isSaving}
|
||
>
|
||
Save Script
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
);
|
||
}
|