Metadata-Version: 2.4
Name: django-word-battle
Version: 0.1.0
Summary: Reusable Django REST Framework + Channels backend for competitive multiplayer word-change games (rules engine, matchmaking, ratings, rewards, store and ad verification).
Author-email: Ifeanyi Stanley Nnamani <nnamaniifeanyi10@gmail.com>
Maintainer-email: Ifeanyi Stanley Nnamani <nnamaniifeanyi10@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/NzeStan/django-word-battle
Project-URL: Repository, https://github.com/NzeStan/django-word-battle
Project-URL: Documentation, https://github.com/NzeStan/django-word-battle/tree/main/docs
Project-URL: Issues, https://github.com/NzeStan/django-word-battle/issues
Project-URL: Changelog, https://github.com/NzeStan/django-word-battle/blob/main/CHANGELOG.md
Keywords: django,djangorestframework,channels,game,multiplayer,word game,websocket
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment :: Puzzle Games
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<6.0,>=5.1
Requires-Dist: djangorestframework>=3.15
Requires-Dist: channels>=4.1
Requires-Dist: cryptography>=42
Requires-Dist: requests>=2.31
Provides-Extra: redis
Requires-Dist: channels-redis>=4.2; extra == "redis"
Requires-Dist: redis>=5.0; extra == "redis"
Provides-Extra: celery
Requires-Dist: celery>=5.4; extra == "celery"
Provides-Extra: jwt
Requires-Dist: djangorestframework-simplejwt>=5.3; extra == "jwt"
Provides-Extra: google
Requires-Dist: google-auth>=2.29; extra == "google"
Provides-Extra: docs
Requires-Dist: drf-spectacular>=0.27; extra == "docs"
Provides-Extra: all
Requires-Dist: channels-redis>=4.2; extra == "all"
Requires-Dist: redis>=5.0; extra == "all"
Requires-Dist: celery>=5.4; extra == "all"
Requires-Dist: djangorestframework-simplejwt>=5.3; extra == "all"
Requires-Dist: google-auth>=2.29; extra == "all"
Requires-Dist: drf-spectacular>=0.27; extra == "all"
Provides-Extra: dev
Requires-Dist: django-word-battle[all]; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-django>=4.8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: daphne>=4.1; extra == "dev"
Requires-Dist: psycopg[binary]>=3.1; extra == "dev"
Requires-Dist: dj-database-url>=2.1; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# django-word-battle

A reusable **Django REST Framework + Channels** backend for competitive multiplayer
word-change games. Players take turns changing letters (FALL → CALL → TALL), and a
player loses when they have no legal move, run out of time or resign.

The product name is **not hard-coded**. Set `WORD_BATTLE["GAME_NAME"]` or the
`WORD_BATTLE_GAME_NAME` environment variable and the API, admin, emails and API docs use it.
The Python package keeps the neutral import name `word_battle`.

```
pip install "django-word-battle[all]"
```

## What's inside

| Area | What you get |
|---|---|
| **Rules engine** | Pure-Python authoritative engine: 1..k letter changes, scaling by word length, add/remove letters, lock-last-letter, power moves, repeat policy, move cap. Fast legal-move discovery through a word graph. Start-word rating that avoids dud games. |
| **Modes** | Classic (untimed/async), Blitz, Ranked, Long Words, Shape-shifter, Power Blitz, Arena (3–8 players), Daily Challenge and Practice. All are configured in the admin, not in code. |
| **Matches** | Private challenges with invite codes, rematches, bots (always labelled), spectators with privacy controls, resign/leave, server-side clocks with latency grace, timeouts, eliminations and placements. |
| **Real time** | WebSockets per match and per user, with JWT auth. Gapless, persisted event log with sequence numbers for resync. Idempotent moves (`client_move_id`), presence, reactions, per-socket rate limits. |
| **Matchmaking** | PostgreSQL queue using `SKIP LOCKED`, a rating window that widens with wait time, region hints, avoidance of recent opponents and blocked players, arena filling, and bot fallback. |
| **Competition** | Glicko-2 ratings per pool, both all-time and per season. Leaderboards, daily challenge leaderboard and streaks, achievements, XP and levels. |
| **Economy** | Idempotent reward ledger with a daily coin cap. Wallet separates earned and purchased coins and never allows cash-out. Cosmetic store, inventory and equipping. |
| **Monetization** | Google Play verification (Developer API, RTDN via Pub/Sub with OIDC check), Apple StoreKit 2 JWS verification (x5c chain up to Apple Root G3, Apple OIDs), App Store Server Notifications V2, refunds and revocations, subscriptions and entitlements. AdMob rewarded-ad SSV with ECDSA verification and replay protection. |
| **Safety** | Friends, blocks, reports, moderation actions with appeals, word reports, profanity filtering, fast-move and win-trading signals for human review. No automatic bans. |
| **Ops** | Feature flags with rollout percentages, audit log, admin with read-only match outcomes and audited wallet adjustments, metrics endpoint (DAU/MAU, retention, verified revenue), health checks, system checks, Celery tasks or a no-Celery worker loop. |
| **Privacy** | Personal data export and account deletion; match and accounting history are anonymised, not destroyed. |

Real-money wagering, deposits, cash-out and paid-entry cash prizes are **not implemented**.
Sponsored real-world prizes are blocked by a database constraint until legal approval,
an app-store policy review and official rules are recorded. They also need the
`real_world_prizes` flag, which can only be turned on in the admin.

## Quick start (existing Django project)

```python
# settings.py
INSTALLED_APPS += ["rest_framework", "channels", "word_battle"]
ASGI_APPLICATION = "config.asgi.application"
CHANNEL_LAYERS = {"default": {"BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": {
    "hosts": [{"address": "redis://localhost:6379/0", "socket_timeout": 30}]}}}
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ["rest_framework_simplejwt.authentication.JWTAuthentication"],
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
    "EXCEPTION_HANDLER": "word_battle.api.exceptions.exception_handler",
}
WORD_BATTLE = {"GAME_NAME": "Word Clash"}

# urls.py
path("api/v1/", include("word_battle.api.urls")),

# asgi.py
from word_battle.realtime.middleware import OriginOrNativeValidator, WordBattleAuthMiddlewareStack
from word_battle.realtime.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": OriginOrNativeValidator(WordBattleAuthMiddlewareStack(URLRouter(websocket_urlpatterns))),
})
```

```bash
python manage.py migrate
python manage.py wb_setup_defaults      # dictionary (ENABLE), modes, rewards, achievements, cosmetics, flags, start words
celery -A config worker & celery -A config beat   # or: python manage.py wb_worker
```

Set `app.conf.beat_schedule = word_battle.tasks.BEAT_SCHEDULE` in your Celery app. The
schedule covers turn timeouts every 1 s, matchmaking every 2 s, and the daily challenge,
housekeeping and anti-cheat scans.

To run the complete example stack with Docker:

```bash
cp .env.example .env
docker compose up --build        # API on :8000, docs at /api/docs/, admin at /admin/
```

## Configuration

All keys are optional. Nested dicts are merged with the defaults; see `src/word_battle/conf.py` for every key.

| Key | Default | Purpose |
|---|---|---|
| `GAME_NAME` / `GAME_SLUG` | `Word Battle` / `word-battle` | Branding, shown via `/meta/`, admin and notifications |
| `ASYNC_TASKS` | `False` | Push and other background work through Celery |
| `BOT_EXECUTION` | `inline` | `inline` (in-request) or `celery` (with a human-like delay) |
| `LATENCY_GRACE_SECONDS` | `1.5` | Extra time after a deadline before a timeout is applied |
| `MATCHMAKING` | window 100 → 800, bots after 30 s | Rating window growth, bot fallback, arena fill wait |
| `RATING` | Glicko-2 1500/350/0.06, τ 0.5 | Initial rating values; minimum games for leaderboards |
| `REWARDS` | daily coin cap 500 | Reward codes used automatically and the XP curve |
| `ADS` | 10 rewarded ads per day | AdMob key URL, reward code, daily cap |
| `GOOGLE_PLAY`, `APPLE` | — | Store verification credentials (see docs/monetization.md) |
| `NOTIFICATION_BACKEND` | logging | `word_battle.services.notifications.FCMBackend` for push |
| `THROTTLE_RATES` | moves 120/min … | Per-scope rate limits |
| `FEATURE_FLAGS` | — | Defaults; rows in the `FeatureFlag` table override them |
| `HOOKS` | — | Dotted paths called on match completion, verified purchases and reports |

The package also sends Django signals: `match_completed`, `move_accepted`,
`purchase_verified`, `reward_granted` and `achievement_unlocked`, all from
`word_battle.signals`.

## Management commands

| Command | Purpose |
|---|---|
| `wb_setup_defaults` | Load the dictionary and default content (safe to re-run) |
| `wb_import_dictionary --file words.txt [--common common.txt]` | Import a new dictionary version |
| `wb_rate_start_words [mode ...]` | Pre-rate playable start words (rejects starts with an instant trap) |
| `wb_worker` | Scheduler loop when you don't run Celery beat |

## Documentation

- [docs/api.md](docs/api.md): REST endpoints, error codes, idempotency
- [docs/realtime.md](docs/realtime.md): WebSocket protocol and event catalogue
- [docs/flutter-integration.md](docs/flutter-integration.md): client integration guide
- [docs/monetization.md](docs/monetization.md): store, ads and sponsored-campaign setup and compliance limits
- [docs/deployment.md](docs/deployment.md): production setup, scaling, backups, runbooks
- [docs/release-checklist.md](docs/release-checklist.md): Google Play and App Store release checklist
- `docs/openapi.yml`: generated OpenAPI 3 schema. With drf-spectacular, `/api/docs/` serves it live.

## Development

```bash
pip install -e ".[dev]"
createdb word_battle    # tests need PostgreSQL (row locks, SKIP LOCKED); set DATABASE_URL if different
pytest                  # 109 tests: engine, services, API, WebSockets, real concurrency
ruff check src tests example_project
python scripts/smoke_test.py --base http://localhost:8000   # end-to-end against a running stack
```

## Licence

MIT. The bundled ENABLE word list is in the public domain. A "common words" list is
**not** bundled, because the popular frequency lists are not licensed for commercial use.
Supply your own with `wb_import_dictionary --common`.
