Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
stockfishapi
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
stockfishapi
Commits
37a67945
Commit
37a67945
authored
May 24, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
PUSHEEDDD
parent
2ffe26b9
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
2242 additions
and
84 deletions
+2242
-84
Dockerfile
Dockerfile
+2
-0
NewServer.pem
NewServer.pem
+27
-0
STOCKFISH_API_REFERENCE copy.txt
STOCKFISH_API_REFERENCE copy.txt
+485
-0
STOCKFISH_API_REFERENCE.txt
STOCKFISH_API_REFERENCE.txt
+813
-0
main.go
cmd/server/main.go
+3
-0
admin.go
internal/admin/admin.go
+91
-5
management.go
internal/api/management.go
+639
-0
personalities.go
internal/bots/personalities.go
+16
-0
bot_create.html
web/templates/bot_create.html
+69
-44
bot_edit.html
web/templates/bot_edit.html
+79
-31
bots.html
web/templates/bots.html
+18
-4
No files found.
Dockerfile
View file @
37a67945
...
...
@@ -45,6 +45,8 @@ COPY --from=stockfish-builder /stockfish/src/*.nnue /app/
COPY
--from=go-builder /app/stockfish-api /app/stockfish-api
RUN
mkdir
-p
/app/portraits
ENV
PORT=80
ENV
STOCKFISH_PATH=/usr/local/bin/stockfish
ENV
POOL_SIZE=12
...
...
NewServer.pem
0 → 100644
View file @
37a67945
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwNRFitb/O8fS9TaUoCZ/VJZUEehvdyb1EgCjPrQbTeT6TlZh
rvkzHYcGIKsgarI6wuP9aK4+rLW8SL9VP6Ey3G4CgY/Hx9ZxhoSN5N2ffZkJE1Ji
hvgkDXzSN+l4P3e422ICxuVQqozba8/o8pZo46EzgRry760i9RcR8Q8JsXysjeQ1
Q68F8JhUYt1GNQlc5/A1EHEHyv1XIMDkYQ0eart1iUf9uvU/tp6pFTNUq/UtL/BT
RaJdnShbstS7bsfZwkyRtzXUlu15z/xdCsoXbbz+GC4oV7thzZQ+eRS8sZBGTsHF
6AaNqvd3QQnbFEpUSDzK3xupVEvLw3BbwYFvxwIDAQABAoIBAB4Gr9F/yvynD/1p
A1mwxPEJ+4tSU1ENeunTuZfA+eN2PVfHcayKV2BIrzaVDxYuLKI+WC5du5qvLeNy
D7c5xa63XqKIHgbLKKBWsbWqoPQwyU397SOxLgP/pMhaDYRsgxd+Oop4GMiF6IDw
PgjQTQLtDhUTejLCFghuEDgmLE87oi4oV3m8y36Yl1gHSHLzHivk8tiJoFdd9Jw3
FdM9wPS2FcafGaT7CDhbmo8XtHgynxbjCAX6D8tOpsbhVuClseRLXMfhkai0UuO4
JhgJ4tvDoxW3G/3qZkSvL/jUr5gybUCjVAcBfE7HIvfcYKQxU+iX9R8Q7cWzFO/d
RjooSWECgYEA9DDSYxBXuOkVHq9KDWnRBWUslLk2i9nvPEL+JVPqzR6Q+uRnZzl/
j54XBd25lAcCkTDmjZTHKFroX5uvgBzHGfGZFGtoZuObfVkzK9eicupPqTe7rcN6
fTJWeJnNJsYbkGzv0jrqvBJOG+/9zGVsn7UQZvWHiS9lgKYrDz2Vx5cCgYEAyieS
3xFK+lytLgJ6pqNx6RuvEAKgouZi3sgYIxwyMSA9Yap/5po6Osj8w9X18Bvm6YYF
gok+Zx63pEB7296RrmGDxkOw5Hl/gH07Yx2hvM4et3RyvK3udOXCdcXgWN0ue/Uf
H75UZ4CLAmNALEUa9lOcB2uydVHOhXCmgPveH1ECgYAtShzLKM3MStaS8VnfsP+G
a6RgFRXrzEjVuWsfizfiQUgMcG5JM93Xyi9k9CGmNcKhIRuxqKVjc7DjgqGDNlMr
GacVpXIgmxhMoE2gVQcZHyIVNXQGn1nJfJuTFJt7FIUqPTohmLHOneqEvfcpgKor
2M4o+mLf6718pdUYp4hvEwKBgBSJhLBIz3cz5xwfgFphjHcEKvrTaYJjKXQ8m8cl
XCwFfHbpnWjODlBejt9OY1frXcAnr3Odgct0IW/8ZRjnOaGfooWH5vavKTbigiAF
qKLHxfMZT3a/rNQPa3wPiEU+4zQQqQLOkUCanIS3lJNqydxwjg9q74xfrT19Pk0o
SV6hAoGBAKfidUGqWGH3FugbgG2cm7rK54nh978brZLKglekR1RlRWKG7QpRP33v
D13y3BD1rRM3vguD2aABhwqbYVt1hjHA+mv+yDzJps08FtZIasiTRpm2mFanOD84
yKf+0/HMD2G45HzoMYdG6BdZ5HP1y4WFNfRoxjwCTnwyrDMNJhOl
-----END RSA PRIVATE KEY-----
STOCKFISH_API_REFERENCE copy.txt
0 → 100644
View file @
37a67945
================================================================================
STOCKFISH CHESS BOT API - COMPLETE REFERENCE
================================================================================
BASE URL: https://stockfishapi.caprover.al-arcade.com
================================================================================
ENDPOINTS
================================================================================
--------------------------------------------------------------------------------
1. GET MOVE FROM BOT
--------------------------------------------------------------------------------
POST /api/chess/move
Description:
Get a chess move from a specific bot personality. The bot will play according
to its configured skill level, depth, contempt, and blunder probability.
Response includes simulated human-like think time.
Headers:
Content-Type: application/json
Request Body:
{
"fen": "string (required) - FEN notation of the current board position",
"bot_id": "string (required) - ID of the bot to play against",
"time_limit_ms": 0 (optional, int) - time limit in ms. If 0 or omitted, uses depth-based search
}
Response (200 OK):
{
"best_move": "e2e4", // UCI move notation (from-square + to-square, e.g. e2e4, g1f3, e7e8q for promotion)
"evaluation": 0.35, // centipawn evaluation / 100. Positive = white advantage. 999.0 = white mates, -999.0 = black mates
"depth": 10, // search depth reached
"nodes": 125000, // nodes searched
"think_time_ms": 1500, // total time including artificial delay (simulates human thinking)
"pv": "e2e4 e7e5 g1f3" // principal variation (best line), space-separated UCI moves
}
Errors:
400: {"error": "invalid request body"}
400: {"error": "fen is required"}
400: {"error": "bot_id is required"}
404: {"error": "bot not found"}
500: {"error": "engine error: ..."}
Example:
curl -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/move \
-H "Content-Type: application/json" \
-d '{"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "bot_id": "nour"}'
--------------------------------------------------------------------------------
2. ANALYZE POSITION
--------------------------------------------------------------------------------
POST /api/chess/analyze
Description:
Deep multi-line analysis of a position at full Stockfish strength (skill 20).
Returns multiple candidate moves ranked by evaluation.
Headers:
Content-Type: application/json
Request Body:
{
"fen": "string (required) - FEN notation of the position to analyze",
"depth": 18, (optional, int 1-30, default 18) - search depth
"lines": 3 (optional, int 1-5, default 3) - number of candidate moves to return
}
Response (200 OK):
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
"depth": 18,
"lines": [
{
"rank": 1,
"move": "e7e5", // best move in UCI notation
"evaluation": -0.25, // eval from engine perspective (positive = side to move is better)
"depth": 18,
"pv": "e7e5 g1f3 b8c6 ..." // full principal variation
},
{
"rank": 2,
"move": "c7c5",
"evaluation": -0.15,
"depth": 18,
"pv": "c7c5 g1f3 d7d6 ..."
}
]
}
Errors:
400: {"error": "invalid request body"}
400: {"error": "fen is required"}
500: {"error": "engine error: ..."}
Timeout: 30 seconds max per analysis request.
Example:
curl -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/analyze \
-H "Content-Type: application/json" \
-d '{"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", "depth": 20, "lines": 3}'
--------------------------------------------------------------------------------
3. LIST BOTS
--------------------------------------------------------------------------------
GET /api/chess/bots
Description:
Returns all available bot personalities sorted by difficulty (easiest first).
Response (200 OK):
{
"bots": [
{
"id": "amina",
"name": "Amina",
"name_ar": "أمينة المبتدئة",
"style": "beginner",
"bio": "Just learning chess! Makes mistakes but tries her best.",
"bio_ar": "لسه بتتعلم شطرنج! بتغلط كتير بس بتحاول.",
"elo_min": 400,
"elo_max": 600,
"skill_level": 1,
"depth": 3,
"contempt": 0,
"blunder_chance": 0.30,
"think_time_min_ms": 500,
"think_time_max_ms": 2000,
"opening_book": [],
"avatar_id": "bot-amina"
},
...
]
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/api/chess/bots
--------------------------------------------------------------------------------
4. POOL STATS
--------------------------------------------------------------------------------
GET /api/chess/stats
Description:
Returns the current Stockfish process pool status.
Response (200 OK):
{
"pool_alive": 6, // number of Stockfish processes currently running
"pool_idle": 4 // number of those processes currently idle (available)
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/api/chess/stats
--------------------------------------------------------------------------------
5. HEALTH CHECK
--------------------------------------------------------------------------------
GET /health
Description:
Verifies the engine is operational by running a depth-1 search on the
starting position. Used by CapRover/Docker health checks.
Response (200 OK):
{
"status": "healthy",
"engine": "stockfish-18",
"pool_alive": 6,
"pool_idle": 4
}
Response (503 Service Unavailable):
{
"status": "unhealthy",
"error": "acquire process: context deadline exceeded"
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/health
================================================================================
AVAILABLE BOTS (sorted by difficulty)
================================================================================
ID | Name | Arabic Name | Style | ELO | Skill | Depth | Blunder% | Think Time (ms)
--------------|-----------------|------------------|--------------|-----------|-------|-------|----------|----------------
amina | Amina | أمينة المبتدئة | beginner | 400-600 | 1 | 3 | 30% | 500-2000
tarek | Tarek | طارق المتحفظ | defensive | 800-1000 | 5 | 6 | 15% | 1000-3000
nour | Nour | نور المهاجمة | aggressive | 1000-1200 | 8 | 10 | 8% | 800-3000
omar | Omar | عمر الاستراتيجي | positional | 1200-1400 | 11 | 12 | 4% | 1500-4000
layla | Layla | ليلى المبدعة | creative | 1400-1600 | 14 | 14 | 2% | 1000-5000
ziad | Ziad | زياد الصلب | solid | 1600-1800 | 17 | 16 | 1% | 2000-6000
grandmaster | Grandmaster Bot | الجراند ماستر | near_perfect | 2000-2200 | 20 | 20 | 0% | 3000-8000
================================================================================
BOT BEHAVIOR DETAILS
================================================================================
BLUNDER MECHANISM:
Each move request, the bot rolls against its blunder_chance probability.
If it "blunders", the engine searches at depth=1 with skill_level=0,
producing a weak/random move. Otherwise it plays at its configured strength.
THINK TIME SIMULATION:
After the engine returns a move, the API adds artificial delay to simulate
human-like thinking. The delay is random between think_time_min and think_time_max.
Total response time = engine_time + artificial_delay.
If the client disconnects (context cancelled), the delay is aborted.
CONTEMPT:
Positive contempt = bot plays more aggressively, avoids draws.
Negative contempt = bot is happy to draw, plays defensively.
Range: -100 to 100.
SKILL LEVEL:
Stockfish's internal skill parameter (0-20).
0 = weakest, introduces random errors.
20 = full strength, no artificial weakening.
OPENING BOOKS (metadata only, not enforced by engine):
Listed per bot for frontend display. The engine does not use opening books;
it calculates from the given FEN position directly.
AVATAR IDs:
Each bot has an avatar_id field for frontend use (e.g. "bot-amina", "bot-nour").
Map these to your avatar image assets.
================================================================================
MOVE NOTATION
================================================================================
All moves use UCI (Universal Chess Interface) long algebraic notation:
- Normal move: source_square + destination_square (e.g. "e2e4", "g1f3")
- Pawn promotion: source + destination + piece_letter (e.g. "e7e8q" for queen promotion)
- Castling: king's start + king's end (e.g. "e1g1" for white kingside, "e1c1" for queenside)
- En passant: normal pawn capture notation (e.g. "e5d6")
Piece letters for promotion: q=queen, r=rook, b=bishop, n=knight
================================================================================
FEN (Forsyth-Edwards Notation) FORMAT
================================================================================
A FEN string describes a complete board position in a single line:
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
| | | | | |
| | | | | +-- fullmove number
| | | | +----- halfmove clock (50-move rule)
| | | +---------- en passant target square ("-" if none)
| | +----------------- castling availability (KQkq or "-")
| +--------------------------- active color: "w" or "b"
+------------------------------------- piece placement (rank 8 to rank 1, "/" separated)
Piece letters: K=king, Q=queen, R=rook, B=bishop, N=knight, P=pawn
Uppercase = white, lowercase = black
Numbers = consecutive empty squares
Starting position FEN:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
================================================================================
EVALUATION VALUES
================================================================================
The "evaluation" field in responses:
- Measured in pawns (centipawns / 100)
- Positive = white advantage
- Negative = black advantage
- +0.50 means white is half a pawn ahead
- +999.0 = white has forced checkmate
- -999.0 = black has forced checkmate
- Values near 0.0 = roughly equal position
================================================================================
RATE LIMITING
================================================================================
- 60 requests per minute per IP address
- Applies to all endpoints
- X-Forwarded-For header is respected (for reverse proxy setups)
- When exceeded: 429 Too Many Requests {"error": "rate limit exceeded"}
================================================================================
CORS
================================================================================
- Access-Control-Allow-Origin: * (all origins allowed)
- Allowed Methods: GET, POST, OPTIONS
- Allowed Headers: Content-Type, Authorization, apikey
- Preflight cache: 86400 seconds (24 hours)
================================================================================
ADMIN PANEL
================================================================================
URL: https://stockfishapi.caprover.al-arcade.com/admin
Login: username "admin", password "Alarcade123#"
Features:
- Dashboard: live pool stats, bot count, request metrics
- Bot management: create, edit, delete bots at runtime
- Pool monitoring: alive/idle process counts
- Test Move: test any bot with a custom FEN position
- Request Logs: last 100 requests with method, path, status, duration, IP
- Settings: view/modify port, pool size, idle timeout
Note: Runtime changes (bot edits, new bots) persist only until container restart.
Core bot definitions are compiled into the binary.
================================================================================
INFRASTRUCTURE
================================================================================
- Engine: Stockfish 18 (compiled from source, NNUE enabled)
- Runtime: Go 1.22, single static binary
- Process Pool: 12 Stockfish processes (configurable via POOL_SIZE env)
- Pre-warmed: half the pool at startup for instant first requests
- Idle Reaper: kills processes unused for 300s (configurable via IDLE_TIMEOUT_SEC)
- Platform: Docker on CapRover (Ubuntu 22.04 base)
- Architecture: x86-64-sse41-popcnt (broad server CPU compatibility)
- Health Check: every 30s, 10s timeout, 3 retries, 15s start period
- Port: 80 inside container (CapRover handles HTTPS/reverse proxy)
================================================================================
ENVIRONMENT VARIABLES
================================================================================
PORT=80 # HTTP listen port
STOCKFISH_PATH=/usr/local/bin/stockfish # path to Stockfish binary
POOL_SIZE=12 # max concurrent Stockfish processes
IDLE_TIMEOUT_SEC=300 # kill idle processes after this many seconds
================================================================================
INTEGRATION EXAMPLES
================================================================================
--- JavaScript/Fetch ---
// Get a move from "nour" bot
const response = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/move', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
bot_id: 'nour'
})
});
const data = await response.json();
console.log(data.best_move); // e.g. "e7e5"
// Analyze a position
const analysis = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
depth: 20,
lines: 3
})
});
const result = await analysis.json();
result.lines.forEach(line => {
console.log(`#${line.rank}: ${line.move} (eval: ${line.evaluation})`);
});
// List all bots
const botsResp = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/bots');
const botsData = await botsResp.json();
botsData.bots.forEach(bot => {
console.log(`${bot.name} (${bot.elo_min}-${bot.elo_max} ELO) - ${bot.style}`);
});
--- Python ---
import requests
# Get move
resp = requests.post('https://stockfishapi.caprover.al-arcade.com/api/chess/move', json={
'fen': 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
'bot_id': 'omar'
})
move = resp.json()
print(f"Best move: {move['best_move']}, Eval: {move['evaluation']}")
# Analyze
resp = requests.post('https://stockfishapi.caprover.al-arcade.com/api/chess/analyze', json={
'fen': 'r1bqkbnr/pppppppp/2n5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2',
'depth': 22,
'lines': 5
})
for line in resp.json()['lines']:
print(f"#{line['rank']}: {line['move']} (eval {line['evaluation']:.2f})")
--- cURL ---
# Quick move
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/move \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","bot_id":"amina"}' | jq .
# Health check
curl -s https://stockfishapi.caprover.al-arcade.com/health | jq .
================================================================================
TYPICAL GAME FLOW (for client integration)
================================================================================
1. GET /api/chess/bots -> show bot selection to user
2. User picks a bot (e.g. "nour") and starts a game
3. Client tracks FEN locally (or uses a chess library)
4. When it's the bot's turn:
POST /api/chess/move with current FEN and bot_id
5. Apply the returned best_move to the board
6. Repeat from step 4 until checkmate/stalemate/draw
7. Optionally: POST /api/chess/analyze for post-game analysis
Important notes for client implementation:
- Always send the FULL FEN including castling rights, en passant, move counters
- The API does NOT validate if the FEN is a legal position
- The API does NOT track game state - it's stateless (one move per request)
- Handle the artificial think_time_ms for UX (show "thinking..." animation)
- Handle 429 rate limit gracefully (retry after 1 second)
- The response time can be up to 8000ms+ for grandmaster bot (due to simulated think time)
================================================================================
ERROR HANDLING
================================================================================
All errors return JSON with an "error" field:
400 Bad Request - malformed JSON, missing required fields
404 Not Found - invalid bot_id
429 Too Many Requests - rate limit exceeded (60/min/IP)
500 Internal Server Error - Stockfish process crash, pool exhaustion, timeout
503 Service Unavailable - health check failed (engine not responding)
Recommended retry strategy:
- 429: wait 1-2 seconds, retry
- 500: wait 2-5 seconds, retry up to 3 times
- 503: service is down, alert user
================================================================================
RESPONSE TIME EXPECTATIONS
================================================================================
Bot | Typical Response Time (includes simulated think time)
----------------|------------------------------------------------------
amina | 500ms - 2000ms
tarek | 1000ms - 3000ms
nour | 800ms - 3000ms
omar | 1500ms - 4000ms
layla | 1000ms - 5000ms
ziad | 2000ms - 6000ms
grandmaster | 3000ms - 8000ms
Analysis endpoint: 1-30 seconds depending on depth (no artificial delay).
Health check: < 5 seconds.
Set client timeouts accordingly (recommend 15s for moves, 35s for analysis).
STOCKFISH_API_REFERENCE.txt
0 → 100644
View file @
37a67945
================================================================================
STOCKFISH CHESS BOT API - COMPLETE REFERENCE
================================================================================
BASE URL: https://stockfishapi.caprover.al-arcade.com
================================================================================
ENDPOINTS
================================================================================
--------------------------------------------------------------------------------
1. GET MOVE FROM BOT
--------------------------------------------------------------------------------
POST /api/chess/move
Description:
Get a chess move from a specific bot personality. The bot will play according
to its configured skill level, depth, contempt, and blunder probability.
Response includes simulated human-like think time.
Headers:
Content-Type: application/json
Request Body:
{
"fen": "string (required) - FEN notation of the current board position",
"bot_id": "string (required) - ID of the bot to play against",
"time_limit_ms": 0 (optional, int) - time limit in ms. If 0 or omitted, uses depth-based search
}
Response (200 OK):
{
"best_move": "e2e4", // UCI move notation (from-square + to-square, e.g. e2e4, g1f3, e7e8q for promotion)
"evaluation": 0.35, // centipawn evaluation / 100. Positive = white advantage. 999.0 = white mates, -999.0 = black mates
"depth": 10, // search depth reached
"nodes": 125000, // nodes searched
"think_time_ms": 1500, // total time including artificial delay (simulates human thinking)
"pv": "e2e4 e7e5 g1f3" // principal variation (best line), space-separated UCI moves
}
Errors:
400: {"error": "invalid request body"}
400: {"error": "fen is required"}
400: {"error": "bot_id is required"}
404: {"error": "bot not found"}
500: {"error": "engine error: ..."}
Example:
curl -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/move \
-H "Content-Type: application/json" \
-d '{"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "bot_id": "nour"}'
--------------------------------------------------------------------------------
2. ANALYZE POSITION
--------------------------------------------------------------------------------
POST /api/chess/analyze
Description:
Deep multi-line analysis of a position at full Stockfish strength (skill 20).
Returns multiple candidate moves ranked by evaluation.
Headers:
Content-Type: application/json
Request Body:
{
"fen": "string (required) - FEN notation of the position to analyze",
"depth": 18, (optional, int 1-30, default 18) - search depth
"lines": 3 (optional, int 1-5, default 3) - number of candidate moves to return
}
Response (200 OK):
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
"depth": 18,
"lines": [
{
"rank": 1,
"move": "e7e5", // best move in UCI notation
"evaluation": -0.25, // eval from engine perspective (positive = side to move is better)
"depth": 18,
"pv": "e7e5 g1f3 b8c6 ..." // full principal variation
},
{
"rank": 2,
"move": "c7c5",
"evaluation": -0.15,
"depth": 18,
"pv": "c7c5 g1f3 d7d6 ..."
}
]
}
Errors:
400: {"error": "invalid request body"}
400: {"error": "fen is required"}
500: {"error": "engine error: ..."}
Timeout: 30 seconds max per analysis request.
Example:
curl -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/analyze \
-H "Content-Type: application/json" \
-d '{"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", "depth": 20, "lines": 3}'
--------------------------------------------------------------------------------
3. LIST BOTS
--------------------------------------------------------------------------------
GET /api/chess/bots
Description:
Returns all available bot personalities sorted by difficulty (easiest first).
Response (200 OK):
{
"bots": [
{
"id": "amina",
"name": "Amina",
"name_ar": "أمينة المبتدئة",
"style": "beginner",
"style_ar": "مبتدئة",
"bio": "Just learning chess! Makes mistakes but tries her best.",
"bio_ar": "لسه بتتعلم شطرنج! بتغلط كتير بس بتحاول.",
"elo_min": 400,
"elo_max": 600,
"skill_level": 1,
"depth": 3,
"contempt": 0,
"blunder_chance": 0.30,
"think_time_min_ms": 500,
"think_time_max_ms": 2000,
"opening_book": [],
"avatar_id": "bot-amina",
"portrait_url": "/portraits/amina.png"
},
...
]
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/api/chess/bots
--------------------------------------------------------------------------------
4. POOL STATS
--------------------------------------------------------------------------------
GET /api/chess/stats
Description:
Returns the current Stockfish process pool status.
Response (200 OK):
{
"pool_alive": 6, // number of Stockfish processes currently running
"pool_idle": 4 // number of those processes currently idle (available)
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/api/chess/stats
--------------------------------------------------------------------------------
5. HEALTH CHECK
--------------------------------------------------------------------------------
GET /health
Description:
Verifies the engine is operational by running a depth-1 search on the
starting position. Used by CapRover/Docker health checks.
Response (200 OK):
{
"status": "healthy",
"engine": "stockfish-18",
"pool_alive": 6,
"pool_idle": 4
}
Response (503 Service Unavailable):
{
"status": "unhealthy",
"error": "acquire process: context deadline exceeded"
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/health
================================================================================
AVAILABLE BOTS (sorted by difficulty)
================================================================================
ID | Name | Arabic Name | Style | Style (AR) | ELO | Skill | Depth | Blunder% | Think Time (ms) | Portrait
--------------|-----------------|------------------|--------------|-------------|-----------|-------|-------|----------|-----------------|------------------
amina | Amina | أمينة المبتدئة | beginner | مبتدئة | 400-600 | 1 | 3 | 30% | 500-2000 | /portraits/amina.png
tarek | Tarek | طارق المتحفظ | defensive | دفاعي | 800-1000 | 5 | 6 | 15% | 1000-3000 | /portraits/tarek.png
nour | Nour | نور المهاجمة | aggressive | هجومية | 1000-1200 | 8 | 10 | 8% | 800-3000 | /portraits/nour.png
omar | Omar | عمر الاستراتيجي | positional | استراتيجي | 1200-1400 | 11 | 12 | 4% | 1500-4000 | /portraits/omar.png
layla | Layla | ليلى المبدعة | creative | إبداعية | 1400-1600 | 14 | 14 | 2% | 1000-5000 | /portraits/layla.png
ziad | Ziad | زياد الصلب | solid | صلب | 1600-1800 | 17 | 16 | 1% | 2000-6000 | /portraits/ziad.png
grandmaster | Grandmaster Bot | الجراند ماستر | near_perfect | شبه مثالي | 2000-2200 | 20 | 20 | 0% | 3000-8000 | /portraits/grandmaster.png
================================================================================
BOT BEHAVIOR DETAILS
================================================================================
BLUNDER MECHANISM:
Each move request, the bot rolls against its blunder_chance probability.
If it "blunders", the engine searches at depth=1 with skill_level=0,
producing a weak/random move. Otherwise it plays at its configured strength.
THINK TIME SIMULATION:
After the engine returns a move, the API adds artificial delay to simulate
human-like thinking. The delay is random between think_time_min and think_time_max.
Total response time = engine_time + artificial_delay.
If the client disconnects (context cancelled), the delay is aborted.
CONTEMPT:
Positive contempt = bot plays more aggressively, avoids draws.
Negative contempt = bot is happy to draw, plays defensively.
Range: -100 to 100.
SKILL LEVEL:
Stockfish's internal skill parameter (0-20).
0 = weakest, introduces random errors.
20 = full strength, no artificial weakening.
OPENING BOOKS (metadata only, not enforced by engine):
Listed per bot for frontend display. The engine does not use opening books;
it calculates from the given FEN position directly.
AVATAR IDs:
Each bot has an avatar_id field for frontend use (e.g. "bot-amina", "bot-nour").
Map these to your avatar image assets.
PORTRAITS:
Each bot has a portrait_url field pointing to a 512x512 pixel image.
Portraits are served publicly at: https://stockfishapi.caprover.al-arcade.com/portraits/{bot_id}.png
Supported formats: PNG, JPG, WebP.
Upload via admin panel: /admin/bots/edit/{id} -> Portrait upload form.
Recommended dimensions: 512x512 pixels (square).
These are character portraits/avatars for display in the chess UI.
STYLE LABELS (Arabic):
Each bot has a "style_ar" field with the Arabic translation of its play style.
Use this for bilingual (EN/AR) UI display.
Examples: "مبتدئة" (beginner), "دفاعي" (defensive), "هجومية" (aggressive),
"استراتيجي" (positional), "إبداعية" (creative), "صلب" (solid),
"شبه مثالي" (near_perfect).
================================================================================
MOVE NOTATION
================================================================================
All moves use UCI (Universal Chess Interface) long algebraic notation:
- Normal move: source_square + destination_square (e.g. "e2e4", "g1f3")
- Pawn promotion: source + destination + piece_letter (e.g. "e7e8q" for queen promotion)
- Castling: king's start + king's end (e.g. "e1g1" for white kingside, "e1c1" for queenside)
- En passant: normal pawn capture notation (e.g. "e5d6")
Piece letters for promotion: q=queen, r=rook, b=bishop, n=knight
================================================================================
FEN (Forsyth-Edwards Notation) FORMAT
================================================================================
A FEN string describes a complete board position in a single line:
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
| | | | | |
| | | | | +-- fullmove number
| | | | +----- halfmove clock (50-move rule)
| | | +---------- en passant target square ("-" if none)
| | +----------------- castling availability (KQkq or "-")
| +--------------------------- active color: "w" or "b"
+------------------------------------- piece placement (rank 8 to rank 1, "/" separated)
Piece letters: K=king, Q=queen, R=rook, B=bishop, N=knight, P=pawn
Uppercase = white, lowercase = black
Numbers = consecutive empty squares
Starting position FEN:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
================================================================================
EVALUATION VALUES
================================================================================
The "evaluation" field in responses:
- Measured in pawns (centipawns / 100)
- Positive = white advantage
- Negative = black advantage
- +0.50 means white is half a pawn ahead
- +999.0 = white has forced checkmate
- -999.0 = black has forced checkmate
- Values near 0.0 = roughly equal position
================================================================================
RATE LIMITING
================================================================================
- 60 requests per minute per IP address
- Applies to all endpoints
- X-Forwarded-For header is respected (for reverse proxy setups)
- When exceeded: 429 Too Many Requests {"error": "rate limit exceeded"}
================================================================================
CORS
================================================================================
- Access-Control-Allow-Origin: * (all origins allowed)
- Allowed Methods: GET, POST, OPTIONS
- Allowed Headers: Content-Type, Authorization, apikey
- Preflight cache: 86400 seconds (24 hours)
================================================================================
MANAGEMENT API (Full Control)
================================================================================
Base: https://stockfishapi.caprover.al-arcade.com/api/manage
Authentication:
All /api/manage/* endpoints require an API key via header:
X-API-Key: sk-alarc-stockfish-mgmt-2024
OR:
Authorization: Bearer sk-alarc-stockfish-mgmt-2024
Rate Limiting: None on management endpoints (auth-gated).
--------------------------------------------------------------------------------
SYSTEM INFO
--------------------------------------------------------------------------------
GET /api/manage/info
Returns full system overview + all available endpoint list.
Response:
{
"engine": "stockfish-18",
"version": "1.0.0",
"bot_count": 7,
"pool": { "alive": 6, "idle": 4, "max_size": 12, "idle_timeout": 300 },
"settings": { "port": "80", "stockfish_path": "/usr/local/bin/stockfish" },
"endpoints": { ... all endpoints listed ... }
}
--------------------------------------------------------------------------------
BOT CRUD
--------------------------------------------------------------------------------
GET /api/manage/bots
List all bots with count.
GET /api/manage/bots/{id}
Get a single bot by ID.
POST /api/manage/bots
Create a new bot. Send full bot JSON in body.
Required: id, name, style
Returns 201 on success, 409 if ID already exists.
Body:
{
"id": "yasmin",
"name": "Yasmin",
"name_ar": "ياسمين",
"style": "tricky",
"style_ar": "ماكرة",
"bio": "Sets traps and waits for you to fall in.",
"bio_ar": "بتحط فخاخ وبتستنى تقع فيها.",
"elo_min": 1100,
"elo_max": 1300,
"skill_level": 9,
"depth": 11,
"contempt": 20,
"blunder_chance": 0.06,
"think_time_min_ms": 1000,
"think_time_max_ms": 3500,
"opening_book": ["sicilian", "french"],
"avatar_id": "bot-yasmin",
"portrait_url": "/portraits/yasmin.png"
}
PATCH /api/manage/bots/{id}
Partial update. Only send fields you want to change.
Body: { "skill_level": 12, "blunder_chance": 0.05, "style_ar": "ذكية" }
PUT /api/manage/bots/{id}
Full replace. Overwrites entire bot with new data.
Body: same as POST but ID comes from URL.
DELETE /api/manage/bots/{id}
Delete a bot and its portrait files.
--------------------------------------------------------------------------------
BULK OPERATIONS
--------------------------------------------------------------------------------
POST /api/manage/bots/bulk
Create multiple bots at once.
Body: [ {bot1}, {bot2}, ... ]
Response: { "created": ["id1","id2"], "skipped": ["id3 (already exists)"] }
DELETE /api/manage/bots/bulk
Delete multiple bots at once.
Body: { "ids": ["amina", "tarek"] }
Response: { "deleted": ["amina","tarek"], "not_found": [] }
GET /api/manage/bots/export
Download all bots as JSON file (Content-Disposition: attachment).
POST /api/manage/bots/import?overwrite=true
Import bots from JSON array. With overwrite=true, existing bots are replaced.
Body: [ {bot1}, {bot2}, ... ]
Response: { "imported": ["id1"], "skipped": ["id2 (exists)"], "total": 8 }
--------------------------------------------------------------------------------
PORTRAIT MANAGEMENT
--------------------------------------------------------------------------------
POST /api/manage/bots/{id}/portrait
Upload a portrait image (512x512 px).
Content-Type: multipart/form-data
Field name: "portrait"
Accepted: .png, .jpg, .jpeg, .webp (max 10MB)
curl example:
curl -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina/portrait \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-F "portrait=@amina_512x512.png"
Response: { "status": "uploaded", "bot_id": "amina", "portrait_url": "/portraits/amina.png", "size_bytes": 245000 }
DELETE /api/manage/bots/{id}/portrait
Delete all portrait files for a bot.
Response: { "status": "deleted", "bot_id": "amina" }
Portraits are served publicly (no auth):
GET https://stockfishapi.caprover.al-arcade.com/portraits/{bot_id}.png
--------------------------------------------------------------------------------
ENGINE / POOL
--------------------------------------------------------------------------------
GET /api/manage/pool
Full pool stats with utilization percentage.
Response:
{
"pool_alive": 6,
"pool_idle": 4,
"pool_max_size": 12,
"pool_utilization": "16.7%",
"idle_timeout_sec": 300
}
POST /api/manage/test-move
Test the engine directly. Supports raw mode (custom params) or bot mode.
Bot mode:
{ "fen": "...", "bot_id": "nour" }
Raw mode (bypass bot config, use custom engine params):
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
"raw_mode": true,
"depth": 25,
"skill_level": 20,
"contempt": 0,
"time_limit_ms": 5000,
"multi_pv": 3
}
Response:
{
"best_move": "e7e5",
"evaluation": -0.12,
"depth": 25,
"nodes": 5000000,
"pv": "e7e5 g1f3 b8c6",
"engine_time_ms": 3200,
"request": { "fen": "...", "depth": 25, "skill_level": 20, "contempt": 0, "multi_pv": 3 }
}
POST /api/manage/analyze
Deep analysis (up to depth 40, up to 10 lines).
{ "fen": "...", "depth": 30, "lines": 5 }
Response: { "fen": "...", "depth": 30, "lines": [...], "engine_time_ms": 12000 }
--------------------------------------------------------------------------------
SETTINGS
--------------------------------------------------------------------------------
GET /api/manage/settings
{ "port": "80", "pool_size": 12, "idle_timeout_sec": 300, "stockfish_path": "/usr/local/bin/stockfish" }
PATCH /api/manage/settings
Update settings (partial). Some require restart.
Body: { "pool_size": 16, "idle_timeout_sec": 600 }
Response: { "status": "updated", "settings": {...}, "note": "some changes require restart" }
--------------------------------------------------------------------------------
LOGS
--------------------------------------------------------------------------------
GET /api/manage/logs?limit=50
Get recent request logs (max 100).
Response:
{
"logs": [
{ "Timestamp": "2024-01-15 14:30:22", "Method": "POST", "Path": "/api/chess/move", "Status": 200, "Duration": "45ms", "IP": "1.2.3.4" },
...
],
"count": 50
}
================================================================================
MANAGEMENT API - QUICK REFERENCE (curl examples)
================================================================================
# Auth header (use in all requests)
AUTH="-H 'X-API-Key: sk-alarc-stockfish-mgmt-2024'"
# System info
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/info \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# List bots
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/bots \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Get single bot
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/bots/nour \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Create bot
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/bots \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"id":"yasmin","name":"Yasmin","name_ar":"ياسمين","style":"tricky","style_ar":"ماكرة","skill_level":9,"depth":11,"contempt":20,"blunder_chance":0.06,"think_time_min_ms":1000,"think_time_max_ms":3500,"elo_min":1100,"elo_max":1300}' | jq .
# Update bot (partial)
curl -s -X PATCH https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"skill_level":2,"blunder_chance":0.25}' | jq .
# Replace bot (full)
curl -s -X PUT https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"name":"Amina","name_ar":"أمينة","style":"beginner","style_ar":"مبتدئة","skill_level":1,"depth":3}' | jq .
# Delete bot
curl -s -X DELETE https://stockfishapi.caprover.al-arcade.com/api/manage/bots/yasmin \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Bulk create
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/bots/bulk \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '[{"id":"bot1","name":"Bot One","style":"test","skill_level":5,"depth":5},{"id":"bot2","name":"Bot Two","style":"test","skill_level":10,"depth":10}]' | jq .
# Bulk delete
curl -s -X DELETE https://stockfishapi.caprover.al-arcade.com/api/manage/bots/bulk \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"ids":["bot1","bot2"]}' | jq .
# Export all bots (download)
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/bots/export \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" -o bots_backup.json
# Import bots (with overwrite)
curl -s -X POST "https://stockfishapi.caprover.al-arcade.com/api/manage/bots/import?overwrite=true" \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d @bots_backup.json | jq .
# Upload portrait
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina/portrait \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-F "portrait=@amina.png" | jq .
# Delete portrait
curl -s -X DELETE https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina/portrait \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Pool stats
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/pool \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Test move (bot mode)
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/test-move \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1","bot_id":"grandmaster"}' | jq .
# Test move (raw mode - custom engine params)
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/test-move \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1","raw_mode":true,"depth":30,"skill_level":20,"contempt":0}' | jq .
# Deep analysis
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/analyze \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1","depth":30,"lines":5}' | jq .
# Get settings
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/settings \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Update settings
curl -s -X PATCH https://stockfishapi.caprover.al-arcade.com/api/manage/settings \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"pool_size":16,"idle_timeout_sec":600}' | jq .
# View logs
curl -s "https://stockfishapi.caprover.al-arcade.com/api/manage/logs?limit=20" \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
================================================================================
ADMIN PANEL
================================================================================
URL: https://stockfishapi.caprover.al-arcade.com/admin
Login: username "admin", password "Alarcade123#"
Features:
- Dashboard: live pool stats, bot count, request metrics
- Bot management: create, edit, delete bots at runtime
- Pool monitoring: alive/idle process counts
- Test Move: test any bot with a custom FEN position
- Request Logs: last 100 requests with method, path, status, duration, IP
- Settings: view/modify port, pool size, idle timeout
Note: Runtime changes (bot edits, new bots) persist only until container restart.
Core bot definitions are compiled into the binary.
================================================================================
INFRASTRUCTURE
================================================================================
- Engine: Stockfish 18 (compiled from source, NNUE enabled)
- Runtime: Go 1.22, single static binary
- Process Pool: 12 Stockfish processes (configurable via POOL_SIZE env)
- Pre-warmed: half the pool at startup for instant first requests
- Idle Reaper: kills processes unused for 300s (configurable via IDLE_TIMEOUT_SEC)
- Platform: Docker on CapRover (Ubuntu 22.04 base)
- Architecture: x86-64-sse41-popcnt (broad server CPU compatibility)
- Health Check: every 30s, 10s timeout, 3 retries, 15s start period
- Port: 80 inside container (CapRover handles HTTPS/reverse proxy)
================================================================================
ENVIRONMENT VARIABLES
================================================================================
PORT=80 # HTTP listen port
STOCKFISH_PATH=/usr/local/bin/stockfish # path to Stockfish binary
POOL_SIZE=12 # max concurrent Stockfish processes
IDLE_TIMEOUT_SEC=300 # kill idle processes after this many seconds
================================================================================
INTEGRATION EXAMPLES
================================================================================
--- JavaScript/Fetch ---
// Get a move from "nour" bot
const response = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/move', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
bot_id: 'nour'
})
});
const data = await response.json();
console.log(data.best_move); // e.g. "e7e5"
// Analyze a position
const analysis = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
depth: 20,
lines: 3
})
});
const result = await analysis.json();
result.lines.forEach(line => {
console.log(`#${line.rank}: ${line.move} (eval: ${line.evaluation})`);
});
// List all bots
const botsResp = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/bots');
const botsData = await botsResp.json();
botsData.bots.forEach(bot => {
console.log(`${bot.name} (${bot.elo_min}-${bot.elo_max} ELO) - ${bot.style}`);
});
--- Python ---
import requests
# Get move
resp = requests.post('https://stockfishapi.caprover.al-arcade.com/api/chess/move', json={
'fen': 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
'bot_id': 'omar'
})
move = resp.json()
print(f"Best move: {move['best_move']}, Eval: {move['evaluation']}")
# Analyze
resp = requests.post('https://stockfishapi.caprover.al-arcade.com/api/chess/analyze', json={
'fen': 'r1bqkbnr/pppppppp/2n5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2',
'depth': 22,
'lines': 5
})
for line in resp.json()['lines']:
print(f"#{line['rank']}: {line['move']} (eval {line['evaluation']:.2f})")
--- cURL ---
# Quick move
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/move \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","bot_id":"amina"}' | jq .
# Health check
curl -s https://stockfishapi.caprover.al-arcade.com/health | jq .
================================================================================
TYPICAL GAME FLOW (for client integration)
================================================================================
1. GET /api/chess/bots -> show bot selection to user
2. User picks a bot (e.g. "nour") and starts a game
3. Client tracks FEN locally (or uses a chess library)
4. When it's the bot's turn:
POST /api/chess/move with current FEN and bot_id
5. Apply the returned best_move to the board
6. Repeat from step 4 until checkmate/stalemate/draw
7. Optionally: POST /api/chess/analyze for post-game analysis
Important notes for client implementation:
- Always send the FULL FEN including castling rights, en passant, move counters
- The API does NOT validate if the FEN is a legal position
- The API does NOT track game state - it's stateless (one move per request)
- Handle the artificial think_time_ms for UX (show "thinking..." animation)
- Handle 429 rate limit gracefully (retry after 1 second)
- The response time can be up to 8000ms+ for grandmaster bot (due to simulated think time)
================================================================================
ERROR HANDLING
================================================================================
All errors return JSON with an "error" field:
400 Bad Request - malformed JSON, missing required fields
404 Not Found - invalid bot_id
429 Too Many Requests - rate limit exceeded (60/min/IP)
500 Internal Server Error - Stockfish process crash, pool exhaustion, timeout
503 Service Unavailable - health check failed (engine not responding)
Recommended retry strategy:
- 429: wait 1-2 seconds, retry
- 500: wait 2-5 seconds, retry up to 3 times
- 503: service is down, alert user
================================================================================
RESPONSE TIME EXPECTATIONS
================================================================================
Bot | Typical Response Time (includes simulated think time)
----------------|------------------------------------------------------
amina | 500ms - 2000ms
tarek | 1000ms - 3000ms
nour | 800ms - 3000ms
omar | 1500ms - 4000ms
layla | 1000ms - 5000ms
ziad | 2000ms - 6000ms
grandmaster | 3000ms - 8000ms
Analysis endpoint: 1-30 seconds depending on depth (no artificial delay).
Health check: < 5 seconds.
Set client timeouts accordingly (recommend 15s for moves, 35s for analysis).
cmd/server/main.go
View file @
37a67945
...
...
@@ -41,6 +41,9 @@ func main() {
r
.
Get
(
"/api/chess/stats"
,
api
.
HandleStats
)
r
.
Get
(
"/health"
,
api
.
HandleHealth
)
// Management API
api
.
RegisterManagementRoutes
(
r
)
// Admin panel
admin
.
RegisterRoutes
(
r
)
...
...
internal/admin/admin.go
View file @
37a67945
...
...
@@ -5,7 +5,10 @@ import (
"encoding/hex"
"fmt"
"html/template"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
...
...
@@ -18,6 +21,8 @@ import (
"stockfish-api/web"
)
const
PortraitDir
=
"/app/portraits"
// Template functions
var
funcMap
=
template
.
FuncMap
{
"mul"
:
func
(
a
,
b
interface
{})
float64
{
...
...
@@ -181,7 +186,7 @@ func AddLog(method, path string, status int, duration time.Duration, ip string)
}
}
func
g
etRecentLogs
()
[]
LogEntry
{
func
G
etRecentLogs
()
[]
LogEntry
{
logMu
.
Lock
()
defer
logMu
.
Unlock
()
...
...
@@ -314,6 +319,24 @@ func HandleBotEdit(w http.ResponseWriter, r *http.Request) {
// POST - update bot parameters
r
.
ParseForm
()
if
v
:=
strings
.
TrimSpace
(
r
.
FormValue
(
"name"
));
v
!=
""
{
bot
.
Name
=
v
}
if
v
:=
r
.
FormValue
(
"name_ar"
);
v
!=
""
{
bot
.
NameAr
=
v
}
if
v
:=
strings
.
TrimSpace
(
r
.
FormValue
(
"style"
));
v
!=
""
{
bot
.
Style
=
v
}
if
v
:=
r
.
FormValue
(
"style_ar"
);
v
!=
""
{
bot
.
StyleAr
=
v
}
if
v
:=
r
.
FormValue
(
"bio"
);
v
!=
""
{
bot
.
Bio
=
v
}
if
v
:=
r
.
FormValue
(
"bio_ar"
);
v
!=
""
{
bot
.
BioAr
=
v
}
if
v
,
err
:=
strconv
.
Atoi
(
r
.
FormValue
(
"skill_level"
));
err
==
nil
{
bot
.
SkillLevel
=
v
}
...
...
@@ -378,9 +401,15 @@ func HandleBotCreate(w http.ResponseWriter, r *http.Request) {
}
bot
:=
&
bots
.
BotPersonality
{
ID
:
id
,
Name
:
name
,
Style
:
style
,
ID
:
id
,
Name
:
name
,
NameAr
:
strings
.
TrimSpace
(
r
.
FormValue
(
"name_ar"
)),
Style
:
style
,
StyleAr
:
strings
.
TrimSpace
(
r
.
FormValue
(
"style_ar"
)),
Bio
:
strings
.
TrimSpace
(
r
.
FormValue
(
"bio"
)),
BioAr
:
strings
.
TrimSpace
(
r
.
FormValue
(
"bio_ar"
)),
AvatarID
:
strings
.
TrimSpace
(
r
.
FormValue
(
"avatar_id"
)),
PortraitURL
:
"/portraits/"
+
id
+
".png"
,
}
if
v
,
err
:=
strconv
.
Atoi
(
r
.
FormValue
(
"skill_level"
));
err
==
nil
{
...
...
@@ -483,7 +512,7 @@ func HandleSettings(w http.ResponseWriter, r *http.Request) {
}
func
HandleLogs
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
logs
:=
g
etRecentLogs
()
logs
:=
G
etRecentLogs
()
renderPage
(
w
,
"logs.html"
,
map
[
string
]
interface
{}{
"Title"
:
"Logs"
,
"Logs"
:
logs
,
...
...
@@ -559,6 +588,59 @@ func HandleTestMove(w http.ResponseWriter, r *http.Request) {
})
}
func
HandlePortraitUpload
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
botID
:=
chi
.
URLParam
(
r
,
"id"
)
bot
:=
bots
.
GetBot
(
botID
)
if
bot
==
nil
{
http
.
Error
(
w
,
"Bot not found"
,
http
.
StatusNotFound
)
return
}
r
.
ParseMultipartForm
(
10
<<
20
)
// 10MB max
file
,
header
,
err
:=
r
.
FormFile
(
"portrait"
)
if
err
!=
nil
{
http
.
Error
(
w
,
"No file uploaded"
,
http
.
StatusBadRequest
)
return
}
defer
file
.
Close
()
ext
:=
strings
.
ToLower
(
filepath
.
Ext
(
header
.
Filename
))
if
ext
!=
".png"
&&
ext
!=
".jpg"
&&
ext
!=
".jpeg"
&&
ext
!=
".webp"
{
http
.
Error
(
w
,
"Only PNG, JPG, and WebP files are allowed"
,
http
.
StatusBadRequest
)
return
}
os
.
MkdirAll
(
PortraitDir
,
0755
)
filename
:=
botID
+
ext
destPath
:=
filepath
.
Join
(
PortraitDir
,
filename
)
dst
,
err
:=
os
.
Create
(
destPath
)
if
err
!=
nil
{
http
.
Error
(
w
,
"Failed to save file"
,
http
.
StatusInternalServerError
)
return
}
defer
dst
.
Close
()
io
.
Copy
(
dst
,
file
)
bot
.
PortraitURL
=
"/portraits/"
+
filename
http
.
Redirect
(
w
,
r
,
"/admin/bots/edit/"
+
botID
,
http
.
StatusFound
)
}
func
HandlePortraitServe
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
filename
:=
chi
.
URLParam
(
r
,
"*"
)
filePath
:=
filepath
.
Join
(
PortraitDir
,
filepath
.
Base
(
filename
))
if
_
,
err
:=
os
.
Stat
(
filePath
);
os
.
IsNotExist
(
err
)
{
http
.
Error
(
w
,
"Not found"
,
http
.
StatusNotFound
)
return
}
http
.
ServeFile
(
w
,
r
,
filePath
)
}
// --- Route Registration ---
// RegisterRoutes mounts all admin routes under the provided router.
...
...
@@ -566,6 +648,9 @@ func RegisterRoutes(r chi.Router) {
r
.
Get
(
"/admin/login"
,
HandleLogin
)
r
.
Post
(
"/admin/login"
,
HandleLogin
)
// Serve portraits publicly (no auth needed for API consumers)
r
.
Get
(
"/portraits/*"
,
HandlePortraitServe
)
r
.
Group
(
func
(
protected
chi
.
Router
)
{
protected
.
Use
(
RequireAuth
)
...
...
@@ -574,6 +659,7 @@ func RegisterRoutes(r chi.Router) {
protected
.
Get
(
"/admin/bots"
,
HandleBots
)
protected
.
Get
(
"/admin/bots/edit/{id}"
,
HandleBotEdit
)
protected
.
Post
(
"/admin/bots/edit/{id}"
,
HandleBotEdit
)
protected
.
Post
(
"/admin/bots/portrait/{id}"
,
HandlePortraitUpload
)
protected
.
Get
(
"/admin/bots/create"
,
HandleBotCreate
)
protected
.
Post
(
"/admin/bots/create"
,
HandleBotCreate
)
protected
.
Post
(
"/admin/bots/delete/{id}"
,
HandleBotDelete
)
...
...
internal/api/management.go
0 → 100644
View file @
37a67945
package
api
import
(
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"stockfish-api/internal/admin"
"stockfish-api/internal/bots"
"stockfish-api/internal/engine"
)
const
managementAPIKey
=
"sk-alarc-stockfish-mgmt-2024"
const
portraitDir
=
"/app/portraits"
func
ManagementAuth
(
next
http
.
Handler
)
http
.
Handler
{
return
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
key
:=
r
.
Header
.
Get
(
"X-API-Key"
)
if
key
==
""
{
key
=
r
.
Header
.
Get
(
"Authorization"
)
key
=
strings
.
TrimPrefix
(
key
,
"Bearer "
)
}
if
key
!=
managementAPIKey
{
writeJSON
(
w
,
http
.
StatusUnauthorized
,
map
[
string
]
string
{
"error"
:
"invalid or missing API key"
})
return
}
next
.
ServeHTTP
(
w
,
r
)
})
}
// --- Bot CRUD ---
func
HandleAPIListBots
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"bots"
:
bots
.
ListBots
(),
"count"
:
len
(
bots
.
Personalities
),
})
}
func
HandleAPIGetBot
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
id
:=
chi
.
URLParam
(
r
,
"id"
)
bot
:=
bots
.
GetBot
(
id
)
if
bot
==
nil
{
writeJSON
(
w
,
http
.
StatusNotFound
,
map
[
string
]
string
{
"error"
:
"bot not found"
})
return
}
writeJSON
(
w
,
http
.
StatusOK
,
bot
)
}
func
HandleAPICreateBot
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
var
bot
bots
.
BotPersonality
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
bot
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON: "
+
err
.
Error
()})
return
}
if
bot
.
ID
==
""
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"id is required"
})
return
}
if
bot
.
Name
==
""
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"name is required"
})
return
}
if
bot
.
Style
==
""
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"style is required"
})
return
}
if
bots
.
GetBot
(
bot
.
ID
)
!=
nil
{
writeJSON
(
w
,
http
.
StatusConflict
,
map
[
string
]
string
{
"error"
:
"bot already exists with id: "
+
bot
.
ID
})
return
}
if
bot
.
PortraitURL
==
""
{
bot
.
PortraitURL
=
"/portraits/"
+
bot
.
ID
+
".png"
}
bots
.
Personalities
[
bot
.
ID
]
=
&
bot
writeJSON
(
w
,
http
.
StatusCreated
,
bot
)
}
func
HandleAPIUpdateBot
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
id
:=
chi
.
URLParam
(
r
,
"id"
)
existing
:=
bots
.
GetBot
(
id
)
if
existing
==
nil
{
writeJSON
(
w
,
http
.
StatusNotFound
,
map
[
string
]
string
{
"error"
:
"bot not found"
})
return
}
var
updates
map
[
string
]
interface
{}
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
updates
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON: "
+
err
.
Error
()})
return
}
if
v
,
ok
:=
updates
[
"name"
]
.
(
string
);
ok
&&
v
!=
""
{
existing
.
Name
=
v
}
if
v
,
ok
:=
updates
[
"name_ar"
]
.
(
string
);
ok
{
existing
.
NameAr
=
v
}
if
v
,
ok
:=
updates
[
"style"
]
.
(
string
);
ok
&&
v
!=
""
{
existing
.
Style
=
v
}
if
v
,
ok
:=
updates
[
"style_ar"
]
.
(
string
);
ok
{
existing
.
StyleAr
=
v
}
if
v
,
ok
:=
updates
[
"bio"
]
.
(
string
);
ok
{
existing
.
Bio
=
v
}
if
v
,
ok
:=
updates
[
"bio_ar"
]
.
(
string
);
ok
{
existing
.
BioAr
=
v
}
if
v
,
ok
:=
updates
[
"elo_min"
]
.
(
float64
);
ok
{
existing
.
ELOMin
=
int
(
v
)
}
if
v
,
ok
:=
updates
[
"elo_max"
]
.
(
float64
);
ok
{
existing
.
ELOMax
=
int
(
v
)
}
if
v
,
ok
:=
updates
[
"skill_level"
]
.
(
float64
);
ok
{
existing
.
SkillLevel
=
int
(
v
)
}
if
v
,
ok
:=
updates
[
"depth"
]
.
(
float64
);
ok
{
existing
.
Depth
=
int
(
v
)
}
if
v
,
ok
:=
updates
[
"contempt"
]
.
(
float64
);
ok
{
existing
.
Contempt
=
int
(
v
)
}
if
v
,
ok
:=
updates
[
"blunder_chance"
]
.
(
float64
);
ok
{
existing
.
BlunderChance
=
v
}
if
v
,
ok
:=
updates
[
"think_time_min_ms"
]
.
(
float64
);
ok
{
existing
.
ThinkTimeMin
=
int
(
v
)
}
if
v
,
ok
:=
updates
[
"think_time_max_ms"
]
.
(
float64
);
ok
{
existing
.
ThinkTimeMax
=
int
(
v
)
}
if
v
,
ok
:=
updates
[
"avatar_id"
]
.
(
string
);
ok
{
existing
.
AvatarID
=
v
}
if
v
,
ok
:=
updates
[
"portrait_url"
]
.
(
string
);
ok
{
existing
.
PortraitURL
=
v
}
if
v
,
ok
:=
updates
[
"opening_book"
]
.
([]
interface
{});
ok
{
books
:=
make
([]
string
,
0
,
len
(
v
))
for
_
,
b
:=
range
v
{
if
s
,
ok
:=
b
.
(
string
);
ok
{
books
=
append
(
books
,
s
)
}
}
existing
.
OpeningBook
=
books
}
writeJSON
(
w
,
http
.
StatusOK
,
existing
)
}
func
HandleAPIReplaceBot
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
id
:=
chi
.
URLParam
(
r
,
"id"
)
if
bots
.
GetBot
(
id
)
==
nil
{
writeJSON
(
w
,
http
.
StatusNotFound
,
map
[
string
]
string
{
"error"
:
"bot not found"
})
return
}
var
bot
bots
.
BotPersonality
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
bot
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON: "
+
err
.
Error
()})
return
}
bot
.
ID
=
id
if
bot
.
PortraitURL
==
""
{
bot
.
PortraitURL
=
"/portraits/"
+
id
+
".png"
}
bots
.
Personalities
[
id
]
=
&
bot
writeJSON
(
w
,
http
.
StatusOK
,
bot
)
}
func
HandleAPIDeleteBot
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
id
:=
chi
.
URLParam
(
r
,
"id"
)
if
bots
.
GetBot
(
id
)
==
nil
{
writeJSON
(
w
,
http
.
StatusNotFound
,
map
[
string
]
string
{
"error"
:
"bot not found"
})
return
}
delete
(
bots
.
Personalities
,
id
)
portraitPath
:=
filepath
.
Join
(
portraitDir
,
id
+
".png"
)
os
.
Remove
(
portraitPath
)
portraitPath
=
filepath
.
Join
(
portraitDir
,
id
+
".jpg"
)
os
.
Remove
(
portraitPath
)
portraitPath
=
filepath
.
Join
(
portraitDir
,
id
+
".webp"
)
os
.
Remove
(
portraitPath
)
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
string
{
"status"
:
"deleted"
,
"id"
:
id
})
}
// --- Bot Bulk Operations ---
func
HandleAPIBulkCreateBots
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
var
botList
[]
bots
.
BotPersonality
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
botList
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON: "
+
err
.
Error
()})
return
}
created
:=
[]
string
{}
skipped
:=
[]
string
{}
for
i
:=
range
botList
{
bot
:=
&
botList
[
i
]
if
bot
.
ID
==
""
||
bot
.
Name
==
""
{
skipped
=
append
(
skipped
,
fmt
.
Sprintf
(
"index %d: missing id or name"
,
i
))
continue
}
if
bots
.
GetBot
(
bot
.
ID
)
!=
nil
{
skipped
=
append
(
skipped
,
bot
.
ID
+
" (already exists)"
)
continue
}
if
bot
.
PortraitURL
==
""
{
bot
.
PortraitURL
=
"/portraits/"
+
bot
.
ID
+
".png"
}
bots
.
Personalities
[
bot
.
ID
]
=
bot
created
=
append
(
created
,
bot
.
ID
)
}
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"created"
:
created
,
"skipped"
:
skipped
,
})
}
func
HandleAPIBulkDeleteBots
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
var
body
struct
{
IDs
[]
string
`json:"ids"`
}
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
body
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON: "
+
err
.
Error
()})
return
}
deleted
:=
[]
string
{}
notFound
:=
[]
string
{}
for
_
,
id
:=
range
body
.
IDs
{
if
bots
.
GetBot
(
id
)
==
nil
{
notFound
=
append
(
notFound
,
id
)
continue
}
delete
(
bots
.
Personalities
,
id
)
deleted
=
append
(
deleted
,
id
)
}
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"deleted"
:
deleted
,
"not_found"
:
notFound
,
})
}
func
HandleAPIExportBots
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
allBots
:=
bots
.
ListBots
()
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
w
.
Header
()
.
Set
(
"Content-Disposition"
,
"attachment; filename=bots_export.json"
)
json
.
NewEncoder
(
w
)
.
Encode
(
allBots
)
}
func
HandleAPIImportBots
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
overwrite
:=
r
.
URL
.
Query
()
.
Get
(
"overwrite"
)
==
"true"
var
botList
[]
bots
.
BotPersonality
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
botList
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON: "
+
err
.
Error
()})
return
}
imported
:=
[]
string
{}
skipped
:=
[]
string
{}
for
i
:=
range
botList
{
bot
:=
&
botList
[
i
]
if
bot
.
ID
==
""
{
skipped
=
append
(
skipped
,
fmt
.
Sprintf
(
"index %d: missing id"
,
i
))
continue
}
if
bots
.
GetBot
(
bot
.
ID
)
!=
nil
&&
!
overwrite
{
skipped
=
append
(
skipped
,
bot
.
ID
+
" (exists, overwrite=false)"
)
continue
}
if
bot
.
PortraitURL
==
""
{
bot
.
PortraitURL
=
"/portraits/"
+
bot
.
ID
+
".png"
}
bots
.
Personalities
[
bot
.
ID
]
=
bot
imported
=
append
(
imported
,
bot
.
ID
)
}
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"imported"
:
imported
,
"skipped"
:
skipped
,
"total"
:
len
(
bots
.
Personalities
),
})
}
// --- Portrait Management ---
func
HandleAPIUploadPortrait
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
id
:=
chi
.
URLParam
(
r
,
"id"
)
bot
:=
bots
.
GetBot
(
id
)
if
bot
==
nil
{
writeJSON
(
w
,
http
.
StatusNotFound
,
map
[
string
]
string
{
"error"
:
"bot not found"
})
return
}
r
.
ParseMultipartForm
(
10
<<
20
)
file
,
header
,
err
:=
r
.
FormFile
(
"portrait"
)
if
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"no file in 'portrait' field"
})
return
}
defer
file
.
Close
()
ext
:=
strings
.
ToLower
(
filepath
.
Ext
(
header
.
Filename
))
if
ext
!=
".png"
&&
ext
!=
".jpg"
&&
ext
!=
".jpeg"
&&
ext
!=
".webp"
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"only .png, .jpg, .webp allowed"
})
return
}
os
.
MkdirAll
(
portraitDir
,
0755
)
filename
:=
id
+
ext
destPath
:=
filepath
.
Join
(
portraitDir
,
filename
)
dst
,
err
:=
os
.
Create
(
destPath
)
if
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusInternalServerError
,
map
[
string
]
string
{
"error"
:
"failed to write file"
})
return
}
defer
dst
.
Close
()
io
.
Copy
(
dst
,
file
)
bot
.
PortraitURL
=
"/portraits/"
+
filename
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"status"
:
"uploaded"
,
"bot_id"
:
id
,
"portrait_url"
:
bot
.
PortraitURL
,
"size_bytes"
:
header
.
Size
,
})
}
func
HandleAPIDeletePortrait
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
id
:=
chi
.
URLParam
(
r
,
"id"
)
bot
:=
bots
.
GetBot
(
id
)
if
bot
==
nil
{
writeJSON
(
w
,
http
.
StatusNotFound
,
map
[
string
]
string
{
"error"
:
"bot not found"
})
return
}
for
_
,
ext
:=
range
[]
string
{
".png"
,
".jpg"
,
".jpeg"
,
".webp"
}
{
os
.
Remove
(
filepath
.
Join
(
portraitDir
,
id
+
ext
))
}
bot
.
PortraitURL
=
""
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
string
{
"status"
:
"deleted"
,
"bot_id"
:
id
})
}
// --- Engine / Pool ---
func
HandleAPIPoolStats
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
alive
,
idle
:=
engine
.
PoolStats
()
s
:=
admin
.
GetSettings
()
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"pool_alive"
:
alive
,
"pool_idle"
:
idle
,
"pool_max_size"
:
s
.
PoolSize
,
"pool_utilization"
:
fmt
.
Sprintf
(
"%.1f%%"
,
float64
(
alive
-
idle
)
/
float64
(
s
.
PoolSize
)
*
100
),
"idle_timeout_sec"
:
s
.
IdleTimeout
,
})
}
func
HandleAPITestMove
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
var
body
struct
{
FEN
string
`json:"fen"`
BotID
string
`json:"bot_id"`
Depth
int
`json:"depth"`
SkillLevel
int
`json:"skill_level"`
Contempt
int
`json:"contempt"`
TimeLimitMs
int
`json:"time_limit_ms"`
MultiPV
int
`json:"multi_pv"`
RawMode
bool
`json:"raw_mode"`
}
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
body
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON"
})
return
}
if
body
.
FEN
==
""
{
body
.
FEN
=
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
}
depth
:=
body
.
Depth
skill
:=
body
.
SkillLevel
contempt
:=
body
.
Contempt
multiPV
:=
body
.
MultiPV
if
body
.
BotID
!=
""
&&
!
body
.
RawMode
{
bot
:=
bots
.
GetBot
(
body
.
BotID
)
if
bot
==
nil
{
writeJSON
(
w
,
http
.
StatusNotFound
,
map
[
string
]
string
{
"error"
:
"bot not found"
})
return
}
depth
=
bot
.
Depth
skill
=
bot
.
SkillLevel
contempt
=
bot
.
Contempt
}
if
depth
<=
0
{
depth
=
15
}
if
multiPV
<=
0
{
multiPV
=
1
}
req
:=
engine
.
MoveRequest
{
FEN
:
body
.
FEN
,
Depth
:
depth
,
SkillLevel
:
skill
,
Contempt
:
contempt
,
TimeLimitMs
:
body
.
TimeLimitMs
,
MultiPV
:
multiPV
,
}
start
:=
time
.
Now
()
resp
,
err
:=
engine
.
GetMove
(
r
.
Context
(),
req
)
if
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusInternalServerError
,
map
[
string
]
string
{
"error"
:
"engine error: "
+
err
.
Error
()})
return
}
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"best_move"
:
resp
.
BestMove
,
"evaluation"
:
resp
.
Evaluation
,
"depth"
:
resp
.
Depth
,
"nodes"
:
resp
.
Nodes
,
"pv"
:
resp
.
PV
,
"engine_time_ms"
:
time
.
Since
(
start
)
.
Milliseconds
(),
"request"
:
map
[
string
]
interface
{}{
"fen"
:
body
.
FEN
,
"depth"
:
depth
,
"skill_level"
:
skill
,
"contempt"
:
contempt
,
"multi_pv"
:
multiPV
,
},
})
}
func
HandleAPIAnalyzePosition
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
var
body
struct
{
FEN
string
`json:"fen"`
Depth
int
`json:"depth"`
Lines
int
`json:"lines"`
}
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
body
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON"
})
return
}
if
body
.
FEN
==
""
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"fen is required"
})
return
}
if
body
.
Depth
<=
0
||
body
.
Depth
>
40
{
body
.
Depth
=
20
}
if
body
.
Lines
<=
0
||
body
.
Lines
>
10
{
body
.
Lines
=
5
}
start
:=
time
.
Now
()
lines
,
err
:=
engine
.
Analyze
(
r
.
Context
(),
body
.
FEN
,
body
.
Depth
,
body
.
Lines
)
if
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusInternalServerError
,
map
[
string
]
string
{
"error"
:
"engine error: "
+
err
.
Error
()})
return
}
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"fen"
:
body
.
FEN
,
"depth"
:
body
.
Depth
,
"lines"
:
lines
,
"engine_time_ms"
:
time
.
Since
(
start
)
.
Milliseconds
(),
})
}
// --- Settings ---
func
HandleAPIGetSettings
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
:=
admin
.
GetSettings
()
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"port"
:
s
.
Port
,
"pool_size"
:
s
.
PoolSize
,
"idle_timeout_sec"
:
s
.
IdleTimeout
,
"stockfish_path"
:
os
.
Getenv
(
"STOCKFISH_PATH"
),
})
}
func
HandleAPIUpdateSettings
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
var
body
struct
{
PoolSize
*
int
`json:"pool_size"`
IdleTimeout
*
int
`json:"idle_timeout_sec"`
Port
*
string
`json:"port"`
}
if
err
:=
json
.
NewDecoder
(
r
.
Body
)
.
Decode
(
&
body
);
err
!=
nil
{
writeJSON
(
w
,
http
.
StatusBadRequest
,
map
[
string
]
string
{
"error"
:
"invalid JSON"
})
return
}
s
:=
admin
.
GetSettings
()
if
body
.
PoolSize
!=
nil
{
s
.
PoolSize
=
*
body
.
PoolSize
}
if
body
.
IdleTimeout
!=
nil
{
s
.
IdleTimeout
=
*
body
.
IdleTimeout
}
if
body
.
Port
!=
nil
{
s
.
Port
=
*
body
.
Port
}
admin
.
SetSettings
(
s
)
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"status"
:
"updated"
,
"settings"
:
s
,
"note"
:
"some changes require restart to take effect"
,
})
}
// --- Logs ---
func
HandleAPIGetLogs
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
limitStr
:=
r
.
URL
.
Query
()
.
Get
(
"limit"
)
limit
:=
100
if
v
,
err
:=
strconv
.
Atoi
(
limitStr
);
err
==
nil
&&
v
>
0
&&
v
<=
100
{
limit
=
v
}
logs
:=
admin
.
GetRecentLogs
()
if
len
(
logs
)
>
limit
{
logs
=
logs
[
:
limit
]
}
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"logs"
:
logs
,
"count"
:
len
(
logs
),
})
}
// --- System Info ---
func
HandleAPISystemInfo
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
alive
,
idle
:=
engine
.
PoolStats
()
s
:=
admin
.
GetSettings
()
writeJSON
(
w
,
http
.
StatusOK
,
map
[
string
]
interface
{}{
"engine"
:
"stockfish-18"
,
"version"
:
"1.0.0"
,
"bot_count"
:
len
(
bots
.
Personalities
),
"pool"
:
map
[
string
]
interface
{}{
"alive"
:
alive
,
"idle"
:
idle
,
"max_size"
:
s
.
PoolSize
,
"idle_timeout"
:
s
.
IdleTimeout
,
},
"settings"
:
map
[
string
]
interface
{}{
"port"
:
s
.
Port
,
"stockfish_path"
:
os
.
Getenv
(
"STOCKFISH_PATH"
),
},
"endpoints"
:
map
[
string
]
string
{
"bots_list"
:
"GET /api/manage/bots"
,
"bot_get"
:
"GET /api/manage/bots/{id}"
,
"bot_create"
:
"POST /api/manage/bots"
,
"bot_update"
:
"PATCH /api/manage/bots/{id}"
,
"bot_replace"
:
"PUT /api/manage/bots/{id}"
,
"bot_delete"
:
"DELETE /api/manage/bots/{id}"
,
"bots_bulk_create"
:
"POST /api/manage/bots/bulk"
,
"bots_bulk_delete"
:
"DELETE /api/manage/bots/bulk"
,
"bots_export"
:
"GET /api/manage/bots/export"
,
"bots_import"
:
"POST /api/manage/bots/import?overwrite=true"
,
"portrait_upload"
:
"POST /api/manage/bots/{id}/portrait"
,
"portrait_delete"
:
"DELETE /api/manage/bots/{id}/portrait"
,
"pool_stats"
:
"GET /api/manage/pool"
,
"test_move"
:
"POST /api/manage/test-move"
,
"analyze"
:
"POST /api/manage/analyze"
,
"settings_get"
:
"GET /api/manage/settings"
,
"settings_update"
:
"PATCH /api/manage/settings"
,
"logs"
:
"GET /api/manage/logs?limit=50"
,
"system_info"
:
"GET /api/manage/info"
,
},
})
}
// --- Register Management Routes ---
func
RegisterManagementRoutes
(
r
chi
.
Router
)
{
r
.
Route
(
"/api/manage"
,
func
(
mgmt
chi
.
Router
)
{
mgmt
.
Use
(
ManagementAuth
)
// System
mgmt
.
Get
(
"/info"
,
HandleAPISystemInfo
)
// Bots CRUD
mgmt
.
Get
(
"/bots"
,
HandleAPIListBots
)
mgmt
.
Post
(
"/bots"
,
HandleAPICreateBot
)
mgmt
.
Get
(
"/bots/export"
,
HandleAPIExportBots
)
mgmt
.
Post
(
"/bots/import"
,
HandleAPIImportBots
)
mgmt
.
Post
(
"/bots/bulk"
,
HandleAPIBulkCreateBots
)
mgmt
.
Delete
(
"/bots/bulk"
,
HandleAPIBulkDeleteBots
)
mgmt
.
Get
(
"/bots/{id}"
,
HandleAPIGetBot
)
mgmt
.
Patch
(
"/bots/{id}"
,
HandleAPIUpdateBot
)
mgmt
.
Put
(
"/bots/{id}"
,
HandleAPIReplaceBot
)
mgmt
.
Delete
(
"/bots/{id}"
,
HandleAPIDeleteBot
)
// Portraits
mgmt
.
Post
(
"/bots/{id}/portrait"
,
HandleAPIUploadPortrait
)
mgmt
.
Delete
(
"/bots/{id}/portrait"
,
HandleAPIDeletePortrait
)
// Pool / Engine
mgmt
.
Get
(
"/pool"
,
HandleAPIPoolStats
)
mgmt
.
Post
(
"/test-move"
,
HandleAPITestMove
)
mgmt
.
Post
(
"/analyze"
,
HandleAPIAnalyzePosition
)
// Settings
mgmt
.
Get
(
"/settings"
,
HandleAPIGetSettings
)
mgmt
.
Patch
(
"/settings"
,
HandleAPIUpdateSettings
)
// Logs
mgmt
.
Get
(
"/logs"
,
HandleAPIGetLogs
)
})
}
internal/bots/personalities.go
View file @
37a67945
...
...
@@ -7,6 +7,7 @@ type BotPersonality struct {
Name
string
`json:"name"`
NameAr
string
`json:"name_ar"`
Style
string
`json:"style"`
StyleAr
string
`json:"style_ar"`
Bio
string
`json:"bio"`
BioAr
string
`json:"bio_ar"`
ELOMin
int
`json:"elo_min"`
...
...
@@ -19,6 +20,7 @@ type BotPersonality struct {
ThinkTimeMax
int
`json:"think_time_max_ms"`
OpeningBook
[]
string
`json:"opening_book"`
AvatarID
string
`json:"avatar_id"`
PortraitURL
string
`json:"portrait_url"`
}
var
Personalities
=
map
[
string
]
*
BotPersonality
{
...
...
@@ -27,6 +29,7 @@ var Personalities = map[string]*BotPersonality{
Name
:
"Amina"
,
NameAr
:
"أمينة المبتدئة"
,
Style
:
"beginner"
,
StyleAr
:
"مبتدئة"
,
Bio
:
"Just learning chess! Makes mistakes but tries her best."
,
BioAr
:
"لسه بتتعلم شطرنج! بتغلط كتير بس بتحاول."
,
ELOMin
:
400
,
...
...
@@ -39,12 +42,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax
:
2000
,
OpeningBook
:
[]
string
{},
AvatarID
:
"bot-amina"
,
PortraitURL
:
"/portraits/amina.png"
,
},
"tarek"
:
{
ID
:
"tarek"
,
Name
:
"Tarek"
,
NameAr
:
"طارق المتحفظ"
,
Style
:
"defensive"
,
StyleAr
:
"دفاعي"
,
Bio
:
"Plays it safe. Castles early, protects everything, rarely attacks."
,
BioAr
:
"بيلعب أمان. بيتبيّت بدري و بيحمي كل حاجة."
,
ELOMin
:
800
,
...
...
@@ -57,12 +62,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax
:
3000
,
OpeningBook
:
[]
string
{
"italian"
,
"queens_gambit_declined"
,
"caro_kann"
},
AvatarID
:
"bot-tarek"
,
PortraitURL
:
"/portraits/tarek.png"
,
},
"nour"
:
{
ID
:
"nour"
,
Name
:
"Nour"
,
NameAr
:
"نور المهاجمة"
,
Style
:
"aggressive"
,
StyleAr
:
"هجومية"
,
Bio
:
"Loves to attack! Sacrifices pieces for a shot at your king."
,
BioAr
:
"بتحب الهجوم! بتضحي بقطع عشان توصل للملك."
,
ELOMin
:
1000
,
...
...
@@ -75,12 +82,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax
:
3000
,
OpeningBook
:
[]
string
{
"kings_gambit"
,
"sicilian_dragon"
,
"evans_gambit"
},
AvatarID
:
"bot-nour"
,
PortraitURL
:
"/portraits/nour.png"
,
},
"omar"
:
{
ID
:
"omar"
,
Name
:
"Omar"
,
NameAr
:
"عمر الاستراتيجي"
,
Style
:
"positional"
,
StyleAr
:
"استراتيجي"
,
Bio
:
"Controls the center, improves pieces slowly, dominates the endgame."
,
BioAr
:
"بيسيطر على المركز، بيحسّن القطع ببطء، و بيكسب النهايات."
,
ELOMin
:
1200
,
...
...
@@ -93,12 +102,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax
:
4000
,
OpeningBook
:
[]
string
{
"queens_gambit"
,
"english"
,
"reti"
},
AvatarID
:
"bot-omar"
,
PortraitURL
:
"/portraits/omar.png"
,
},
"layla"
:
{
ID
:
"layla"
,
Name
:
"Layla"
,
NameAr
:
"ليلى المبدعة"
,
Style
:
"creative"
,
StyleAr
:
"إبداعية"
,
Bio
:
"Looks for tactics, pins, forks, and sacrifices. Unpredictable."
,
BioAr
:
"بتدور على التكتيكات والتضحيات. مش هتتوقع خطوتها."
,
ELOMin
:
1400
,
...
...
@@ -111,12 +122,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax
:
5000
,
OpeningBook
:
[]
string
{
"sicilian_najdorf"
,
"kings_indian"
,
"grunfeld"
},
AvatarID
:
"bot-layla"
,
PortraitURL
:
"/portraits/layla.png"
,
},
"ziad"
:
{
ID
:
"ziad"
,
Name
:
"Ziad"
,
NameAr
:
"زياد الصلب"
,
Style
:
"solid"
,
StyleAr
:
"صلب"
,
Bio
:
"No weaknesses. Plays principled chess. Hard to beat, rarely blunders."
,
BioAr
:
"مفيش نقط ضعف. بيلعب شطرنج محترم. صعب تكسبه."
,
ELOMin
:
1600
,
...
...
@@ -129,12 +142,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax
:
6000
,
OpeningBook
:
[]
string
{
"ruy_lopez"
,
"queens_gambit"
,
"nimzo_indian"
},
AvatarID
:
"bot-ziad"
,
PortraitURL
:
"/portraits/ziad.png"
,
},
"grandmaster"
:
{
ID
:
"grandmaster"
,
Name
:
"Grandmaster Bot"
,
NameAr
:
"الجراند ماستر"
,
Style
:
"near_perfect"
,
StyleAr
:
"شبه مثالي"
,
Bio
:
"Full Stockfish strength. Punishes every mistake. Good luck."
,
BioAr
:
"قوة ستوكفيش الكاملة. بيعاقب كل غلطة. حظ سعيد."
,
ELOMin
:
2000
,
...
...
@@ -147,6 +162,7 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax
:
8000
,
OpeningBook
:
[]
string
{},
AvatarID
:
"bot-grandmaster"
,
PortraitURL
:
"/portraits/grandmaster.png"
,
},
}
...
...
web/templates/bot_create.html
View file @
37a67945
...
...
@@ -3,52 +3,77 @@
<h2>
Create New Bot
</h2>
{{if .Error}}
<div
class=
"alert alert-error"
>
{{.Error}}
</div>
{{end}}
<form
method=
"POST"
action=
"/admin/bots/create"
>
<div
class=
"form-group"
>
<label>
ID (lowercase, no spaces)
</label>
<input
type=
"text"
name=
"id"
required
pattern=
"[a-z0-9_]+"
>
<div
style=
"display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;"
>
<div
class=
"form-group"
>
<label>
ID (lowercase, no spaces)
</label>
<input
type=
"text"
name=
"id"
required
pattern=
"[a-z0-9_]+"
>
</div>
<div
class=
"form-group"
>
<label>
Avatar ID
</label>
<input
type=
"text"
name=
"avatar_id"
placeholder=
"bot-name"
>
</div>
<div
class=
"form-group"
>
<label>
Name
</label>
<input
type=
"text"
name=
"name"
required
>
</div>
<div
class=
"form-group"
>
<label>
Name (Arabic)
</label>
<input
type=
"text"
name=
"name_ar"
dir=
"rtl"
>
</div>
<div
class=
"form-group"
>
<label>
Style
</label>
<input
type=
"text"
name=
"style"
required
placeholder=
"e.g. aggressive, defensive, positional"
>
</div>
<div
class=
"form-group"
>
<label>
Style (Arabic)
</label>
<input
type=
"text"
name=
"style_ar"
dir=
"rtl"
placeholder=
"مثلا: هجومي، دفاعي"
>
</div>
<div
class=
"form-group"
style=
"grid-column: 1 / -1;"
>
<label>
Bio
</label>
<input
type=
"text"
name=
"bio"
placeholder=
"Short description of the bot's personality"
>
</div>
<div
class=
"form-group"
style=
"grid-column: 1 / -1;"
>
<label>
Bio (Arabic)
</label>
<input
type=
"text"
name=
"bio_ar"
dir=
"rtl"
placeholder=
"وصف قصير لشخصية البوت"
>
</div>
<div
class=
"form-group"
>
<label>
Skill Level (0-20)
</label>
<input
type=
"number"
name=
"skill_level"
value=
"10"
min=
"0"
max=
"20"
>
</div>
<div
class=
"form-group"
>
<label>
Depth (1-30)
</label>
<input
type=
"number"
name=
"depth"
value=
"10"
min=
"1"
max=
"30"
>
</div>
<div
class=
"form-group"
>
<label>
Contempt (-100 to 100)
</label>
<input
type=
"number"
name=
"contempt"
value=
"0"
min=
"-100"
max=
"100"
>
</div>
<div
class=
"form-group"
>
<label>
Blunder Chance (0-100%)
</label>
<input
type=
"number"
name=
"blunder_chance"
value=
"5"
min=
"0"
max=
"100"
>
</div>
<div
class=
"form-group"
>
<label>
Think Time Min (ms)
</label>
<input
type=
"number"
name=
"think_time_min"
value=
"1000"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
Think Time Max (ms)
</label>
<input
type=
"number"
name=
"think_time_max"
value=
"3000"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
ELO Min
</label>
<input
type=
"number"
name=
"elo_min"
value=
"1000"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
ELO Max
</label>
<input
type=
"number"
name=
"elo_max"
value=
"1200"
min=
"0"
>
</div>
</div>
<div
class=
"form-group"
>
<label>
Name
</label>
<input
type=
"text"
name=
"name"
required
>
<p
style=
"margin-top: 0.5rem; color: #666; font-size: 0.85rem;"
>
Portrait can be uploaded after creation from the edit page.
</p>
<div
style=
"margin-top: 1rem;"
>
<button
type=
"submit"
class=
"btn btn-success"
>
Create Bot
</button>
<a
href=
"/admin/bots"
class=
"btn"
style=
"margin-left:1rem;"
>
Cancel
</a>
</div>
<div
class=
"form-group"
>
<label>
Style
</label>
<input
type=
"text"
name=
"style"
required
>
</div>
<div
class=
"form-group"
>
<label>
Skill Level (0-20)
</label>
<input
type=
"number"
name=
"skill_level"
value=
"10"
min=
"0"
max=
"20"
>
</div>
<div
class=
"form-group"
>
<label>
Depth
</label>
<input
type=
"number"
name=
"depth"
value=
"10"
min=
"1"
max=
"30"
>
</div>
<div
class=
"form-group"
>
<label>
Contempt
</label>
<input
type=
"number"
name=
"contempt"
value=
"0"
min=
"-100"
max=
"100"
>
</div>
<div
class=
"form-group"
>
<label>
Blunder Chance (0-100%)
</label>
<input
type=
"number"
name=
"blunder_chance"
value=
"5"
min=
"0"
max=
"100"
>
</div>
<div
class=
"form-group"
>
<label>
Think Time Min (ms)
</label>
<input
type=
"number"
name=
"think_time_min"
value=
"1000"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
Think Time Max (ms)
</label>
<input
type=
"number"
name=
"think_time_max"
value=
"3000"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
ELO Min
</label>
<input
type=
"number"
name=
"elo_min"
value=
"1000"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
ELO Max
</label>
<input
type=
"number"
name=
"elo_max"
value=
"1200"
min=
"0"
>
</div>
<button
type=
"submit"
class=
"btn btn-success"
>
Create Bot
</button>
<a
href=
"/admin/bots"
class=
"btn"
style=
"margin-left:1rem;"
>
Cancel
</a>
</form>
</div>
{{end}}
web/templates/bot_edit.html
View file @
37a67945
...
...
@@ -3,41 +3,89 @@
<h2>
Edit Bot: {{.Bot.Name}}
</h2>
{{if .Success}}
<div
class=
"alert alert-success"
>
Bot updated successfully.
</div>
{{end}}
{{if .Error}}
<div
class=
"alert alert-error"
>
{{.Error}}
</div>
{{end}}
<form
method=
"POST"
action=
"/admin/bots/edit/{{.Bot.ID}}"
>
<div
class=
"form-group"
>
<label>
Skill Level (0-20)
</label>
<input
type=
"number"
name=
"skill_level"
value=
"{{.Bot.SkillLevel}}"
min=
"0"
max=
"20"
>
</div>
<div
class=
"form-group"
>
<label>
Depth
</label>
<input
type=
"number"
name=
"depth"
value=
"{{.Bot.Depth}}"
min=
"1"
max=
"30"
>
</div>
<div
class=
"form-group"
>
<label>
Contempt
</label>
<input
type=
"number"
name=
"contempt"
value=
"{{.Bot.Contempt}}"
min=
"-100"
max=
"100"
>
<!-- Portrait Section -->
<div
style=
"margin-bottom: 1.5rem; padding: 1rem; background: #f8f9fa; border-radius: 8px; display: flex; align-items: center; gap: 1.5rem;"
>
<div
style=
"width: 128px; height: 128px; border-radius: 8px; overflow: hidden; background: #ddd; flex-shrink: 0;"
>
{{if .Bot.PortraitURL}}
<img
src=
"{{.Bot.PortraitURL}}"
alt=
"{{.Bot.Name}}"
style=
"width: 100%; height: 100%; object-fit: cover;"
>
{{else}}
<div
style=
"width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999; font-size: 2rem;"
>
♜
</div>
{{end}}
</div>
<div
class=
"form-group"
>
<label>
Blunder Chance (0-100%)
</label>
<input
type=
"number"
name=
"blunder_chance"
value=
"{{printf "
%.
0f
"
(
mul
.
Bot
.
BlunderChance
100
)}}"
min=
"0"
max=
"100"
>
<div>
<p
style=
"margin-bottom: 0.5rem; font-weight: 600;"
>
Portrait (512x512 px)
</p>
<form
method=
"POST"
action=
"/admin/bots/portrait/{{.Bot.ID}}"
enctype=
"multipart/form-data"
style=
"display: flex; gap: 0.5rem; align-items: center;"
>
<input
type=
"file"
name=
"portrait"
accept=
".png,.jpg,.jpeg,.webp"
required
style=
"font-size: 0.85rem;"
>
<button
type=
"submit"
class=
"btn btn-primary"
>
Upload
</button>
</form>
<p
style=
"margin-top: 0.3rem; color: #666; font-size: 0.8rem;"
>
Accepts PNG, JPG, WebP. Will be served at {{.Bot.PortraitURL}}
</p>
</div>
<div
class=
"form-group"
>
<label>
Think Time Min (ms)
</label>
<input
type=
"number"
name=
"think_time_min"
value=
"{{.Bot.ThinkTimeMin}}"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
Think Time Max (ms)
</label>
<input
type=
"number"
name=
"think_time_max"
value=
"{{.Bot.ThinkTimeMax}}"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
ELO Min
</label>
<input
type=
"number"
name=
"elo_min"
value=
"{{.Bot.ELOMin}}"
min=
"0"
>
</div>
<form
method=
"POST"
action=
"/admin/bots/edit/{{.Bot.ID}}"
>
<div
style=
"display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;"
>
<div
class=
"form-group"
>
<label>
Name
</label>
<input
type=
"text"
name=
"name"
value=
"{{.Bot.Name}}"
>
</div>
<div
class=
"form-group"
>
<label>
Name (Arabic)
</label>
<input
type=
"text"
name=
"name_ar"
value=
"{{.Bot.NameAr}}"
dir=
"rtl"
>
</div>
<div
class=
"form-group"
>
<label>
Style
</label>
<input
type=
"text"
name=
"style"
value=
"{{.Bot.Style}}"
>
</div>
<div
class=
"form-group"
>
<label>
Style (Arabic)
</label>
<input
type=
"text"
name=
"style_ar"
value=
"{{.Bot.StyleAr}}"
dir=
"rtl"
>
</div>
<div
class=
"form-group"
style=
"grid-column: 1 / -1;"
>
<label>
Bio
</label>
<input
type=
"text"
name=
"bio"
value=
"{{.Bot.Bio}}"
>
</div>
<div
class=
"form-group"
style=
"grid-column: 1 / -1;"
>
<label>
Bio (Arabic)
</label>
<input
type=
"text"
name=
"bio_ar"
value=
"{{.Bot.BioAr}}"
dir=
"rtl"
>
</div>
<div
class=
"form-group"
>
<label>
Skill Level (0-20)
</label>
<input
type=
"number"
name=
"skill_level"
value=
"{{.Bot.SkillLevel}}"
min=
"0"
max=
"20"
>
</div>
<div
class=
"form-group"
>
<label>
Depth (1-30)
</label>
<input
type=
"number"
name=
"depth"
value=
"{{.Bot.Depth}}"
min=
"1"
max=
"30"
>
</div>
<div
class=
"form-group"
>
<label>
Contempt (-100 to 100)
</label>
<input
type=
"number"
name=
"contempt"
value=
"{{.Bot.Contempt}}"
min=
"-100"
max=
"100"
>
</div>
<div
class=
"form-group"
>
<label>
Blunder Chance (0-100%)
</label>
<input
type=
"number"
name=
"blunder_chance"
value=
"{{printf "
%.
0f
"
(
mul
.
Bot
.
BlunderChance
100
)}}"
min=
"0"
max=
"100"
>
</div>
<div
class=
"form-group"
>
<label>
Think Time Min (ms)
</label>
<input
type=
"number"
name=
"think_time_min"
value=
"{{.Bot.ThinkTimeMin}}"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
Think Time Max (ms)
</label>
<input
type=
"number"
name=
"think_time_max"
value=
"{{.Bot.ThinkTimeMax}}"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
ELO Min
</label>
<input
type=
"number"
name=
"elo_min"
value=
"{{.Bot.ELOMin}}"
min=
"0"
>
</div>
<div
class=
"form-group"
>
<label>
ELO Max
</label>
<input
type=
"number"
name=
"elo_max"
value=
"{{.Bot.ELOMax}}"
min=
"0"
>
</div>
</div>
<div
class=
"form-group
"
>
<
label>
ELO Max
</label
>
<
input
type=
"number"
name=
"elo_max"
value=
"{{.Bot.ELOMax}}"
min=
"0"
>
<div
style=
"margin-top: 1rem;
"
>
<
button
type=
"submit"
class=
"btn btn-primary"
>
Save Changes
</button
>
<
a
href=
"/admin/bots"
class=
"btn"
style=
"margin-left:1rem;"
>
Cancel
</a
>
</div>
<button
type=
"submit"
class=
"btn btn-primary"
>
Save Changes
</button>
<a
href=
"/admin/bots"
class=
"btn"
style=
"margin-left:1rem;"
>
Cancel
</a>
</form>
</div>
{{end}}
web/templates/bots.html
View file @
37a67945
...
...
@@ -5,7 +5,7 @@
<table>
<thead>
<tr>
<th>
ID
</th>
<th></th>
<th>
Name
</th>
<th>
Style
</th>
<th>
ELO
</th>
...
...
@@ -18,9 +18,23 @@
<tbody>
{{range .Bots}}
<tr>
<td>
{{.ID}}
</td>
<td>
{{.Name}}
</td>
<td>
{{.Style}}
</td>
<td>
<div
style=
"width: 40px; height: 40px; border-radius: 50%; overflow: hidden; background: #eee;"
>
{{if .PortraitURL}}
<img
src=
"{{.PortraitURL}}"
alt=
"{{.Name}}"
style=
"width: 100%; height: 100%; object-fit: cover;"
>
{{else}}
<div
style=
"width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999;"
>
♜
</div>
{{end}}
</div>
</td>
<td>
<strong>
{{.Name}}
</strong><br>
<span
style=
"color: #666; font-size: 0.8rem;"
dir=
"rtl"
>
{{.NameAr}}
</span>
</td>
<td>
{{.Style}}
<br>
<span
style=
"color: #666; font-size: 0.8rem;"
dir=
"rtl"
>
{{.StyleAr}}
</span>
</td>
<td>
{{.ELOMin}}-{{.ELOMax}}
</td>
<td>
{{.SkillLevel}}
</td>
<td>
{{.Depth}}
</td>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment