noprune 0.2.0
Python client for noprune chess bot platform - easily create custom chess systems
License: MIT
Requires Python: >=3.10
Keywords: ai, bot, chess, engine, game, websocket

# noprune

**Build your own chess system in Python with just a few lines of code.**

noprune is the official Python client for the [noprune.org](https://noprune.org) chess bot platform — where AI chess systems compete against each other in a fair, transparent rating system.

## Vision

**noprune** is a platform where anyone can create, deploy, and compete with their own chess AI.

- **No pruning, pure skill** — Every system plays every position. No shortcuts.
- **Fair matchmaking** — Systems are matched by skill level (Turing rating)
- **Open competition** — Your bot competes 24/7 against others worldwide
- **Learn by doing** — Start with random moves, evolve to neural networks

Whether you're a beginner learning minimax or a researcher testing cutting-edge models, noprune provides the arena.

## Installation

```bash
pip install noprune
```

## Quick Start

Create a chess bot in 10 lines:

```python
from noprune import Bot
import random

@Bot(system_id="your-id", secret="your-secret")
def my_bot(board):
    # board is a python-chess Board object
    moves = list(board.legal_moves)
    return random.choice(moves).uci()

my_bot.run()  # Connect and start playing!
```

That's it. Your bot will connect to noprune.org, join the matchmaking queue, and start playing games automatically.

## Getting Your Credentials

1. Sign up at [noprune.org](https://noprune.org)
2. Create a new "System" (your bot)
3. Copy your `system_id` and `secret`

## Examples

### Random Bot (Simplest)

```python
from noprune import Bot
import random

@Bot(system_id="...", secret="...")
def random_bot(board):
    return random.choice(list(board.legal_moves)).uci()

random_bot.run()
```

### Time-Aware Bot (Using GameState)

```python
from noprune import Bot, GameState

@Bot(system_id="...", secret="...")
def smart_bot(game: GameState):  # Note: takes GameState, not board
    # Access time information
    if game.my_time_ms < 10000:  # Less than 10 seconds
        return quick_move(game.board)

    # Access other game info
    print(f"Move {game.move_number}, playing as {game.color}")
    print(f"My time: {game.my_time_ms}ms, Opponent: {game.opponent_time_ms}ms")

    return best_move(game.board)

smart_bot.run()
```

### Stockfish Bot (Strong)

```python
from noprune import Bot
import chess.engine

@Bot(system_id="...", secret="...")
def stockfish_bot(board):
    with chess.engine.SimpleEngine.popen_uci("/usr/games/stockfish") as engine:
        result = engine.play(board, chess.engine.Limit(time=0.1))
        return result.move.uci()

stockfish_bot.run()
```

### Full-Featured Bot (Class-based)

```python
from noprune import Bot, GameState, GameResult, Action

class MyBot(Bot):
    def on_game_start(self, game: GameState):
        print(f"Game started vs {game.opponent}!")
        print(f"Playing as {game.color}, time: {game.my_time_ms}ms")

    def on_game_end(self, game: GameState, result: GameResult):
        print(f"Game over: {result.result} ({result.reason})")
        print(f"Winner: {result.winner}")
        if result.pgn:
            print(f"PGN: {result.pgn}")

    def on_move(self, game: GameState, move: str, is_my_move: bool):
        who = "I played" if is_my_move else "Opponent played"
        print(f"{who}: {move}")

    def on_draw_offer(self, game: GameState) -> bool:
        # Accept draw if low on time
        return game.my_time_ms < 5000

    def on_draw_declined(self, game: GameState):
        print("Draw offer declined!")

    def on_opponent_disconnect(self, game: GameState):
        print("Opponent disconnected, waiting...")

    def on_opponent_reconnect(self, game: GameState):
        print("Opponent reconnected!")

    def on_error(self, code: str, message: str):
        print(f"Error: {code} - {message}")

    def think(self, game: GameState) -> str | Action:
        # Your engine logic here
        return "e2e4"

bot = MyBot(system_id="...", secret="...")
bot.run()
```

### Resign & Draw Offers

```python
from noprune import Bot, Action

@Bot(system_id="...", secret="...")
def smart_bot(board):
    # Resign if losing badly
    if is_losing(board):
        return Action.RESIGN

    # Offer draw in equal endgame
    if is_drawn_endgame(board):
        return Action.OFFER_DRAW

    # Make a move AND offer draw
    if should_offer_draw(board):
        return Action.move_with_draw("e2e4")

    return calculate_best_move(board)
```

## API Reference

### Bot

The main class for creating chess bots.

```python
# Decorator style (board only)
@Bot(system_id="...", secret="...", server_url="wss://noprune.org/ws")
def my_bot(board):
    return "e2e4"

# Decorator style (with game state)
@Bot(system_id="...", secret="...")
def my_bot(game: GameState):
    return "e2e4"

# Class style
class MyBot(Bot):
    def think(self, game: GameState):
        return "e2e4"
```

**Constructor parameters:**
- `system_id` — Your system ID from noprune.org
- `secret` — Your system secret
- `server_url` — WebSocket URL (default: `wss://noprune.org/ws`)
- `auto_queue` — Auto-join queue after each game (default: `True`)

**Methods to override:**
- `think(game: GameState) -> str | Action` — Calculate your move (required)
- `on_game_start(game)` — Called when game begins
- `on_game_end(game, result: GameResult)` — Called when game ends
- `on_move(game, move, is_my_move)` — Called after each move
- `on_draw_offer(game) -> bool` — Return True to accept draw
- `on_draw_declined(game)` — Your draw offer was declined
- `on_opponent_disconnect(game)` — Opponent lost connection
- `on_opponent_reconnect(game)` — Opponent reconnected
- `on_error(code, message)` — Server error occurred

### GameState

```python
@dataclass
class GameState:
    game_id: str           # Unique game identifier
    board: chess.Board     # Current position (python-chess)
    color: str             # "white" or "black"
    opponent: str          # Opponent's name
    opponent_id: str       # Opponent's system ID
    moves: list[str]       # Move history in UCI format
    my_time_ms: int        # Your remaining time (ms)
    opponent_time_ms: int  # Opponent's remaining time (ms)

    # Properties
    is_my_turn: bool       # Is it my turn?
    move_number: int       # Current move number (1-indexed)
    ply: int               # Total half-moves played
```

### GameResult

```python
@dataclass
class GameResult:
    result: str   # "1-0", "0-1", "1/2-1/2"
    reason: str   # "checkmate", "timeout", "resign", etc.
    pgn: str      # Full game PGN

    # Properties
    winner: str   # "white", "black", or "draw"
```

### Action

```python
Action.RESIGN              # Give up
Action.OFFER_DRAW          # Propose a draw (without moving)
Action.move("e2e4")        # Make a move (same as returning "e2e4")
Action.move_with_draw("e2e4")  # Make a move AND offer draw
```

## Environment Variables

```bash
export SYSTEM_ID="your-system-id"
export SYSTEM_SECRET="your-secret"
export SERVER_URL="wss://noprune.org/ws"  # or ws://localhost:8086/ws

python my_bot.py
```

## Why "noprune"?

In chess programming, "pruning" means skipping moves that seem bad. Top systems like Stockfish prune aggressively — they don't even consider most legal moves.

**noprune** is different. It's a place where any approach is welcome:
- Brute-force minimax? Great.
- Neural network evaluation? Awesome.
- Random moves? Sure, let's see how it does.

The name reminds us: there's no "wrong" way to build a chess system. Every approach teaches us something.

## Links

- **Website**: [noprune.org](https://noprune.org)
- **GitHub**: [github.com/noprune/noprune](https://github.com/noprune/noprune)
- **PyPI**: [pypi.org/project/noprune](https://pypi.org/project/noprune)

## License

MIT