Skill: kenken64/chessguardian (version 1.0.0)

--- name: chessguardian description: > Play chess via ChessGuardian API — start games, make moves, watch live games, run autoplay bots (Stockfish or Minimax), and get board snapshots. Use when the user wants to play chess, start a new chess game, make a chess move, view a live chess board, run an AI chess bot, get a QR code for a chess game, analyze a chess position, or any chess-related interaction. Requires a running ChessGuardian instance. --- # ChessGuardian Skill Interact with ChessGuardian — a live chess platform with Stockfish AI. ## Configuration Set the base URL in the environment or default to: ``` CHESSGUARDIAN_URL=https://chessguardian-production.up.railway.app ``` ## API Endpoints ### Start a new game ``` POST /api/live/start Body: {"mode": "ai"} or {"mode": "pvp"} Returns: {id, fen, history, lastMove} ``` ### Get game state ``` GET /api/live/<game_id> Returns: {id, fen, history, status, turn, winChance, gameOver} ``` ### Make a move ``` POST /api/live/<game_id>/move Body: {"move": "e5"} (SAN or UCI format) Returns: {fen, history, stockfishMove, analysis, gameOver, status} ``` ### Get QR code ``` GET /api/live/<game_id>/qr Returns: PNG image ``` ## Workflows ### Start a game and send QR code 1. POST `/api/live/start` with `{"mode": "ai"}` 2. GET `/api/live/<id>/qr` → save to outbound media 3. Send QR image + live link to user ### Make a move for the user 1. POST `/api/live/<id>/move` with the move in SAN (e.g. "e5", "Nf6", "O-O") 2. Show the AI analysis and Stockfish's reply 3. Optionally render the board snapshot ### Run autoplay bot Execute the bundled scripts: - **Stockfish bot:** `python3 -u scripts/autoplay_stockfish.py <game_id|new> --depth 20 --delay 5` - **Minimax bot:** `python3 -u scripts/autoplay_minimax.py <game_id|new> --depth 4 --delay 5` Both scripts support `new` to auto-start a game. Use `-u` for unbuffered output. ### Render board snapshot 1. Generate an HTML file using chessboard.js with the FEN from game state 2. Screenshot with Playwright: `npx playwright screenshot --browser chromium --viewport-size=440,520 board.html output.png` 3. Send the image to user ## Dependencies - Python 3 with `chess` and `requests` packages - Stockfish binary (for autoplay_stockfish.py) - Playwright + Chromium (for board screenshots) - curl or equivalent for API calls ## Live page Users can watch games in browser at: `<base_url>/live/<game_id>`

path: scripts/autoplay_stockfish.py
content: #!/usr/bin/env python3
"""Stockfish autoplay bot for ChessGuardian live games.
Plays as Black using Stockfish engine against the server's Stockfish (White).

Usage:
  python3 autoplay_stockfish.py <game_id> [--depth 20] [--delay 5] [--url URL]
  python3 autoplay_stockfish.py new [--depth 20] [--delay 5] [--url URL]  # starts a new game
"""

import argparse
import math
import sys
import time

import chess
import chess.engine
import requests

DEFAULT_URL = "https://chessguardian-production.up.railway.app"
DEFAULT_DEPTH = 20
DEFAULT_DELAY = 5


def get_stockfish():
    for path in ["/usr/games/stockfish", "/usr/local/bin/stockfish", "stockfish"]:
        try:
            engine = chess.engine.SimpleEngine.popen_uci(path)
            print(f"✅ Stockfish loaded: {path}")
            return engine
        except Exception:
            continue
    print("❌ Stockfish not found!")
    sys.exit(1)


def start_new_game(base_url):
    resp = requests.post(f"{base_url}/api/live/start", json={"mode": "ai"})
    resp.raise_for_status()
    data = resp.json()
    game_id = data["id"]
    print(f"🆕 New game started: {game_id}")
    print(f"   First move: {data['history'][0]}")
    return game_id


def get_state(base_url, game_id):
    resp = requests.get(f"{base_url}/api/live/{game_id}")
    resp.raise_for_status()
    return resp.json()


def make_move(base_url, game_id, move):
    resp = requests.post(f"{base_url}/api/live/{game_id}/move", json={"move": move})
    return resp.json()


def find_best_move(engine, fen, depth):
    board = chess.Board(fen)
    result = engine.play(board, chess.engine.Limit(depth=depth))
    san = board.san(result.move)
    uci = result.move.uci()

    info = engine.analyse(board, chess.engine.Limit(depth=depth))
    score = info.get("score")
    eval_str, eval_cp = "", 0
    if score:
        pov = score.white()
        if pov.is_mate():
            eval_str = f"M{pov.mate()}"
            eval_cp = 10000 if pov.mate() > 0 else -10000
        else:
            eval_cp = pov.score()
            eval_str = f"{eval_cp / 100:+.2f}"
    return san, uci, eval_str, eval_cp


def eval_to_pct(cp):
    return max(0, min(100, round(50 + 50 * math.tanh(cp / 600))))


def main():
    parser = argparse.ArgumentParser(description="Stockfish autoplay for ChessGuardian")
    parser.add_argument("game_id", help="Live game ID or 'new' to start a new game")
    parser.add_argument("--depth", type=int, default=DEFAULT_DEPTH)
    parser.add_argument("--delay", type=float, default=DEFAULT_DELAY)
    parser.add_argument("--url", default=DEFAULT_URL)
    parser.add_argument("--max-moves", type=int, default=200)
    args = parser.parse_args()

    game_id = args.game_id
    if game_id.lower() == "new":
        game_id = start_new_game(args.url)

    print(f"\n♟️  STOCKFISH AUTOPLAY — Game {game_id}")
    print(f"   Engine: Stockfish (depth {args.depth})")
    print(f"   Delay: {args.delay}s | Max moves: {args.max_moves}")
    print(f"   Live: {args.url}/live/{game_id}")
    print(f"{'─' * 55}")

    engine = get_stockfish()

    try:
        move_count = 0
        while move_count < args.max_moves:
            state = get_state(args.url, game_id)

            if state.get("error"):
                print(f"❌ {state['error']}")
                break
            if state.get("gameOver"):
                print(f"\n🏁 Game Over! {state['status']} | Result: {state.get('result', 'N/A')}")
                print(f"   Total moves: {len(state['history'])}")
                break

            if state.get("turn") != "black":
                time.sleep(1)
                continue

            fen = state["fen"]
            history = state.get("history", [])
            move_num = len(history) // 2 + 1

            san, uci, eval_str, eval_cp = find_best_move(engine, fen, args.depth)
            black_pct = 100 - eval_to_pct(eval_cp)
            print(f"  {move_num}... {san:<8} eval: {eval_str:>8}  Black: {black_pct}%")

            result = make_move(args.url, game_id, san)
            if result.get("error"):
                result = make_move(args.url, game_id, uci)
                if result.get("error"):
                    print(f"  ❌ Move rejected: {result['error']}")
                    break

            sf_move = result.get("stockfishMove")
            if sf_move:
                new_num = len(result.get("history", [])) // 2 + 1
                print(f"  {new_num}. {sf_move:<8} (Server Stockfish)")

            if result.get("gameOver"):
                status = result.get("status", "unknown")
                res = result.get("result", "N/A")
                winner = "White" if res == "1-0" else "Black" if res == "0-1" else "Draw"
                print(f"\n🏆 {status.upper()}! {winner} wins!")
                print(f"   Total moves: {len(result.get('history', []))}")
                break

            move_count += 1
            time.sleep(args.delay)

    except KeyboardInterrupt:
        print("\n⏹️  Stopped")
    finally:
        engine.quit()
        print("👋 Engine closed")


if __name__ == "__main__":
    main()


path: scripts/run_minimax.sh
content: #!/bin/bash
# Run Minimax Alpha-Beta Pruning autoplay bot against ChessGuardian
# Usage: ./run_minimax.sh [game_id|new] [depth] [delay]
#
# Examples:
#   ./run_minimax.sh new          # Start new game, depth 4, delay 5s
#   ./run_minimax.sh new 5 3      # Start new game, depth 5, delay 3s
#   ./run_minimax.sh AbCdEfGh     # Join existing game
#   ./run_minimax.sh AbCdEfGh 4 10  # Join game, depth 4, delay 10s

GAME_ID="${1:-new}"
DEPTH="${2:-4}"
DELAY="${3:-5}"

echo "🧠 Minimax Alpha-Beta Autoplay Bot"
echo "   Game: $GAME_ID | Depth: $DEPTH | Delay: ${DELAY}s"
echo ""

python3 -u "$(dirname "$0")/autoplay_minimax.py" "$GAME_ID" --depth "$DEPTH" --delay "$DELAY"


path: scripts/run_stockfish.sh
content: #!/bin/bash
# Run Stockfish autoplay bot against ChessGuardian
# Usage: ./run_stockfish.sh [game_id|new] [depth] [delay]
#
# Examples:
#   ./run_stockfish.sh new          # Start new game, depth 20, delay 5s
#   ./run_stockfish.sh new 15 3     # Start new game, depth 15, delay 3s
#   ./run_stockfish.sh AbCdEfGh     # Join existing game
#   ./run_stockfish.sh AbCdEfGh 20 10  # Join game, depth 20, delay 10s

GAME_ID="${1:-new}"
DEPTH="${2:-20}"
DELAY="${3:-5}"

echo "🐟 Stockfish Autoplay Bot"
echo "   Game: $GAME_ID | Depth: $DEPTH | Delay: ${DELAY}s"
echo ""

python3 -u "$(dirname "$0")/autoplay_stockfish.py" "$GAME_ID" --depth "$DEPTH" --delay "$DELAY"


path: references/api.md
content: # ChessGuardian API Reference

## Game Modes
- `ai` — Play against Stockfish (server plays White, user plays Black)
- `pvp` — Player vs Player (two humans, no AI)

## Game Status Values
- `active` — Game in progress
- `checkmate` — Game ended by checkmate
- `draw` — Game drawn (stalemate, insufficient material, etc.)
- `resigned` — A player resigned

## Result Values
- `1-0` — White wins
- `0-1` — Black wins
- `1/2-1/2` — Draw

## Move Format
Moves can be in SAN (Standard Algebraic Notation) or UCI format:
- SAN: `e4`, `Nf3`, `O-O`, `Qxd5`, `exd5`
- UCI: `e2e4`, `g1f3`, `e1g1`

## Analysis Response
When making a move against AI, the response includes:
```json
{
  "analysis": {
    "bestMove": "Knight from g8 to f6 (Nf6)",
    "evaluation": "Position is equal",
    "explanation": "Why this move is recommended",
    "winChance": 50
  },
  "stockfishMove": "Nf3",
  "fen": "...",
  "history": ["e4", "e5", "Nf3"],
  "gameOver": false,
  "status": "active"
}
```

## Win Chance
- `winChance` — 0-100, percentage chance White wins
- 50 = equal, >50 = White advantage, <50 = Black advantage
- Computed from material balance using sigmoid function

## Turn Indicator
- Derived from FEN (board.turn), not history length
- `turn: "white"` or `turn: "black"`


Security scan verdicts — ClawScan: clean | static analysis:  | VirusTotal: clean | SkillSpector: suspicious (HIGH)