game-ai-arena-sdk 0.4.0
Python SDK for running bots on Game AI Arena, supports FlipFour, FlipFlop3x3/5x5, Amoeba
Requires Python: >=3.11

# Game AI Arena SDK

Python SDK for running bots on Game AI Arena.

## Installation

```bash
pip install game-ai-arena-sdk
```

## Configuration

Set the matchmaker URL via environment variable or pass directly:

```bash
# Environment variable (recommended)
export MATCHMAKER_URL=ws://<SERVER_ADDRESS>:9000/matchmaking/ws

# Or for tunnel mode
export MATCHMAKER_URL=wss://<YOUR_TUNNEL>.trycloudflare.com/matchmaking/ws
```

Get the correct URL from your admin.

## Getting Your Credentials

### Option A: Swagger UI

1. Open `http://<SERVER>:9000/docs` in your browser
2. Use `/api/auth/signup` to create an account
3. Click **Authorize** and enter your access token
4. Use `/api/bots/` POST to create a bot (save the `bot_api_key` - shown once!)
5. Use `/api/bots/` GET to find your bot's `id`

### Option B: curl

```bash
# Set your server URL (get this from your admin)
SERVER="http://<SERVER_ADDRESS>:9000"

# 1. Create account
curl -X POST $SERVER/api/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "username": "yourname", "password": "SecurePass123"}'

# Response: {"access_token": "eyJhbG...", ...}
TOKEN="paste_your_access_token_here"

# 2. Create a bot
curl -X POST $SERVER/api/bots/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"bot_name": "MyBot", "game_type": "flipflop_3x3"}'

# Response: {"message": "Bot created successfully", "bot_api_key": "..."}
# Save the bot_api_key - it's only shown once!

# 3. Get your bot ID (query your user's bots)
USER_ID="your-user-id"  # from signup response or /api/profile
curl -X POST $SERVER/api/users/$USER_ID/bots \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{}'

# Find your bot in the response, copy its "id" field
```

## Quick Start

```python
import random
from game_ai_arena_sdk import Bot, start, GameType, Move, GameStateLoop

class MyBot(Bot):
    async def on_move(self, state: GameStateLoop) -> Move:
        piece = random.choice(state.legal_moves)
        dest = random.choice(piece.valid_moves)
        return Move(from_pos=piece.pos, to_pos=dest)

bot = MyBot(bot_id="YOUR_BOT_ID", api_key="YOUR_API_KEY")

# Uses MATCHMAKER_URL env var, or pass explicitly:
start(bot, GameType.FLIPFLOP_3X3)
# Or: start(bot, GameType.FLIPFLOP_3X3, matchmaker_url="ws://...")
```

## GameStateLoop

Received in `on_move`:

| Field | Description |
|-------|-------------|
| `board` | Board string representation |
| `my_side` | Your side (`"white"` / `"black"`) |
| `current_turn` | Whose turn it is |
| `legal_moves` | List of `PieceMovesInfo` you can move |

Each `PieceMovesInfo` has `name`, `pos`, and `valid_moves`.

## Hooks

All optional except `on_move`:

```python
async def on_move(self, state: GameStateLoop) -> Move  # Required
async def on_ready(self) -> None                       # Connected to matchmaker
async def on_queue_entry(self) -> None                 # Joined queue
async def on_queue_exit(self) -> None                  # Leaving queue
async def on_match_found(self, match_id: str) -> None  # Matched with opponent
async def on_room_joined(self, room_id: str) -> None   # Joined game room
async def on_game_start(self, state: GameStateLoop) -> None
async def on_game_end(self, winner: str | None, state: GameStateEnd) -> None
async def on_disconnect(self, reason: str) -> None     # Disconnected
```

## Multiple Bots

```python
import asyncio
from game_ai_arena_sdk import run

async def main():
    await asyncio.gather(
        run(MyBot("id1", "key1"), GameType.FLIPFLOP_3X3),
        run(MyBot("id2", "key2"), GameType.FLIPFLOP_3X3),
    )

asyncio.run(main())
```

> **Note:** Bots owned by the same user cannot be matched against each other. To test two bots locally, they must belong to different user accounts.

## FlipFour Notes

FlipFour has two move types: **placement** (from hand) and **movement** (on board).

When placing a piece from hand, you must specify the `side` (`"+"` for orthogonal, `"x"` for diagonal):

```python
# Placement: piece from hand onto the board
Move(from_pos="HAND", to_pos="C3", side="+")  # place as orthogonal
Move(from_pos="HAND", to_pos="C3", side="x")  # place as diagonal

# Movement: existing piece on board (side flips automatically)
Move(from_pos="A1", to_pos="A4")  # no side needed
```

In `legal_moves`, hand placements appear as two entries with `pos="HAND"`:
- `PieceMovesInfo(name="+", pos="HAND", valid_moves=[...])` -- place as `+`
- `PieceMovesInfo(name="x", pos="HAND", valid_moves=[...])` -- place as `x`

Board pieces appear with their current side and position:
- `PieceMovesInfo(name="+", pos="C3", valid_moves=["A3", "B3", ...])`

```python
class MyFlipFourBot(Bot):
    async def on_move(self, state: GameStateLoop) -> Move:
        piece = random.choice(state.legal_moves)
        target = random.choice(piece.valid_moves)
        side = piece.name if piece.pos == "HAND" else None
        return Move(from_pos=piece.pos, to_pos=target, side=side)

start(bot, GameType.FLIPFOUR)
```

## Game Types

| Type | Enum | Description |
|------|------|-------------|
| FlipFlop 3x3 | `FLIPFLOP_3X3` | 3x3 board, rook/bishop pieces |
| FlipFlop 5x5 | `FLIPFLOP_5X5` | 5x5 board, rook/bishop pieces |
| FlipFour | `FLIPFOUR` | 5x5 board, 4 pieces per player, line-of-four wins |
| Amoeba | `AMOEBA` | Hexagonal grid, kernel capture wins |

## Amoeba Notes

Amoeba is played on a hexagonal board (radius 3) using axial coordinates (`x,y,z` where `x+y+z=0`).

**Key mechanics:**
- Each player has 1 **kernel** + 10 standard pieces
- Pieces can **stack** on top of each other; the top piece controls the stack
- Stack height determines **mandatory** move distance (a stack of 3 must move exactly 3 cells)
- **Win** by capturing the opponent's kernel (landing on it) or when opponent has no legal moves
- Threefold repetition triggers a stack-count tiebreaker (not automatic draw)

**Splitting moves:**

When moving a stack, you can optionally **split** it by setting `splitting=True`. This spreads pieces along the movement path instead of keeping them together:

```python
# Normal move: entire stack moves together
Move(from_pos="0,0,0", to_pos="2,-1,-1")

# Split move: pieces spread along the path
Move(from_pos="0,0,0", to_pos="2,-1,-1", splitting=True)
```

**Example bot:**

```python
class MyAmoebaBot(Bot):
    async def on_move(self, state: GameStateLoop) -> Move:
        # Pick a piece with valid moves
        pieces_with_moves = [p for p in state.legal_moves if p.valid_moves]
        piece = random.choice(pieces_with_moves)
        target = random.choice(piece.valid_moves)

        # Optionally split the stack (random choice here)
        should_split = random.choice([True, False])

        return Move(from_pos=piece.pos, to_pos=target, splitting=should_split)

start(bot, GameType.AMOEBA)
```

**Coordinates:**

Positions use axial coordinates as comma-separated strings: `"x,y,z"` (e.g., `"0,0,0"`, `"-1,2,-1"`).

In `legal_moves`, each `PieceMovesInfo` shows the piece name, its position, and valid destination coordinates.