One of the most powerful things you can do with a free API is execute arbitrary code in multiple programming languages directly from the browser, with no backend, no Docker setup, no language runtimes installed locally, and no configuration whatsoever. The Piston API makes this possible by providing a remote code execution service that supports over 60 programming languages, all accessible through a single REST endpoint.
Code Gladiator Arena takes this capability and turns it into a competitive, educational tool. Users write code in one of seven popular languages — Python, JavaScript, Rust, Go, C++, Java, and Ruby — execute it remotely, and see not just the output but also execution time, memory indicators, and error diagnostics. The "arena" aspect comes from the ability to solve the same problem in multiple languages side by side and compare the results: which language produces the shortest code? Which executes fastest? Which has the most elegant solution?
This project teaches you how to build a browser-based IDE (or at least the execution part of one), how to handle the unique challenges of remote code execution (timeouts, sandboxing, error parsing), and how to design a multi-language interface that respects each language's idioms while providing a consistent user experience.
Piston is an open-source code execution engine that runs untrusted code in isolated containers. It was originally built for Discord bots that execute code snippets, but its HTTP API makes it perfect for browser-based applications. The public instance at emkc.org is free to use and supports a staggering number of languages.
| Property | Detail |
|---|---|
| Base URL | https://emkc.org/api/v2/piston |
| Auth | None required |
| Rate Limit | 5 requests/second (public instance) |
| Format | JSON |
| CORS | Enabled |
| Max Execution | ~10 seconds per run |
Piston has two primary endpoints:
GET /runtimes — List all available languages and their versionsPOST /execute — Execute code in a specified languageThe /execute endpoint accepts a JSON body with the language, version, source code files, stdin input, and optional resource limits. The response separates stdout from stderr and includes the exit code, making it possible to distinguish successful execution from runtime errors.
// Request body structure
{
"language": "python",
"version": "3.10.0",
"files": [
{
"name": "main.py",
"content": "print('Hello from Python!')"
}
],
"stdin": "",
"args": [],
"compile_timeout": 10000,
"run_timeout": 5000,
"compile_memory_limit": -1,
"run_memory_limit": -1
}
// Response structure
{
"language": "python",
"version": "3.10.0",
"run": {
"stdout": "Hello from Python!\n",
"stderr": "",
"output": "Hello from Python!\n",
"code": 0,
"signal": null
},
"compile": null // null for interpreted languages
}
While Piston supports 60+ languages, we focus on seven that represent different paradigms and are commonly compared in benchmarks and coding challenges:
| Language | Paradigm | Compiled | Piston ID |
|---|---|---|---|
| Python | Dynamic, interpreted | No | python |
| JavaScript | Dynamic, JIT-compiled | No | javascript |
| Rust | Systems, compiled | Yes | rust |
| Go | Compiled, concurrent | Yes | go |
| C++ | Systems, compiled | Yes | c++ |
| Java | OOP, bytecode compiled | Yes | java |
| Ruby | Dynamic, interpreted | No | ruby |
Code Gladiator Arena's architecture centers around three core concerns: code editing, code execution, and result presentation. Each has distinct requirements and challenges.
A full code editor in the browser is a complex problem (Monaco, CodeMirror, and Ace editor each represent thousands of hours of development). For our purposes, we use a <textarea> enhanced with basic keyboard handling (tab insertion, auto-indent) rather than embedding a full editor library. This keeps the project dependency-free while providing a usable editing experience.
This layer manages communication with Piston. It handles request formatting (each language has different filename conventions), timeout management (aborting requests that take too long), and response normalization (compiled languages return both compile and run phases).
When the user runs the same algorithm in multiple languages, the comparison layer collects and displays the results side by side. It tracks execution time, output correctness (do all languages produce the same output?), and code length metrics.
// Architecture overview
//
// [Code Editor] ─── language-specific config
// | (filename, boilerplate)
// v
// [Execution Manager] ─── rate limiting
// | timeout handling
// | request queuing
// v
// [Piston API] ──→ { stdout, stderr, code, timing }
// |
// v
// [Result Processor] ─── error formatting
// | timing extraction
// | output diffing
// v
// [Comparison View] ─── side-by-side results
// performance ranking
// correctness verification
Each language needs specific configuration: the Piston language identifier, the version to use, the default filename, boilerplate code for a "Hello World," and the file extension. We also store a display color for each language to use in the UI.
const LANGUAGES = {
python: {
id: 'python',
version: '3.10.0',
name: 'Python',
filename: 'main.py',
extension: '.py',
color: '#3776ab',
boilerplate: `# Python 3.10
def main():
print("Hello from Python!")
if __name__ == "__main__":
main()
`,
helloWorld: 'print("Hello, World!")'
},
javascript: {
id: 'javascript',
version: '18.15.0',
name: 'JavaScript',
filename: 'main.js',
extension: '.js',
color: '#f7df1e',
boilerplate: `// Node.js 18
function main() {
console.log("Hello from JavaScript!");
}
main();
`,
helloWorld: 'console.log("Hello, World!");'
},
rust: {
id: 'rust',
version: '1.68.2',
name: 'Rust',
filename: 'main.rs',
extension: '.rs',
color: '#dea584',
boilerplate: `fn main() {
println!("Hello from Rust!");
}
`,
helloWorld: 'fn main() {\n println!("Hello, World!");\n}'
},
go: {
id: 'go',
version: '1.16.2',
name: 'Go',
filename: 'main.go',
extension: '.go',
color: '#00add8',
boilerplate: `package main
import "fmt"
func main() {
fmt.Println("Hello from Go!")
}
`,
helloWorld: 'package main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Hello, World!")\n}'
},
'c++': {
id: 'c++',
version: '10.2.0',
name: 'C++',
filename: 'main.cpp',
extension: '.cpp',
color: '#00599c',
boilerplate: `#include
using namespace std;
int main() {
cout << "Hello from C++!" << endl;
return 0;
}
`,
helloWorld: '#include \nusing namespace std;\n\nint main() {\n cout << "Hello, World!" << endl;\n return 0;\n}'
},
java: {
id: 'java',
version: '15.0.2',
name: 'Java',
filename: 'Main.java',
extension: '.java',
color: '#ed8b00',
boilerplate: `public class Main {
public static void main(String[] args) {
System.out.println("Hello from Java!");
}
}
`,
helloWorld: 'public class Main {\n public static void main(String[] args) {\n System.out.println("Hello, World!");\n }\n}'
},
ruby: {
id: 'ruby',
version: '3.0.1',
name: 'Ruby',
filename: 'main.rb',
extension: '.rb',
color: '#cc342d',
boilerplate: `# Ruby 3.0
def main
puts "Hello from Ruby!"
end
main
`,
helloWorld: 'puts "Hello, World!"'
}
};
Our API client handles request formatting, rate limiting, and response normalization. The rate limiter is crucial for the public Piston instance, which allows approximately 5 requests per second.
class PistonClient {
constructor() {
this.baseURL = 'https://emkc.org/api/v2/piston';
this.requestQueue = [];
this.processing = false;
this.minInterval = 250; // 4 requests/sec max
this.lastRequest = 0;
}
async execute(language, code, stdin = '') {
const config = LANGUAGES[language];
if (!config) {
throw new Error(`Unsupported language: ${language}`);
}
const body = {
language: config.id,
version: config.version,
files: [{
name: config.filename,
content: code
}],
stdin: stdin,
args: [],
compile_timeout: 10000,
run_timeout: 5000
};
return this.throttledRequest(body);
}
async throttledRequest(body) {
// Wait if we're too close to the last request
const now = Date.now();
const elapsed = now - this.lastRequest;
if (elapsed < this.minInterval) {
await new Promise(r =>
setTimeout(r, this.minInterval - elapsed)
);
}
this.lastRequest = Date.now();
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(), 30000
);
try {
const response = await fetch(
`${this.baseURL}/execute`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Piston error ${response.status}: ${errorText}`
);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(
'Execution timed out after 30 seconds'
);
}
throw error;
}
}
async getRuntimes() {
const response = await fetch(
`${this.baseURL}/runtimes`
);
if (!response.ok) {
throw new Error('Failed to fetch runtimes');
}
return response.json();
}
}
const piston = new PistonClient();
The execution manager wraps the Piston client with timing, error formatting, and result normalization. It distinguishes between compilation errors, runtime errors, and successful execution.
class ExecutionManager {
constructor(client) {
this.client = client;
}
async run(language, code, stdin = '') {
const startTime = performance.now();
try {
const result = await this.client.execute(
language, code, stdin
);
const endTime = performance.now();
const clientTime = Math.round(endTime - startTime);
return this.normalizeResult(
result, language, clientTime
);
} catch (error) {
return {
success: false,
language,
error: error.message,
stdout: '',
stderr: error.message,
exitCode: -1,
clientTimeMs: Math.round(
performance.now() - startTime
),
phase: 'network'
};
}
}
normalizeResult(raw, language, clientTimeMs) {
const hasCompile = raw.compile !== null
&& raw.compile !== undefined;
const compileError = hasCompile
&& raw.compile.code !== 0;
if (compileError) {
return {
success: false,
language,
phase: 'compile',
stdout: '',
stderr: this.formatError(
raw.compile.stderr || raw.compile.output,
language
),
exitCode: raw.compile.code,
clientTimeMs,
rawCompile: raw.compile,
rawRun: null
};
}
const run = raw.run;
const runtimeError = run.code !== 0;
return {
success: !runtimeError,
language,
phase: runtimeError ? 'runtime' : 'success',
stdout: run.stdout || '',
stderr: run.stderr || '',
exitCode: run.code,
signal: run.signal,
clientTimeMs,
rawCompile: raw.compile,
rawRun: run,
outputLines: (run.stdout || '').split('\n')
.filter(l => l.length > 0).length,
codeLength: undefined // set by caller
};
}
formatError(errorText, language) {
if (!errorText) return 'Unknown error';
// Clean up common error prefixes
let cleaned = errorText.trim();
// Remove file path noise from compiled languages
cleaned = cleaned.replace(
/\/tmp\/[a-f0-9-]+\//g, ''
);
// Limit error length
if (cleaned.length > 2000) {
cleaned = cleaned.substring(0, 2000)
+ '\n... (truncated)';
}
return cleaned;
}
async runMultiple(codeMap, stdin = '') {
// Run all languages in sequence (rate limiting)
const results = {};
for (const [lang, code] of Object.entries(codeMap)) {
results[lang] = await this.run(lang, code, stdin);
results[lang].codeLength = code.length;
}
return results;
}
}
const executor = new ExecutionManager(piston);
When the user solves a challenge in multiple languages, the comparison engine analyzes the results across several dimensions.
class ComparisonEngine {
static compare(results) {
const entries = Object.entries(results)
.filter(([, r]) => r.success);
if (entries.length === 0) {
return {
hasComparison: false,
message: 'No successful executions to compare'
};
}
// Speed ranking (by client-measured time)
const bySpeed = [...entries].sort(
(a, b) => a[1].clientTimeMs - b[1].clientTimeMs
);
// Code length ranking
const byLength = [...entries].sort(
(a, b) => a[1].codeLength - b[1].codeLength
);
// Output consistency check
const outputs = entries.map(([, r]) =>
r.stdout.trim()
);
const allSameOutput = outputs.every(
o => o === outputs[0]
);
// Calculate rankings
const rankings = {};
entries.forEach(([lang]) => {
const speedRank = bySpeed.findIndex(
([l]) => l === lang
) + 1;
const lengthRank = byLength.findIndex(
([l]) => l === lang
) + 1;
rankings[lang] = {
speedRank,
lengthRank,
overallScore: speedRank + lengthRank,
executionTime: results[lang].clientTimeMs,
codeLength: results[lang].codeLength
};
});
// Overall winner (lowest combined rank)
const overall = Object.entries(rankings)
.sort((a, b) =>
a[1].overallScore - b[1].overallScore
);
return {
hasComparison: true,
fastest: bySpeed[0][0],
shortest: byLength[0][0],
overallWinner: overall[0][0],
allSameOutput,
rankings,
details: {
speedSpread:
bySpeed[bySpeed.length - 1][1].clientTimeMs
- bySpeed[0][1].clientTimeMs,
lengthSpread:
byLength[byLength.length - 1][1].codeLength
- byLength[0][1].codeLength
}
};
}
static generateReport(results, comparison) {
if (!comparison.hasComparison) {
return comparison.message;
}
const lines = [
`--- GLADIATOR ARENA RESULTS ---`,
``,
`Fastest: ${LANGUAGES[comparison.fastest].name}` +
` (${results[comparison.fastest].clientTimeMs}ms)`,
`Shortest: ${LANGUAGES[comparison.shortest].name}` +
` (${results[comparison.shortest].codeLength} chars)`,
`Overall: ${LANGUAGES[comparison.overallWinner].name}`,
``,
`Output consistent: ${
comparison.allSameOutput ? 'Yes' : 'MISMATCH!'
}`,
``,
`Rankings:`
];
Object.entries(comparison.rankings)
.sort((a, b) =>
a[1].overallScore - b[1].overallScore
)
.forEach(([lang, rank], i) => {
lines.push(
` ${i + 1}. ${LANGUAGES[lang].name}` +
` - Speed: #${rank.speedRank}` +
` (${rank.executionTime}ms)` +
` | Length: #${rank.lengthRank}` +
` (${rank.codeLength} chars)`
);
});
return lines.join('\n');
}
}
Our lightweight editor handles tab insertion, basic auto-indent, and keyboard shortcuts for running code.
class CodeEditor {
constructor(containerId, language) {
this.container = document.getElementById(containerId);
this.language = language;
this.config = LANGUAGES[language];
this.textarea = null;
this.build();
}
build() {
this.container.innerHTML = `
<div class="editor-wrapper"
style="border-color: ${this.config.color}">
<div class="editor-header">
<span class="lang-badge"
style="background: ${this.config.color}">
${this.config.name}
</span>
<span class="filename">
${this.config.filename}
</span>
</div>
<textarea
class="code-input"
spellcheck="false"
autocapitalize="off"
autocomplete="off"
>${this.config.boilerplate}</textarea>
<div class="editor-footer">
<button class="run-btn">
Run (Ctrl+Enter)
</button>
<span class="char-count">0 chars</span>
</div>
</div>
`;
this.textarea =
this.container.querySelector('.code-input');
this.charCount =
this.container.querySelector('.char-count');
this.runBtn =
this.container.querySelector('.run-btn');
this.setupKeybindings();
this.updateCharCount();
}
setupKeybindings() {
this.textarea.addEventListener('keydown', (e) => {
// Tab inserts spaces instead of changing focus
if (e.key === 'Tab') {
e.preventDefault();
const start = this.textarea.selectionStart;
const end = this.textarea.selectionEnd;
const value = this.textarea.value;
this.textarea.value =
value.substring(0, start)
+ ' '
+ value.substring(end);
this.textarea.selectionStart =
this.textarea.selectionEnd = start + 4;
}
// Ctrl+Enter runs code
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.runBtn.click();
}
// Auto-indent on Enter
if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
const start = this.textarea.selectionStart;
const value = this.textarea.value;
const line = value.substring(0, start)
.split('\n').pop();
const indent = line.match(/^\s*/)[0];
const insertion = '\n' + indent;
this.textarea.value =
value.substring(0, start)
+ insertion
+ value.substring(this.textarea.selectionEnd);
this.textarea.selectionStart =
this.textarea.selectionEnd =
start + insertion.length;
}
});
this.textarea.addEventListener('input', () => {
this.updateCharCount();
});
}
updateCharCount() {
this.charCount.textContent =
`${this.textarea.value.length} chars`;
}
getCode() {
return this.textarea.value;
}
setCode(code) {
this.textarea.value = code;
this.updateCharCount();
}
setRunHandler(handler) {
this.runBtn.addEventListener('click', handler);
}
}
class GladiatorArena {
constructor() {
this.executor = new ExecutionManager(piston);
this.editors = {};
this.results = {};
this.activeLanguage = 'python';
}
initialize() {
// Create editor for active language
this.switchLanguage(this.activeLanguage);
// Language selector
this.renderLanguageSelector();
// Challenge panel
this.renderChallenges();
}
switchLanguage(lang) {
this.activeLanguage = lang;
this.editors[lang] = new CodeEditor(
'editor-container', lang
);
this.editors[lang].setRunHandler(async () => {
await this.runCode(lang);
});
// Restore previous code if any
const savedCode = this.getSavedCode(lang);
if (savedCode) {
this.editors[lang].setCode(savedCode);
}
}
async runCode(lang) {
const code = this.editors[lang].getCode();
this.saveCode(lang, code);
this.showRunning(lang);
const result = await this.executor.run(lang, code);
result.codeLength = code.length;
this.results[lang] = result;
this.renderResult(result);
// If we have results for multiple languages,
// show comparison
const successCount = Object.values(this.results)
.filter(r => r.success).length;
if (successCount >= 2) {
const comparison = ComparisonEngine.compare(
this.results
);
this.renderComparison(comparison);
}
}
async runChallenge(challenge) {
// Load challenge code for all languages
for (const [lang, code] of
Object.entries(challenge.solutions)) {
this.saveCode(lang, code);
}
// Run all in sequence
this.showRunning('all');
const codeMap = {};
for (const [lang, code] of
Object.entries(challenge.solutions)) {
codeMap[lang] = code;
}
this.results = await this.executor.runMultiple(
codeMap, challenge.stdin || ''
);
const comparison =
ComparisonEngine.compare(this.results);
this.renderAllResults(this.results);
this.renderComparison(comparison);
console.log(
ComparisonEngine.generateReport(
this.results, comparison
)
);
}
renderResult(result) {
const output = document.getElementById(
'output-container'
);
const langConfig = LANGUAGES[result.language];
const statusColor = result.success
? '#4ade80' : '#ef4444';
const statusText = result.success
? 'Success' : `Error (${result.phase})`;
output.innerHTML = `
<div class="result-panel">
<div class="result-header">
<span class="result-lang"
style="color: ${langConfig.color}">
${langConfig.name}
</span>
<span class="result-status"
style="color: ${statusColor}">
${statusText}
</span>
<span class="result-time">
${result.clientTimeMs}ms
</span>
</div>
${result.stdout ? `
<div class="output-section">
<h4>Output</h4>
<pre class="output-text">${
this.escapeHtml(result.stdout)
}</pre>
</div>
` : ''}
${result.stderr ? `
<div class="error-section">
<h4>${result.phase === 'compile'
? 'Compilation Error' : 'Error Output'}</h4>
<pre class="error-text">${
this.escapeHtml(result.stderr)
}</pre>
</div>
` : ''}
<div class="result-meta">
<span>Exit code: ${result.exitCode}</span>
${result.signal
? `<span>Signal: ${result.signal}</span>`
: ''}
<span>Output lines: ${result.outputLines || 0}</span>
</div>
</div>
`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
saveCode(lang, code) {
try {
localStorage.setItem(
`gladiator-${lang}`, code
);
} catch { /* storage full */ }
}
getSavedCode(lang) {
return localStorage.getItem(`gladiator-${lang}`);
}
showRunning(lang) {
const output = document.getElementById(
'output-container'
);
output.innerHTML = `
<div class="running-state">
<div class="spinner"></div>
<p>Executing ${
lang === 'all' ? 'all languages' :
LANGUAGES[lang].name
}...</p>
</div>
`;
}
renderComparison(comparison) {
// Implementation renders side-by-side ranking table
const el = document.getElementById('comparison');
if (!el || !comparison.hasComparison) return;
el.innerHTML = `
<div class="comparison-panel">
<h3>Arena Results</h3>
<div class="winner-badges">
<span class="badge fastest">
Fastest: ${LANGUAGES[comparison.fastest].name}
</span>
<span class="badge shortest">
Shortest: ${LANGUAGES[comparison.shortest].name}
</span>
<span class="badge overall">
Overall: ${LANGUAGES[comparison.overallWinner].name}
</span>
</div>
<p class="output-check">
${comparison.allSameOutput
? 'All languages produced identical output'
: 'Warning: Output differs between languages!'}
</p>
</div>
`;
}
renderLanguageSelector() {
// Renders language tabs
}
renderChallenges() {
// Renders challenge selection panel
}
renderAllResults(results) {
// Renders results for all languages in a grid
}
}
// Bootstrap
document.addEventListener('DOMContentLoaded', () => {
const arena = new GladiatorArena();
arena.initialize();
});
The public Piston instance at emkc.org enforces a rate limit of approximately 5 requests per second. When running code in 7 languages simultaneously, you will exceed this limit. Our throttled request queue with 250ms minimum intervals ensures we stay within bounds, but it means a 7-language comparison takes at least 1.75 seconds just for the API calls, plus execution time.
Our timing measurements use performance.now() to capture the total round-trip time from the browser. This includes network latency (typically 50-200ms), the Piston API overhead (container spin-up, compilation), and actual execution time. For compiled languages, there is an additional compilation phase. This means our timing reflects "user-perceived performance" rather than pure execution speed. For fair benchmarking, you would need server-side timing, which Piston does not currently expose.
Java requires the public class name to match the filename. Piston expects Main.java with public class Main. If a user names their class anything else, they get a confusing compilation error about the class name not matching the file. Our boilerplate uses the correct convention, but user-modified code can easily break this. Add a check that extracts the class name from the code and sets the filename accordingly.
function getJavaFilename(code) {
const match = code.match(
/public\s+class\s+(\w+)/
);
if (match) {
return `${match[1]}.java`;
}
return 'Main.java';
}
Go requires package main at the top of executable files. If a user deletes it, they get a compilation error. Similarly, unused imports are a compilation error in Go, which catches many users off guard. Consider adding a pre-submission check for Go that verifies the package declaration exists.
Rust compilation is notoriously slow, even for small programs. On Piston, a simple Rust program can take 2-3 seconds to compile before execution begins. This makes Rust appear much slower in our timing comparisons, even though the compiled binary might execute faster than Python's interpreted run. Consider separating compile time from run time in the comparison display.
While Piston runs code in sandboxed containers, the public instance is a shared resource. Users could theoretically submit code that attempts to mine cryptocurrency, make outbound network requests, or consume excessive resources. Piston's container limits mitigate most of these concerns, but be aware that submitted code is visible to the Piston service operator. Never submit code containing secrets, API keys, or sensitive data.
Candidates preparing for technical interviews can solve algorithm problems in multiple languages to identify which language they are most productive in. The side-by-side comparison reveals which language requires less boilerplate for common patterns, helping candidates make an informed choice about which language to use in their interview.
Instructors can set up challenges that students solve in a specified language, with the arena providing instant feedback. The multi-language mode lets students compare how the same algorithm looks in different paradigms, reinforcing concepts like static vs. dynamic typing, compiled vs. interpreted execution, and imperative vs. functional style.
Developers learning a new language can write code they already know in their familiar language, then translate it to the new language side by side. The instant execution feedback tightens the feedback loop compared to setting up a local development environment for an unfamiliar language.
For small algorithms, data transformations, or mathematical computations, the arena is faster than opening an IDE. Write a quick Python script to calculate something, run it, get the answer. No project files, no dependencies, no setup. This "scratchpad" use case is why browser-based code execution tools are popular even among experienced developers.
Code golf challenges competitors to solve a problem in the fewest characters possible. The arena's character count feature and multi-language support make it a natural platform for code golf. Different languages have different strengths: Python's concise syntax versus Perl's regex power versus APL's array operations.
Call the /runtimes endpoint once at startup and cache the result. This lets you show the user which language versions are available without repeated API calls. Cache in localStorage with a 24-hour TTL since runtime versions change infrequently.
async function getCachedRuntimes() {
const CACHE_KEY = 'piston-runtimes';
const CACHE_TTL = 24 * 60 * 60 * 1000;
try {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const { runtimes, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_TTL) {
return runtimes;
}
}
} catch { /* cache miss */ }
const runtimes = await piston.getRuntimes();
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({
runtimes,
timestamp: Date.now()
}));
} catch { /* storage full */ }
return runtimes;
}
Save the user's code to localStorage on every input event (debounced). This prevents code loss on accidental page refresh and lets users pick up where they left off. Key by language name so each language's code is stored independently.
For very long code (hundreds of lines), textarea input events can cause jank if the character counter or other derived values are computed on every keystroke. Debounce the character count update to 100ms. For the code persistence save, debounce to 1000ms.
For preset challenges, consider pre-submitting the solutions to Piston during idle time (after the page loads but before the user selects a challenge). Cache the results so that when the user clicks "Run Challenge," the results display instantly. This is speculative execution — the same pattern CPU designers use to improve throughput.
Replace the basic textarea with CodeMirror 6 or Monaco Editor for full syntax highlighting, autocomplete, bracket matching, and error markers. Both editors support all seven of our languages and are widely used in production web IDEs. The integration is straightforward — swap the textarea for an editor instance and keep the same run/save logic.
Create a challenge system with predefined test cases. The user submits their solution, and the arena runs it against multiple inputs and verifies the outputs. Display pass/fail status for each test case. This transforms the arena from a playground into a competitive coding platform.
Encode the current code, language selection, and stdin into a URL hash or a short code stored in a lightweight backend. Users can share their solutions with a link. This is how sites like JSFiddle and CodePen work, and it transforms the arena into a collaboration tool.
For algorithms that process large inputs, run the same code with increasing input sizes and plot execution time versus input size. This reveals algorithmic complexity (O(n), O(n log n), O(n^2)) empirically. The arena could automatically generate inputs of size 100, 1000, 10000, and 100000, run the code for each, and plot the resulting curve.
Integrate with a language model API to automatically translate code from one language to another. The user writes a solution in Python, clicks "Translate to Rust," and the AI generates the Rust equivalent. Then both are executed to verify that they produce the same output. This is an advanced extension but extraordinarily useful for polyglot development.
Code Gladiator Arena demonstrates that a single well-designed API can be the foundation for a rich, multi-faceted application. Piston's /execute endpoint is conceptually simple (send code, get output), but when combined with thoughtful client-side architecture — language configuration, execution management, comparison engines, and editor integration — it becomes the backbone of a tool that serves education, interview prep, code golf, and rapid prototyping.
The challenges we solved here are universal to any code execution platform: timing measurement across different execution models (compiled vs. interpreted), error classification (syntax, compilation, runtime, timeout), sandboxing concerns, rate limiting strategies, and the UX of presenting code and its results in a clear, scannable format. These same problems appear in production tools like Replit, CodeSandbox, and LeetCode.
The key architectural takeaway is the separation between the execution layer (Piston communication) and the application layer (comparison, challenges, persistence). The execution layer could be swapped for a different provider (Judge0, Sphere Engine, or a self-hosted solution) without changing the comparison engine, the editor, or the challenge system. This kind of clean separation is what makes software adaptable, and it is the hallmark of well-designed API integrations. Go build your arena, and may the best code win.