Commit b845b5a0 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: Bot Tournament Testing — populate tournaments with bots and auto-simulate results

Adds a complete bot testing system to the Arbiter Tools tab:
- Fill tournaments with bot players (8-128) with realistic Arabic names and
  bell-curve rating distribution
- Auto-play individual rounds using ELO probability simulation
- Simulate entire tournament with progress bar UI (sequential AJAX)
- Cleanup bots with one click
- Bot players marked with 🤖 badge in players list

New files: BotSimulationService.php, _bot_testing.php, migration 005
Also updates EL3AB_PLAYER_APP_DATA.md with domino/ludo/theme/tournament schemas
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 0dc3c835
# EL3AB Player App — Complete Server Data Reference
> This document contains ALL actual production data from the El3ab self-hosted Supabase instance.
> Every schema, enum value, config setting, and function listed here was queried directly from the live database.
> **Do NOT invent or assume data that is not in this file.**
---
## 1. CONNECTION DETAILS
### Supabase (Self-hosted on Docker/CapRover)
| Key | Value |
|-----|-------|
| **SUPABASE_URL** | `https://safe-supabase-kong.caprover.al-arcade.com` |
| **ANON KEY** | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.bFnS-YBhykTQ6vqrfTKJqmAB_aSW6GUgCat3QLkgCv8` |
| **SERVICE_ROLE KEY** | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4` |
| **JWT Secret** | `super-secret-jwt-token-with-at-least-32-characters-long` |
| **Server IP** | `3.68.63.185` (AWS Frankfurt) |
| **DB Container** | `safe-supabase-db` |
| **DB User** | `supabase_admin` |
| **DB Name** | `postgres` |
### Auth Configuration
- Provider: Supabase Auth (email/password + OAuth)
- New user trigger: `handle_new_user()` creates profile automatically
- Default language: Arabic (`'ar'`)
- Default display_name: `'لاعب جديد'` (New Player)
### Swiss API (Tournament Engine)
| Key | Value |
|-----|-------|
| **URL** | `https://swissapi.caprover.al-arcade.com/api/v1` |
| **Purpose** | Swiss pairing generation, standings calculation, TRF export |
### Storage Base URL
```
https://safe-supabase-kong.caprover.al-arcade.com/storage/v1/object/public/{bucket-name}/{path}
```
---
## 2. DATABASE SCHEMA (99 Tables)
### 2.1 PROFILES (Core Player Table)
```sql
-- Table: profiles
-- RLS: SELECT open to everyone, UPDATE only own profile
-- Realtime: enabled
-- FK: profiles.id -> auth.users(id) ON DELETE CASCADE
id UUID PK (= auth.users.id)
username TEXT NOT NULL UNIQUE
display_name TEXT NOT NULL
display_name_ar TEXT
avatar_url TEXT
banner_url TEXT
bio TEXT
bio_ar TEXT
country_code CHAR(3)
city TEXT
preferred_language TEXT DEFAULT 'ar'
-- Ratings (per time control)
elo_bullet INT DEFAULT 1200
elo_blitz INT DEFAULT 1200
elo_rapid INT DEFAULT 1200
elo_classical INT DEFAULT 1200
-- FIDE Integration
fide_id TEXT
fide_rating_standard INT
fide_rating_rapid INT
fide_rating_blitz INT
fide_title TEXT -- GM, IM, FM, CM, WGM, etc.
-- Progression
xp INT DEFAULT 0
level INT DEFAULT 1
coins INT DEFAULT 0
gems INT DEFAULT 0
premium_currency INT DEFAULT 0
-- Status
is_online BOOLEAN DEFAULT false
last_seen_at TIMESTAMPTZ
is_banned BOOLEAN DEFAULT false
ban_reason TEXT
ban_expires_at TIMESTAMPTZ
banned_by UUID
-- Stats
total_games_played INT DEFAULT 0
total_wins INT DEFAULT 0
total_draws INT DEFAULT 0
total_losses INT DEFAULT 0
total_tournaments_played INT DEFAULT 0
total_tournaments_won INT DEFAULT 0
win_streak INT DEFAULT 0
best_win_streak INT DEFAULT 0
games_played INT DEFAULT 0
-- Cosmetics
current_game TEXT
avatar_frame_id UUID
avatar_border_color TEXT
active_org_frame_id UUID
-- Timestamps
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
-- Daily Rewards
last_daily_reward TIMESTAMPTZ
daily_streak INT DEFAULT 0
```
### 2.2 MATCHES
```sql
-- Table: matches
-- RLS: SELECT open to everyone, INSERT/UPDATE only players in match
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
game_key TEXT NOT NULL DEFAULT 'chess'
white_player_id UUID -> profiles(id)
black_player_id UUID -> profiles(id)
match_type TEXT NOT NULL -- 'ranked', 'casual', 'tournament', 'bot'
tournament_id UUID
tournament_round INT
pairing_id UUID
status match_status DEFAULT 'waiting'
result match_result
time_control time_control NOT NULL
initial_time_ms INT NOT NULL
increment_ms INT DEFAULT 0
white_time_remaining_ms INT
black_time_remaining_ms INT
starting_fen TEXT DEFAULT 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
current_fen TEXT
pgn TEXT
moves JSONB DEFAULT '[]'
move_count INT DEFAULT 0
game_state JSONB DEFAULT '{}'
white_rating_before INT
black_rating_before INT
white_rating_after INT
black_rating_after INT
rating_change_white INT
rating_change_black INT
bot_id TEXT
bot_difficulty TEXT
is_flagged BOOLEAN DEFAULT false
cheat_score NUMERIC(5,2) DEFAULT 0
analysis_complete BOOLEAN DEFAULT false
is_rated BOOLEAN DEFAULT true
is_rematch BOOLEAN DEFAULT false
rematch_of UUID -> matches(id)
started_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.3 MATCHMAKING_QUEUE
```sql
-- Table: matchmaking_queue
-- RLS: Players see/insert/delete only their own entries
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
player_id UUID -> profiles(id) ON DELETE CASCADE
game_key TEXT NOT NULL DEFAULT 'chess'
time_control time_control NOT NULL
rating INT NOT NULL
rating_range_min INT
rating_range_max INT
is_rated BOOLEAN DEFAULT true
queued_at TIMESTAMPTZ DEFAULT now()
range_expansion_per_sec INT DEFAULT 10
max_wait_seconds INT DEFAULT 60
block_list UUID[] DEFAULT '{}'
region_preference TEXT
status TEXT DEFAULT 'searching' -- 'searching', 'matched'
matched_with UUID -> profiles(id)
match_id UUID -> matches(id)
expires_at TIMESTAMPTZ
```
### 2.4 EL3AB_TOURNAMENTS
```sql
-- Table: el3ab_tournaments
-- RLS: SELECT open to everyone, INSERT requires platform role
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
swiss_api_tournament_id UUID
org_id UUID -> el3ab_organizations(id)
game_key TEXT NOT NULL DEFAULT 'chess'
name TEXT NOT NULL
name_ar TEXT
description TEXT
description_ar TEXT
banner_url TEXT
format tournament_format NOT NULL
time_control time_control NOT NULL
custom_time_initial_ms INT
custom_time_increment_ms INT
rounds_total INT
swiss_rounds INT
bracket_size INT
bracket_best_of INT DEFAULT 1
entry_fee_coins INT DEFAULT 0
entry_fee_gems INT DEFAULT 0
min_rating INT
max_rating INT
min_players INT DEFAULT 4
max_players INT DEFAULT 256
prize_pool_coins INT DEFAULT 0
prize_pool_gems INT DEFAULT 0
prize_distribution JSONB DEFAULT '[]'
cosmetic_rewards JSONB DEFAULT '[]'
registration_opens_at TIMESTAMPTZ
registration_closes_at TIMESTAMPTZ
starts_at TIMESTAMPTZ NOT NULL
status TEXT DEFAULT 'draft' -- draft, registration, in_progress, completed, cancelled
current_round INT DEFAULT 0
charity_id UUID
charity_percent NUMERIC(5,2) DEFAULT 0
sponsor_id UUID
sponsor_branding JSONB DEFAULT '{}'
is_rated BOOLEAN DEFAULT true
is_fide_rated BOOLEAN DEFAULT false
allow_berserk BOOLEAN DEFAULT false
auto_start BOOLEAN DEFAULT true
created_by UUID -> profiles(id)
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
tournament_mode TEXT DEFAULT 'single' CHECK (IN ('single', 'multi_phase'))
phase_config JSONB DEFAULT '[]'
current_phase INT DEFAULT 1
total_phases INT DEFAULT 1
tiebreak_rules JSONB DEFAULT '["buchholz_cut_1", "buchholz", "sonneborn_berger"]'
acceleration_method TEXT
acceleration_rounds INT DEFAULT 0
-- Live page columns
slug TEXT UNIQUE
live_enabled BOOLEAN DEFAULT false
live_theme TEXT DEFAULT 'default'
live_custom_css TEXT
live_visibility JSONB DEFAULT '{"rules":true,"stats":true,"prizes":true,"ticker":true,"bracket":true,"gallery":true,"players":true,"pairings":true,"schedule":true,"standings":true,"announcements":true}'
live_branding JSONB DEFAULT '{}'
view_count INT DEFAULT 0
unique_visitors INT DEFAULT 0
```
### 2.5 TOURNAMENT_REGISTRATIONS
```sql
-- Table: tournament_registrations
-- UNIQUE: (tournament_id, player_id)
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID -> el3ab_tournaments(id) ON DELETE CASCADE
player_id UUID -> profiles(id) ON DELETE CASCADE
entry_fee_paid_coins INT DEFAULT 0
entry_fee_paid_gems INT DEFAULT 0
status TEXT DEFAULT 'registered' -- registered, withdrawn, disqualified
seed INT
final_standing INT
registered_at TIMESTAMPTZ DEFAULT now()
withdrawn_at TIMESTAMPTZ
```
### 2.6 EL3AB_TOURNAMENT_ROUNDS
```sql
-- Table: el3ab_tournament_rounds
-- UNIQUE: (tournament_id, round_number)
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID NOT NULL -> el3ab_tournaments(id) ON DELETE CASCADE
round_number INT NOT NULL
status TEXT DEFAULT 'pending' -- pending, in_progress, completed
started_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
pairings JSONB DEFAULT '[]'
results JSONB DEFAULT '[]'
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.7 TOURNAMENT_PHASES (Multi-Phase)
```sql
-- Table: tournament_phases
-- UNIQUE: (tournament_id, phase_number)
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID NOT NULL -> el3ab_tournaments(id) ON DELETE CASCADE
phase_number INT NOT NULL
name TEXT NOT NULL
name_ar TEXT
type TEXT NOT NULL CHECK (IN ('swiss','round_robin','single_elimination','double_elimination','arena','group_stage'))
config JSONB DEFAULT '{}'
swiss_api_tournament_id UUID
status TEXT DEFAULT 'pending' CHECK (IN ('pending','in_progress','completed'))
advancement_rule JSONB DEFAULT '{}'
started_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.8 BRACKET_MATCHES
```sql
-- Table: bracket_matches
id UUID PK DEFAULT gen_random_uuid()
bracket_id UUID NOT NULL -> tournament_brackets(id) ON DELETE CASCADE
tournament_id UUID NOT NULL -> el3ab_tournaments(id) ON DELETE CASCADE
phase_id UUID NOT NULL -> tournament_phases(id) ON DELETE CASCADE
round_number INT NOT NULL
match_number INT NOT NULL
player_a_id TEXT
player_a_name TEXT
player_a_seed INT
player_b_id TEXT
player_b_name TEXT
player_b_seed INT
result TEXT CHECK (IN ('player_a_wins','player_b_wins','draw','not_played','forfeit_a','forfeit_b','bye'))
score_a TEXT
score_b TEXT
winner_id TEXT
next_match_id UUID -> bracket_matches(id)
next_match_slot TEXT CHECK (IN ('player_a','player_b'))
loser_next_match_id UUID -> bracket_matches(id)
loser_next_match_slot TEXT CHECK (IN ('player_a','player_b'))
source_match_a_id UUID -> bracket_matches(id)
source_match_b_id UUID -> bracket_matches(id)
scheduled_at TIMESTAMPTZ
started_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
status TEXT DEFAULT 'pending' CHECK (IN ('pending','ready','in_progress','completed','bye'))
metadata JSONB DEFAULT '{}'
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.9 EL3AB_ORGANIZATIONS
```sql
-- Table: el3ab_organizations
-- RLS: SELECT where is_active=true, UPDATE requires org_admin role
id UUID PK DEFAULT gen_random_uuid()
swiss_api_org_id UUID
name TEXT NOT NULL
name_ar TEXT
slug TEXT NOT NULL UNIQUE
logo_url TEXT
banner_url TEXT
description TEXT
description_ar TEXT
website TEXT
contact_email TEXT
country_code CHAR(3)
city TEXT
tier TEXT DEFAULT 'free' -- free, bronze, silver, gold, diamond
max_members INT DEFAULT 50
max_concurrent_tournaments INT DEFAULT 3
features_enabled JSONB DEFAULT '{}'
custom_branding JSONB DEFAULT '{}'
revenue_share_percent NUMERIC(5,2) DEFAULT 0
charity_percent NUMERIC(5,2) DEFAULT 0
is_verified BOOLEAN DEFAULT false
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
primary_color TEXT DEFAULT '#6366f1'
secondary_color TEXT DEFAULT '#8b5cf6'
social_links JSONB DEFAULT '{}'
custom_css TEXT
tier_points INT DEFAULT 0
member_count INT DEFAULT 0
total_tournaments INT DEFAULT 0
total_matches INT DEFAULT 0
founded_at DATE
features JSONB DEFAULT '{}'
```
### 2.10 ORG_MEMBERS
```sql
-- Table: org_members
-- UNIQUE: (org_id, player_id)
id UUID PK DEFAULT gen_random_uuid()
org_id UUID -> el3ab_organizations(id) ON DELETE CASCADE
player_id UUID -> profiles(id) ON DELETE CASCADE
role TEXT DEFAULT 'member' -- owner, admin, moderator, member
joined_via TEXT DEFAULT 'invite' -- invite, application, link, referral
application_id UUID -> org_membership_applications(id)
joined_at TIMESTAMPTZ DEFAULT now()
display_name TEXT
custom_title TEXT
permissions JSONB DEFAULT '{}'
is_muted BOOLEAN DEFAULT false
muted_until TIMESTAMPTZ
contribution_points INT DEFAULT 0
last_active_at TIMESTAMPTZ
invited_by UUID
invite_link_id UUID
```
### 2.11 FRIENDSHIPS
```sql
-- Table: friendships
-- RLS: Only involved parties can SELECT; requester can INSERT
-- UNIQUE: (requester_id, addressee_id)
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
requester_id UUID -> profiles(id) ON DELETE CASCADE
addressee_id UUID -> profiles(id) ON DELETE CASCADE
status TEXT DEFAULT 'pending' -- pending, accepted, blocked
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.12 NOTIFICATIONS
```sql
-- Table: notifications
-- RLS: Users see and update only their own
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
user_id UUID -> profiles(id) ON DELETE CASCADE
type TEXT NOT NULL -- friend_request, match_invite, tournament_start, achievement_unlocked, etc.
title TEXT NOT NULL
title_ar TEXT
body TEXT
body_ar TEXT
data JSONB DEFAULT '{}' -- e.g. {match_id, tournament_id, sender_id}
is_read BOOLEAN DEFAULT false
read_at TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.13 CHAT_MESSAGES
```sql
-- Table: chat_messages
-- RLS: Authenticated can INSERT own; SELECT open to authenticated
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
channel_type TEXT NOT NULL -- 'match', 'tournament', 'club', 'direct', 'org'
channel_id UUID NOT NULL -- References match_id, tournament_id, club_id, etc.
sender_id UUID -> profiles(id)
content TEXT NOT NULL
is_system BOOLEAN DEFAULT false
is_deleted BOOLEAN DEFAULT false
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.14 COSMETICS
```sql
-- Table: cosmetics
-- RLS: SELECT open to everyone
id TEXT PK -- e.g. 'frame_gold_dragon', 'board_wood_classic'
name TEXT NOT NULL
name_ar TEXT
description TEXT
type cosmetic_type NOT NULL
rarity cosmetic_rarity NOT NULL
preview_url TEXT
asset_url TEXT
price_coins INT
price_gems INT
unlock_condition JSONB -- e.g. {"type":"level","value":10} or {"type":"achievement","id":"win_100"}
is_purchasable BOOLEAN DEFAULT true
is_limited_edition BOOLEAN DEFAULT false
available_until TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.15 PLAYER_COSMETICS
```sql
-- Table: player_cosmetics
-- RLS: Players see only their own
-- UNIQUE: (player_id, cosmetic_id)
id UUID PK DEFAULT gen_random_uuid()
player_id UUID -> profiles(id) ON DELETE CASCADE
cosmetic_id TEXT -> cosmetics(id)
acquired_at TIMESTAMPTZ DEFAULT now()
acquired_via TEXT -- 'purchase', 'reward', 'gift', 'achievement', 'event'
is_equipped BOOLEAN DEFAULT false
```
### 2.16 ECONOMY_TRANSACTIONS
```sql
-- Table: economy_transactions
-- RLS: Players see only their own
id UUID PK DEFAULT uuid_generate_v4()
player_id UUID NOT NULL -> profiles(id) ON DELETE CASCADE
type TEXT NOT NULL CHECK (IN ('credit', 'debit'))
currency TEXT NOT NULL CHECK (IN ('coins', 'gems', 'premium_currency'))
amount INT NOT NULL
balance_after INT NOT NULL
reason TEXT NOT NULL -- 'match_win', 'tournament_prize', 'purchase', 'daily_reward', 'level_up', etc.
source_id TEXT -- Reference to match_id, tournament_id, etc.
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.17 LEADERBOARDS
```sql
-- Table: leaderboards
-- RLS: SELECT open to everyone
-- UNIQUE: (player_id, game_key, time_control_type, period, period_key)
id UUID PK DEFAULT gen_random_uuid()
player_id UUID -> profiles(id) ON DELETE CASCADE
game_key TEXT NOT NULL DEFAULT 'chess'
time_control_type TEXT NOT NULL -- 'bullet', 'blitz', 'rapid', 'classical'
period TEXT NOT NULL -- 'daily', 'weekly', 'monthly', 'all_time'
period_key TEXT -- '2026-05-26', '2026-W22', '2026-05'
rank INT
rating INT NOT NULL
games_played INT DEFAULT 0
win_rate NUMERIC(5,2)
country_code CHAR(3)
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.18 ACHIEVEMENTS
```sql
-- Table: achievements
-- RLS: SELECT open to everyone
id TEXT PK -- e.g. 'first_win', 'win_streak_10', 'tournament_champion'
name TEXT NOT NULL
name_ar TEXT
description TEXT
description_ar TEXT
icon_url TEXT
category TEXT NOT NULL -- 'games', 'social', 'tournaments', 'progression', 'collection'
tier INT DEFAULT 1 -- 1=bronze, 2=silver, 3=gold
condition JSONB NOT NULL -- e.g. {"type":"wins","count":100,"game":"chess"}
xp_reward INT DEFAULT 0
coins_reward INT DEFAULT 0
cosmetic_reward TEXT -- cosmetic_id
is_hidden BOOLEAN DEFAULT false
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.19 PLAYER_ACHIEVEMENTS
```sql
-- Table: player_achievements
-- RLS: Players see only their own
-- UNIQUE: (player_id, achievement_id)
id UUID PK DEFAULT gen_random_uuid()
player_id UUID -> profiles(id) ON DELETE CASCADE
achievement_id TEXT -> achievements(id)
progress INT DEFAULT 0
completed BOOLEAN DEFAULT false
completed_at TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.20 RATING_HISTORY
```sql
-- Table: rating_history
-- RLS: SELECT open to everyone
id UUID PK DEFAULT gen_random_uuid()
player_id UUID -> profiles(id) ON DELETE CASCADE
game_key TEXT NOT NULL DEFAULT 'chess'
time_control_type TEXT NOT NULL -- 'bullet', 'blitz', 'rapid', 'classical'
rating_before INT NOT NULL
rating_after INT NOT NULL
rating_change INT NOT NULL
match_id UUID -> matches(id)
opponent_id UUID -> profiles(id)
opponent_rating INT
result TEXT -- 'win', 'loss', 'draw'
k_factor INT
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.21 ACTIVITY_FEED
```sql
-- Table: activity_feed
id UUID PK DEFAULT gen_random_uuid()
actor_id UUID -> profiles(id)
action TEXT NOT NULL -- 'won_match', 'joined_tournament', 'leveled_up', 'achievement_unlocked', etc.
target_type TEXT -- 'match', 'tournament', 'achievement', 'player'
target_id UUID
metadata JSONB DEFAULT '{}'
visibility TEXT DEFAULT 'friends' -- 'public', 'friends', 'private'
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.22 XP_LEVELS
```sql
-- Table: xp_levels
-- RLS: SELECT open to everyone
level INT PK
xp_required INT NOT NULL
title TEXT
title_ar TEXT
reward_coins INT DEFAULT 0
reward_cosmetic TEXT -- cosmetic_id
```
### 2.23 PROFILE_FRAMES
```sql
-- Table: profile_frames
id UUID PK DEFAULT gen_random_uuid()
name TEXT NOT NULL
name_ar TEXT
description TEXT
description_ar TEXT
image_url TEXT NOT NULL
thumbnail_url TEXT
category TEXT NOT NULL DEFAULT 'general' CHECK (IN ('general','seasonal','achievement','org','event','premium'))
rarity TEXT DEFAULT 'common' CHECK (IN ('common','uncommon','rare','epic','legendary'))
price_coins INT DEFAULT 0
price_gems INT DEFAULT 0
is_purchasable BOOLEAN DEFAULT true
is_active BOOLEAN DEFAULT true
required_level INT DEFAULT 0
required_achievement_id UUID
org_id UUID -> el3ab_organizations(id) ON DELETE SET NULL
max_supply INT
current_supply INT DEFAULT 0
available_from TIMESTAMPTZ
available_until TIMESTAMPTZ
sort_order INT DEFAULT 0
metadata JSONB DEFAULT '{}'
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.24 PLAYER_FRAMES
```sql
-- Table: player_frames
-- UNIQUE: (player_id, frame_id)
id UUID PK DEFAULT gen_random_uuid()
player_id UUID NOT NULL -> profiles(id) ON DELETE CASCADE
frame_id UUID NOT NULL -> profile_frames(id) ON DELETE CASCADE
acquired_at TIMESTAMPTZ DEFAULT now()
acquisition_type TEXT DEFAULT 'purchase' CHECK (IN ('purchase','reward','gift','achievement','org_membership','admin_grant'))
source_id TEXT
is_equipped BOOLEAN DEFAULT false
```
### 2.25 CLUBS
```sql
-- Table: clubs
-- RLS: SELECT open to everyone
id UUID PK DEFAULT gen_random_uuid()
name TEXT NOT NULL
name_ar TEXT
slug TEXT UNIQUE
description TEXT
logo_url TEXT
banner_url TEXT
owner_id UUID -> profiles(id)
country_code CHAR(3)
max_members INT DEFAULT 100
member_count INT DEFAULT 0
is_public BOOLEAN DEFAULT true
rating_avg INT DEFAULT 1200
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.26 CLUB_MEMBERS
```sql
-- Table: club_members
id UUID PK DEFAULT gen_random_uuid()
club_id UUID -> clubs(id) ON DELETE CASCADE
player_id UUID -> profiles(id) ON DELETE CASCADE
role TEXT DEFAULT 'member' -- owner, admin, member
joined_at TIMESTAMPTZ DEFAULT now()
```
### 2.27 ORG_MEMBERSHIP_APPLICATIONS
```sql
-- Table: org_membership_applications
-- Players submit these to join organizations
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
org_id UUID -> el3ab_organizations(id) ON DELETE CASCADE
player_id UUID -> profiles(id) ON DELETE CASCADE
invite_code_id UUID -> org_invite_codes(id)
document_url TEXT -- proof document (uploaded to membership-proofs bucket)
document_type TEXT
notes TEXT -- player's application message
status TEXT DEFAULT 'pending' -- pending, approved, rejected
reviewed_by UUID -> profiles(id)
reviewed_at TIMESTAMPTZ
rejection_reason TEXT
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.28 ORG_INVITE_CODES
```sql
-- Table: org_invite_codes
-- Used by players to join organizations via invite link/code
id UUID PK DEFAULT gen_random_uuid()
org_id UUID -> el3ab_organizations(id) ON DELETE CASCADE
code TEXT NOT NULL UNIQUE -- the shareable code string
code_type TEXT DEFAULT 'single' -- 'single', 'multi'
max_uses INT
current_uses INT DEFAULT 0
created_by UUID -> profiles(id)
expires_at TIMESTAMPTZ
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.29 TOURNAMENT_PLAYERS (Swiss API — internal)
```sql
-- NOTE: This table is used by the Swiss API system (different from tournament_registrations)
-- Used for detailed player data within Swiss-paired tournaments
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID NOT NULL -> tournaments(id) ON DELETE CASCADE
organization_id UUID NOT NULL -> organizations(id) ON DELETE CASCADE
category_id UUID -> categories(id)
user_id UUID -> auth.users(id)
start_number INT NOT NULL
name TEXT NOT NULL
birth_date DATE
country_code TEXT
city TEXT
club TEXT
title TEXT -- FIDE title: GM, IM, FM, etc.
fide_id TEXT
fide_rating_standard INT
fide_rating_rapid INT
fide_rating_blitz INT
national_id TEXT
national_rating INT
is_active BOOLEAN DEFAULT true
withdrawn_after_round INT
rounds_excluded INT[] DEFAULT '{}'
received_bye_in_rounds INT[] DEFAULT '{}'
total_points NUMERIC(4,1) DEFAULT 0
float_history JSONB DEFAULT '[]'
color_history JSONB DEFAULT '[]'
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.30 TOURNAMENT_BRACKETS
```sql
-- Table: tournament_brackets
-- Container for bracket matches (1 bracket per elimination phase, or per group)
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID NOT NULL -> el3ab_tournaments(id) ON DELETE CASCADE
phase_id UUID NOT NULL -> tournament_phases(id) ON DELETE CASCADE
bracket_type TEXT NOT NULL CHECK (IN ('winners','losers','consolation','group'))
group_name TEXT -- e.g. 'Group A', 'Group B'
total_rounds INT NOT NULL
current_round INT DEFAULT 0
config JSONB DEFAULT '{}'
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.31 TOURNAMENT_PRIZE_PAYOUTS
```sql
-- Table: tournament_prize_payouts
-- Records actual prize payments to players
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID -> el3ab_tournaments(id) ON DELETE CASCADE
player_id UUID -> profiles(id)
place INT NOT NULL -- 1st, 2nd, 3rd...
coins_awarded INT DEFAULT 0
gems_awarded INT DEFAULT 0
cosmetic_awarded TEXT -- cosmetic_id if prize is a cosmetic
paid_at TIMESTAMPTZ DEFAULT now()
```
### 2.32 TOURNAMENT_SPONSORSHIPS
```sql
-- Table: tournament_sponsorships
-- Links sponsors to tournaments
id UUID PK DEFAULT gen_random_uuid()
sponsor_id UUID -> sponsors(id)
tournament_id UUID -> el3ab_tournaments(id)
contribution_coins INT DEFAULT 0 -- coins sponsor added to prize pool
branding_placement JSONB DEFAULT '{}' -- where sponsor logo appears
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.33 TOURNAMENT_FORMAT_REGISTRY
```sql
-- Table: tournament_format_registry
-- Defines which formats are available and which games support them
-- RLS: SELECT open to everyone
format_key TEXT PK -- 'swiss', 'round_robin', etc.
name TEXT NOT NULL
name_ar TEXT NOT NULL
description TEXT
description_ar TEXT
supported_games TEXT[] DEFAULT '{}' -- which games can use this format
config_schema JSONB DEFAULT '{}'
is_enabled BOOLEAN DEFAULT true
sort_order INT DEFAULT 0
created_at TIMESTAMPTZ DEFAULT now()
```
**Current format support matrix:**
| Format | Supported Games |
|--------|----------------|
| swiss | chess, backgammon, dominoes |
| round_robin | chess, backgammon, dominoes |
| single_elimination | chess, backgammon, dominoes, ludo, trivia |
| double_elimination | chess, backgammon |
| swiss_to_bracket | chess, backgammon |
| arena | chess, backgammon, ludo, trivia |
| team_battle | chess, trivia |
### 2.34 SPONSORS
```sql
-- Table: sponsors
-- Companies that sponsor tournaments
id UUID PK DEFAULT gen_random_uuid()
name TEXT NOT NULL
name_ar TEXT
logo_url TEXT
website TEXT
contact_email TEXT
tier TEXT DEFAULT 'bronze' -- bronze, silver, gold, platinum
banner_728x90_url TEXT -- leaderboard banner
banner_300x250_url TEXT -- medium rectangle banner
interstitial_url TEXT -- full-page ad
brand_color TEXT -- hex color for branding
is_active BOOLEAN DEFAULT true
contract_starts_at TIMESTAMPTZ
contract_ends_at TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.35 CHARITIES
```sql
-- Table: charities
-- Verified charitable organizations that receive tournament donations
id UUID PK DEFAULT gen_random_uuid()
name TEXT NOT NULL
name_ar TEXT
description TEXT
description_ar TEXT
logo_url TEXT
website TEXT
country_code CHAR(3)
registration_number TEXT -- official charity registration number
is_verified BOOLEAN DEFAULT false
verified_by UUID -> profiles(id)
total_received_coins INT DEFAULT 0 -- running total of all donations
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.36 CHARITY_DONATIONS
```sql
-- Table: charity_donations
-- Records donations from tournaments to charities
id UUID PK DEFAULT gen_random_uuid()
charity_id UUID -> charities(id)
tournament_id UUID -> el3ab_tournaments(id)
donor_id UUID -> profiles(id) -- null for tournament-pool donations
amount_coins INT NOT NULL
source TEXT NOT NULL -- 'tournament_pool', 'player_direct', etc.
created_at TIMESTAMPTZ DEFAULT now()
```
### 2.37 EVENTS (Swiss API)
```sql
-- Table: events
-- Swiss API event container — a tournament belongs to an event
-- Used internally by the Swiss pairing engine
id UUID PK DEFAULT gen_random_uuid()
organization_id UUID NOT NULL -> organizations(id) ON DELETE CASCADE
name TEXT NOT NULL
description TEXT
venue TEXT
city TEXT
country_code TEXT
date_start DATE NOT NULL
date_end DATE NOT NULL
time_control_description TEXT
time_control_type time_control_type NOT NULL DEFAULT 'standard'
chief_arbiter_id UUID -> auth.users(id)
deputy_arbiter_ids UUID[] DEFAULT '{}'
status TEXT DEFAULT 'draft'
is_fide_rated BOOLEAN DEFAULT false
fide_event_id TEXT
metadata TEXT DEFAULT '{}'
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.38 TOURNAMENTS (Swiss API Engine)
```sql
-- Table: tournaments
-- Internal Swiss pairing engine table — NOT the player-facing el3ab_tournaments
-- Used by the admin panel for generating pairings
id UUID PK DEFAULT gen_random_uuid()
event_id UUID NOT NULL -> events(id) ON DELETE CASCADE
organization_id UUID NOT NULL -> organizations(id) ON DELETE CASCADE
name TEXT NOT NULL
tournament_type tournament_type NOT NULL DEFAULT 'swiss' -- swiss, round_robin, double_round_robin
status tournament_status NOT NULL DEFAULT 'draft'
rounds_number INT NOT NULL CHECK (1-30)
current_round INT DEFAULT 0
table_start_number INT DEFAULT 1
pairing_system TEXT DEFAULT 'dutch'
acceleration_method TEXT
acceleration_rounds INT DEFAULT 0
color_allocation_rule TEXT DEFAULT 'equalise_alternate'
initial_ordering JSONB NOT NULL DEFAULT '["fide_rating", "name"]'
tiebreak_rules JSONB NOT NULL DEFAULT '["buchholz_cut_1", "buchholz", "sonneborn_berger"]'
rated_minimum INT
rated_maximum INT
k_factor_override INT
max_players INT DEFAULT 200
bye_value NUMERIC(3,2) DEFAULT 1.00
max_byes_per_player INT DEFAULT 1
registration_opens_at TIMESTAMPTZ
registration_closes_at TIMESTAMPTZ
started_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.39 ROUNDS (Swiss API)
```sql
-- Table: rounds
-- Individual rounds within a Swiss API tournament
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID NOT NULL -> tournaments(id) ON DELETE CASCADE
organization_id UUID NOT NULL -> organizations(id) ON DELETE CASCADE
round_number INT NOT NULL CHECK (>= 1)
status round_status NOT NULL DEFAULT 'pending' -- pending, paired, in_progress, completed
scheduled_at TIMESTAMPTZ
started_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
paired_at TIMESTAMPTZ
paired_by UUID -> auth.users(id)
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.40 CATEGORIES (Swiss API)
```sql
-- Table: categories
-- Sub-categories within a tournament (e.g. U18, Women, etc.)
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID NOT NULL -> tournaments(id) ON DELETE CASCADE
organization_id UUID NOT NULL -> organizations(id) ON DELETE CASCADE
name TEXT NOT NULL
abbreviation TEXT
description TEXT
min_rating INT
max_rating INT
min_age INT
max_age INT
gender TEXT -- 'male', 'female', null=open
sort_order INT DEFAULT 0
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.41 STANDINGS (Swiss API)
```sql
-- Table: standings
-- UNIQUE: (tournament_id, round_id, player_id)
id UUID PK DEFAULT gen_random_uuid()
tournament_id UUID NOT NULL -> tournaments(id) ON DELETE CASCADE
round_id UUID NOT NULL -> rounds(id) ON DELETE CASCADE
player_id UUID NOT NULL -> tournament_players(id) ON DELETE CASCADE
organization_id UUID NOT NULL -> organizations(id) ON DELETE CASCADE
round_number INT NOT NULL
rank_overall INT NOT NULL
rank_category INT
category_id UUID -> categories(id)
points NUMERIC(4,1) NOT NULL DEFAULT 0
tiebreak_values JSONB NOT NULL DEFAULT '[]'
games_played INT DEFAULT 0
wins INT DEFAULT 0
draws INT DEFAULT 0
losses INT DEFAULT 0
byes INT DEFAULT 0
rating_change NUMERIC(5,2)
performance_rating INT
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
### 2.42 PAIRINGS (Swiss API)
```sql
-- Table: pairings
-- UNIQUE: (round_id, board_number)
id UUID PK DEFAULT gen_random_uuid()
round_id UUID NOT NULL -> rounds(id) ON DELETE CASCADE
tournament_id UUID NOT NULL -> tournaments(id) ON DELETE CASCADE
organization_id UUID NOT NULL -> organizations(id) ON DELETE CASCADE
board_number INT NOT NULL
white_player_id UUID NOT NULL -> tournament_players(id)
black_player_id UUID -> tournament_players(id)
result game_result NOT NULL DEFAULT 'not_played'
white_points NUMERIC(3,2) DEFAULT 0
black_points NUMERIC(3,2) DEFAULT 0
is_forfeit BOOLEAN DEFAULT false
is_bye BOOLEAN DEFAULT false
result_entered_by UUID -> auth.users(id)
result_entered_at TIMESTAMPTZ
result_confirmed BOOLEAN DEFAULT false
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
```
---
## 3. ENUM TYPES (All 27)
### Player/Platform Enums
```sql
-- platform_role (11 values)
superadmin, platform_admin, org_owner, org_admin, tournament_director,
arbiter, content_manager, match_moderator, sponsor_admin, player, spectator
-- user_role (Swiss API roles, 5 values)
super_admin, org_admin, arbiter, player, spectator
```
### Match Enums
```sql
-- match_status (7 values)
waiting, ready, in_progress, paused, completed, aborted, abandoned
-- match_result (15 values)
white_wins, black_wins, draw, white_timeout, black_timeout,
white_resign, black_resign, white_abandon, black_abandon,
stalemate, insufficient_material, threefold_repetition,
fifty_moves, mutual_draw, aborted
-- game_result (Swiss pairings, 10 values)
white_wins, black_wins, draw, white_forfeit, black_forfeit,
double_forfeit, bye_full, bye_half, bye_zero, not_played
-- color (2 values)
white, black
```
### Time Control Enums
```sql
-- time_control (14 values)
bullet_1_0, bullet_1_1, bullet_2_1,
blitz_3_0, blitz_3_2, blitz_5_0, blitz_5_3,
rapid_10_0, rapid_10_5, rapid_15_10, rapid_30_0,
classical_60_0, classical_90_30,
custom
-- time_control_type (4 values)
standard, rapid, blitz, bullet
```
### Tournament Enums
```sql
-- tournament_format (7 values)
swiss, round_robin, single_elimination, double_elimination,
swiss_to_bracket, arena, team_battle
-- tournament_status (5 values)
draft, registration, in_progress, completed, cancelled
-- tournament_type (Swiss API, 3 values)
swiss, round_robin, double_round_robin
-- round_status (4 values)
pending, paired, in_progress, completed
-- tiebreak_type (11 values)
buchholz, buchholz_cut_1, buchholz_median, sonneborn_berger,
direct_encounter, number_of_wins, number_of_blacks, koya,
progressive_score, average_rating_opponents, performance_rating
```
### Cosmetic Enums
```sql
-- cosmetic_type (9 values)
avatar_frame, board_theme, piece_set, profile_banner,
chat_emoji, victory_animation, title_badge, trail_effect, sound_pack
-- cosmetic_rarity (5 values)
common, uncommon, rare, epic, legendary
```
### Organization Enums
```sql
-- org_membership_status (3 values)
active, suspended, invited
-- audit_action (9 values)
create, update, delete, pair_round, unpair_round,
enter_result, modify_result, generate_standings, export_trf
```
---
## 4. GAME PLUGINS (5 Games)
| game_key | name | name_ar | min_players | max_players | supports_ranked | supports_tournament | supports_bot | supports_spectator | supports_arena |
|----------|------|---------|-------------|-------------|-----------------|--------------------|--------------|--------------------|----------------|
| chess | Chess | شطرنج | 2 | 2 | true | true | true | true | true |
| backgammon | Backgammon | طاولة | 2 | 2 | true | true | false | true | true |
| dominoes | Dominoes | دومينو | 2 | 4 | true | true | false | true | false |
| ludo | Ludo | لودو | 2 | 4 | true | false | false | true | true |
| trivia | Trivia Party | تريفيا بارتي | 2 | 8 | true | true | false | true | true |
All games use `elo` rating system. All are enabled and not beta.
---
## 5. XP LEVELS (15 Levels)
| Level | XP Required | Title (EN) | Title (AR) | Coin Reward |
|-------|-------------|------------|------------|-------------|
| 1 | 0 | Beginner | مبتدئ | 0 |
| 2 | 100 | Pawn | بيدق | 50 |
| 3 | 300 | Pawn II | بيدق ٢ | 50 |
| 4 | 600 | Knight | حصان | 100 |
| 5 | 1000 | Knight II | حصان ٢ | 100 |
| 6 | 1500 | Bishop | فيل | 150 |
| 7 | 2100 | Bishop II | فيل ٢ | 150 |
| 8 | 2800 | Rook | قلعة | 200 |
| 9 | 3600 | Rook II | قلعة ٢ | 200 |
| 10 | 4500 | Queen | ملكة | 300 |
| 11 | 5500 | Queen II | ملكة ٢ | 300 |
| 12 | 6600 | King | ملك | 500 |
| 13 | 7800 | King II | ملك ٢ | 500 |
| 14 | 9100 | Master | أستاذ | 750 |
| 15 | 10500 | Grandmaster | غراندماستر | 1000 |
---
## 6. SYSTEM CONFIG (15 Settings)
| Key | Value | Description | Category |
|-----|-------|-------------|----------|
| platform_name | "EL3AB" | Platform name | general |
| platform_name_ar | "إل٣اب" | Platform name in Arabic | general |
| elo_default_rating | 1200 | Default starting rating | rating |
| elo_k_factor_new | 40 | K-factor for < 30 games | rating |
| elo_k_factor_established | 20 | K-factor for established players | rating |
| elo_k_factor_master | 10 | K-factor for 2400+ players | rating |
| matchmaking_range_expansion | 10 | Rating range expansion per second | matchmaking |
| matchmaking_max_wait_sec | 60 | Maximum matchmaking wait time | matchmaking |
| coins_per_win_ranked | 25 | Coins earned per ranked win | economy |
| coins_per_win_casual | 10 | Coins earned per casual win | economy |
| xp_per_game | 50 | XP earned per game played | economy |
| xp_per_win_bonus | 30 | Bonus XP for winning | economy |
| tournament_max_players | 256 | Maximum players per tournament | tournaments |
| anticheat_cpl_threshold | 15 | Average centipawn loss threshold | anticheat |
| anticheat_correlation_threshold | 85 | Engine correlation % to auto-flag | anticheat |
---
## 7. STORAGE BUCKETS (9 Buckets)
| Bucket ID | Purpose | Public URL Pattern |
|-----------|---------|-------------------|
| avatars | Player avatar images | `/storage/v1/object/public/avatars/{user_id}/{filename}` |
| profile-images | Additional profile images | `/storage/v1/object/public/profile-images/{user_id}/{filename}` |
| profile-frames | Frame image assets | `/storage/v1/object/public/profile-frames/{filename}` |
| org-logos | Organization logos | `/storage/v1/object/public/org-logos/{org_id}/{filename}` |
| org-banners | Organization banners | `/storage/v1/object/public/org-banners/{org_id}/{filename}` |
| org-media | Organization media gallery | `/storage/v1/object/public/org-media/{org_id}/{filename}` |
| org-content-attachments | Org content post attachments | `/storage/v1/object/public/org-content-attachments/{org_id}/{filename}` |
| membership-proofs | Membership proof documents | `/storage/v1/object/public/membership-proofs/{filename}` |
| chat-attachments | Chat media (images in chat) | `/storage/v1/object/public/chat-attachments/{channel_id}/{filename}` |
---
## 8. RPC FUNCTIONS (Custom)
### 8.1 handle_new_user() — TRIGGER
Triggered on `auth.users` INSERT. Creates profile:
```sql
INSERT INTO profiles (id, username, display_name, preferred_language)
VALUES (
NEW.id,
COALESCE(NEW.raw_user_meta_data->>'username', 'user_' || substr(NEW.id::text, 1, 8)),
COALESCE(NEW.raw_user_meta_data->>'display_name', 'لاعب جديد'),
'ar'
);
```
### 8.2 check_app_version(client_version TEXT) → JSON
Returns whether client needs update:
```json
{
"min_app_version": "1.0.0",
"google_play_link": "...",
"app_store_link": "...",
"shouldupdate": true/false
}
```
### 8.3 register_tournament_player(p_tournament_id INT, p_player_auth_id TEXT) → JSON
Registers player in tournament. Checks:
- Tournament status must be `'registration_open'`
- `current_players < max_players`
Returns: `{"status":"success","message":"تم التسجيل بنجاح"}` or error.
### 8.4 cancel_tournament_registration(p_tournament_id INT) → JSON
Cancels player's registration. Decrements `current_players`.
### 8.5 get_all_tournaments() → JSON
Returns all tournaments with embedded players and brackets data. Uses the `tournaments` table (legacy).
### 8.6 get_tournament_by_id(p_id INT) → JSON
Returns single tournament with embedded players and brackets.
### 8.7 get_tournament_room(p_tournament_id INT, p_bracket_id INT) → JSON
Returns lobby_id for a bracket match. Checks if player is late based on `max_join_delay`.
### 8.8 check_opponent_timeout(p_bracket_id INT, p_user_numeric_id INT) → JSON
Checks if opponent has timed out for a bracket match.
Returns: `{"status":"opponent_timeout"}`, `{"status":"waiting"}`, or `{"status":"already_finished"}`.
### 8.9 get_friends_by_auth_ids(auth_ids TEXT[]) → JSON
Returns friend profiles for given auth IDs:
```json
{"status":"success", "friendsData":[{"FriendAuthId":"...","FriendName":"...","FriendClub":"...","FriendImage":"..."}]}
```
### 8.10 get_user_org_ids() → UUID[]
Returns array of organization IDs where current user is an active member.
### 8.11 user_has_role_in_org(p_org_id UUID, p_roles user_role[]) → BOOLEAN
Checks if current user has any of the specified roles in the given organization.
---
## 9. ROW LEVEL SECURITY (RLS) POLICIES
### Public Read (No Auth Required)
- `profiles` — SELECT open to everyone
- `matches` — SELECT open to everyone
- `el3ab_tournaments` — SELECT open to everyone
- `el3ab_organizations` — SELECT where `is_active = true`
- `achievements` — SELECT open to everyone
- `cosmetics` — SELECT open to everyone
- `leaderboards` — SELECT open to everyone
- `rating_history` — SELECT open to everyone
- `clubs` — SELECT open to everyone
- `xp_levels` — SELECT open to everyone
### Authenticated User's Own Data
- `notifications` — SELECT/UPDATE only `user_id = auth.uid()`
- `economy_transactions` — SELECT only `player_id = auth.uid()`
- `player_cosmetics` — SELECT only `player_id = auth.uid()`
- `player_achievements` — SELECT only `player_id = auth.uid()`
- `friendships` — SELECT only where user is requester or addressee
- `matchmaking_queue` — full CRUD only for `player_id = auth.uid()`
### Authenticated Mutations
- `profiles` — UPDATE only own (auth.uid() = id)
- `matches` — INSERT/UPDATE only if player is white_player or black_player
- `friendships` — INSERT only as requester
- `chat_messages` — INSERT only as sender
### Admin-Protected
- `el3ab_tournaments` — INSERT requires platform role (superadmin, platform_admin, tournament_director, org_owner, org_admin)
- `el3ab_organizations` — UPDATE requires org_admin role
### Service Role Only
- `player_achievements` — full access for service_role
- `bracket_matches` — full access for service_role
- `tournament_phases` — full access for service_role
- `profile_frames` — full access for service_role
- `player_frames` — full access for service_role
---
## 10. REALTIME SUBSCRIPTIONS
Tables with Realtime enabled (via `supabase_realtime` publication):
- `profiles`
- `matches`
- `matchmaking_queue`
- `friendships`
- `notifications`
- `chat_messages`
- `el3ab_tournaments`
- `el3ab_organizations`
- `tournament_registrations`
- `el3ab_tournament_rounds`
- `bracket_matches`
- `tournament_phases`
- `leaderboards`
- `rating_history`
- `player_cosmetics`
- `player_achievements`
- `economy_transactions`
- `activity_feed`
- `clubs`
- `cosmetics`
- `achievements`
- `xp_levels`
- `org_members`
- `profile_frames`
- `player_frames`
---
## 11. SUPABASE AUTH SIGNUP
When signing up a new user, pass metadata:
```javascript
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
data: {
username: 'chosen_username',
display_name: 'اسم اللاعب'
}
}
});
```
The `handle_new_user()` trigger automatically creates the profile row.
---
## 12. KEY RELATIONSHIPS DIAGRAM
```
auth.users (Supabase Auth)
└── profiles (1:1, id = auth.users.id)
├── matches (white_player_id / black_player_id)
├── matchmaking_queue (player_id)
├── friendships (requester_id / addressee_id)
├── notifications (user_id)
├── chat_messages (sender_id)
├── player_cosmetics (player_id)
├── player_achievements (player_id)
├── economy_transactions (player_id)
├── leaderboards (player_id)
├── rating_history (player_id)
├── activity_feed (actor_id)
├── player_frames (player_id)
├── club_members (player_id)
├── org_members (player_id)
└── tournament_registrations (player_id)
el3ab_tournaments
├── tournament_registrations (tournament_id)
├── el3ab_tournament_rounds (tournament_id)
├── tournament_phases (tournament_id)
│ ├── tournament_brackets (phase_id)
│ │ └── bracket_matches (bracket_id)
│ └── bracket_matches (phase_id)
├── tournament_prize_payouts (tournament_id)
├── tournament_sponsorships (tournament_id) → sponsors
├── charity_donations (tournament_id) → charities
├── tournament_ad_slots (tournament_id)
├── tournament_announcements (tournament_id)
├── tournament_media (tournament_id)
└── tournament_page_views (tournament_id)
tournaments (Swiss API internal)
├── events (event_id) → organizations
├── rounds (tournament_id)
│ ├── pairings (round_id)
│ └── standings (round_id)
├── tournament_players (tournament_id)
│ ├── pairings (white_player_id / black_player_id)
│ └── standings (player_id)
└── categories (tournament_id)
el3ab_organizations
├── org_members (org_id)
├── org_events (org_id)
├── org_chat_channels (org_id)
├── org_chat_messages (org_id)
├── org_announcements (org_id)
├── org_content (org_id)
├── org_challenges (challenger/challenged)
├── org_partnerships (org_a/org_b)
├── org_rosters (org_id)
├── org_treasury (org_id)
├── org_loyalty_rewards (org_id)
└── el3ab_tournaments (org_id)
```
---
## 13. API PATTERNS
### Supabase REST API (PostgREST)
All tables accessible via:
```
GET/POST/PATCH/DELETE {SUPABASE_URL}/rest/v1/{table_name}
Headers:
apikey: {ANON_KEY or SERVICE_ROLE_KEY}
Authorization: Bearer {user_jwt_or_service_role_key}
Content-Type: application/json
Prefer: return=representation (for mutations)
```
### Common Query Patterns
```javascript
// Get player profile
const { data } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single();
// Get matches for a player
const { data } = await supabase
.from('matches')
.select('*')
.or(`white_player_id.eq.${userId},black_player_id.eq.${userId}`)
.order('created_at', { ascending: false });
// Get leaderboard
const { data } = await supabase
.from('leaderboards')
.select('*, profiles(username, display_name, avatar_url, country_code)')
.eq('game_key', 'chess')
.eq('time_control_type', 'blitz')
.eq('period', 'weekly')
.order('rank', { ascending: true })
.limit(100);
// Join matchmaking queue
const { data } = await supabase
.from('matchmaking_queue')
.insert({
player_id: userId,
game_key: 'chess',
time_control: 'blitz_5_0',
rating: playerRating,
is_rated: true
});
// Subscribe to match updates (Realtime)
supabase
.channel('match-updates')
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'matches',
filter: `id=eq.${matchId}`
}, (payload) => {
// Handle match state update
})
.subscribe();
// Subscribe to notifications
supabase
.channel('my-notifications')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'notifications',
filter: `user_id=eq.${userId}`
}, (payload) => {
// Show notification
})
.subscribe();
// Call RPC function
const { data } = await supabase.rpc('register_tournament_player', {
p_tournament_id: tournamentId,
p_player_auth_id: authId
});
```
---
## 14. TIME CONTROL MAPPINGS
| Enum Value | Initial (ms) | Increment (ms) | Category |
|------------|-------------|----------------|----------|
| bullet_1_0 | 60000 | 0 | bullet |
| bullet_1_1 | 60000 | 1000 | bullet |
| bullet_2_1 | 120000 | 1000 | bullet |
| blitz_3_0 | 180000 | 0 | blitz |
| blitz_3_2 | 180000 | 2000 | blitz |
| blitz_5_0 | 300000 | 0 | blitz |
| blitz_5_3 | 300000 | 3000 | blitz |
| rapid_10_0 | 600000 | 0 | rapid |
| rapid_10_5 | 600000 | 5000 | rapid |
| rapid_15_10 | 900000 | 10000 | rapid |
| rapid_30_0 | 1800000 | 0 | rapid |
| classical_60_0 | 3600000 | 0 | classical |
| classical_90_30 | 5400000 | 30000 | classical |
| custom | varies | varies | varies |
---
## 15. ELO RATING SYSTEM
### Calculation Rules
- **Default Rating**: 1200
- **K-Factor New** (< 30 games): 40
- **K-Factor Established** (30+ games): 20
- **K-Factor Master** (2400+ rating): 10
- **Rating per game type**: `elo_bullet`, `elo_blitz`, `elo_rapid`, `elo_classical`
### Standard ELO Formula
```
Expected = 1 / (1 + 10^((opponent_rating - player_rating) / 400))
New_Rating = Old_Rating + K * (Score - Expected)
Score: 1 (win), 0.5 (draw), 0 (loss)
```
---
## 16. ECONOMY SYSTEM
### Currencies
| Currency | Column in profiles | Earn Methods |
|----------|-------------------|--------------|
| Coins | `coins` | Match wins, daily rewards, level-up, tournaments |
| Gems | `gems` | Rare rewards, premium purchases |
| Premium Currency | `premium_currency` | Real money purchase |
### Earning Rates
| Action | Reward |
|--------|--------|
| Ranked win | 25 coins |
| Casual win | 10 coins |
| Game played (any) | 50 XP |
| Win bonus | +30 XP |
| Level up | varies (50-1000 coins) |
| Daily login | coins + streak bonus |
### Transaction Types
- `credit` / `debit`
- Currencies: `coins`, `gems`, `premium_currency`
- Reasons: `match_win`, `tournament_prize`, `purchase`, `daily_reward`, `level_up`, `refund`, `gift`, `admin_grant`
---
## 17. MATCHMAKING ALGORITHM
1. Player enters queue with: `game_key`, `time_control`, `rating`, `is_rated`
2. Rating range starts at `±50` (or custom `rating_range_min/max`)
3. Expands by `range_expansion_per_sec` (default: 10) every second
4. Max wait: `max_wait_seconds` (default: 60)
5. Respects `block_list` (UUID array of blocked players)
6. When matched: both entries get `status='matched'`, `matched_with` set, `match_id` created
---
## 18. TOURNAMENT FORMATS
| Format | Description |
|--------|-------------|
| `swiss` | Swiss pairing system, configurable rounds |
| `round_robin` | Everyone plays everyone |
| `single_elimination` | Bracket, lose once = out |
| `double_elimination` | Winners + losers bracket |
| `swiss_to_bracket` | Swiss group phase → elimination bracket |
| `arena` | Continuous play, points for speed |
| `team_battle` | Organization vs organization |
### Multi-Phase Tournaments
- `tournament_mode`: `'single'` or `'multi_phase'`
- `phase_config` JSONB array defines each phase
- `tournament_phases` table tracks individual phase state
- `advancement_rule` JSONB defines how players advance between phases
---
## 19. ORGANIZATION TIERS
| Tier | Max Members | Max Tournaments | Features |
|------|-------------|-----------------|----------|
| free | 50 | 3 | Basic |
| bronze | 100 | 5 | + Custom branding |
| silver | 250 | 10 | + Revenue share |
| gold | 500 | 25 | + Priority support |
| diamond | Unlimited | Unlimited | + API access, white-label |
---
## 20. ALL 104 TABLE NAMES
### Core Player (7)
`profiles`, `friendships`, `notifications`, `activity_feed`, `player_cosmetics`, `player_achievements`, `player_frames`
### Matches & Matchmaking (3)
`matches`, `matchmaking_queue`, `rating_history`
### Domino Game (2) — NEW
`domino_matches`, `domino_queue`
### Ludo Game (2) — NEW
`ludo_matches`, `ludo_queue`
### Economy & Progression (4)
`economy_transactions`, `leaderboards`, `cosmetics`, `xp_levels`
### Cosmetics & Frames (3)
`cosmetic_type_registry`, `profile_frames`, `player_loyalty_claims`
### Chat (1)
`chat_messages`
### Achievements (2)
`achievements`, `player_achievements`
### Tournaments (El3ab System) (9)
`el3ab_tournaments`, `el3ab_tournament_rounds`, `tournament_registrations`, `tournament_phases`, `bracket_matches`, `tournament_ad_slots`, `tournament_announcements`, `tournament_media`, `tournament_page_views`
### Tournaments (Swiss API) (7)
`tournaments`, `tournament_players`, `rounds`, `pairings`, `standings`, `tournament_brackets`, `tournament_format_registry`
### Tournaments (Misc) (3)
`tournament_prize_payouts`, `tournament_sponsorships`, `categories`
### Organizations (30)
`el3ab_organizations`, `org_members`, `org_memberships`, `org_achievements`, `org_activity_log`, `org_announcements`, `org_asset_overrides`, `org_challenges`, `org_chat_channels`, `org_chat_messages`, `org_chat_moderation`, `org_content`, `org_events`, `org_event_participants`, `org_invite_codes`, `org_invite_links`, `org_invite_uses`, `org_leaderboards`, `org_loyalty_rewards`, `org_media`, `org_member_spotlights`, `org_membership_applications`, `org_partnerships`, `org_player_transfers`, `org_recruitment_posts`, `org_referrals`, `org_rosters`, `org_roster_players`, `org_seasonal_rankings`, `org_theme_overrides`, `org_training_sessions`, `org_treasury`, `org_treasury_transactions`
### Clubs (2)
`clubs`, `club_members`
### Games (2)
`game_plugins`, `game_theme_overrides`
### Platform Admin (11) — includes theme_settings
`admin_users`, `platform_roles`, `platform_assets`, `platform_branding`, `platform_theme`, `system_config`, `audit_log`, `audit_logs`, `approval_requests`, `workflow_rules`, `theme_settings`
### Sponsors & Charities (7)
`sponsors`, `charities`, `charity_donations`, `ad_campaigns`, `ad_creatives`, `ad_impressions`, `ad_slots`
### Anti-Cheat (1)
`cheat_reports`
### Features & Events (3)
`feature_flags`, `events`, `trivia_questions`
### Internal (2)
`_migrations`, `organizations` (Swiss API orgs)
---
## 21. IMPORTANT NOTES FOR PLAYER APP DEVELOPMENT
### Architecture Decisions
1. **Supabase Auth** handles all authentication — email/password and OAuth
2. **PostgREST** (Supabase REST API) is the primary data layer — use `@supabase/supabase-js`
3. **Realtime** for live updates (matches, chat, notifications, matchmaking)
4. **RLS policies** enforce security — the app uses the ANON key + user JWT
5. **Service Role key** should NEVER be in client code — only for admin/backend operations
6. **Arabic-first**: All user-facing strings should have `_ar` variants, UI is RTL
### Player App Scope (what the app needs to do)
- Sign up / login (email + social OAuth)
- View/edit profile
- Play matches (chess, backgammon, dominoes, ludo, trivia)
- Matchmaking queue with rating-based pairing
- Friend system (request, accept, block)
- Chat (match chat, direct messages)
- View/join tournaments
- Leaderboards per game/time-control/period
- Cosmetics shop (buy with coins/gems)
- Achievements tracking
- Activity feed
- Notifications
- Organization membership
- Daily rewards / streaks
- Rating history graphs
### What NOT to build in the player app
- Tournament pairing/management (admin panel handles this)
- Organization creation/management (admin panel)
- Content moderation (admin panel)
- Advertising management (admin panel)
- System config changes (admin panel)
### Two Tournament Systems
There are TWO tournament systems in the database:
1. **El3ab Tournaments** (`el3ab_tournaments`, `el3ab_tournament_rounds`, `tournament_registrations`) — the main system used by the player app for registration, viewing, and live pages
2. **Swiss API Tournaments** (`tournaments`, `tournament_players`, `rounds`, `pairings`, `standings`) — the backend pairing engine used by the admin panel
The player app should primarily interact with the `el3ab_tournaments` system for browsing/registering. For detailed standings/pairings during a live tournament, data flows: Swiss API → admin enters results → el3ab_tournament_rounds.pairings/results JSONB.
### Organization Join Flow (Player Side)
1. Player discovers org (browse, invite link, invite code)
2. If invite code: validate via `org_invite_codes` (check `is_active`, `expires_at`, `current_uses < max_uses`)
3. Player submits `org_membership_applications` with optional document upload to `membership-proofs` bucket
4. Admin approves → player gets added to `org_members` + receives notification
5. Player can also join directly if org has an open invite link
### Realtime Channels to Subscribe
- `matches` — live game state updates during play
- `matchmaking_queue` — matched_with notification
- `notifications` — push notification triggers
- `chat_messages` — live chat
- `el3ab_tournaments` — status changes (registration → in_progress → completed)
- `el3ab_tournament_rounds` — round state changes
- `friendships` — friend request received
- `org_membership_applications` — application status changes (pending → approved/rejected)
---
## 22. LIVE TOURNAMENT PUBLIC API
These endpoints are publicly accessible (no auth required):
| Endpoint | Method | Returns |
|----------|--------|---------|
| `/api/live/{id}/data` | GET | Full tournament data (standings, pairings, bracket, players, config) |
| `/api/live/{id}/standings` | GET | Current standings array |
| `/api/live/{id}/pairings` | GET | Current round pairings |
| `/api/live/{id}/bracket` | GET | Bracket tree data |
| `/api/live/{id}/arena` | GET | Arena standings (if arena format) |
| `/api/live/{id}/announcements` | GET | Tournament announcements |
| `/api/live/{id}/results` | GET | Latest results |
| `/api/live/{id}/track` | POST | Analytics beacon (tracks page view) |
Base URL: `https://el3ab-management.caprover.al-arcade.com`
These routes are served by the admin panel PHP app (not Supabase directly). The player app can hit them for live tournament data without authentication.
---
## 23. PLAYER APP RECOMMENDED TECH STACK
The existing admin panel is pure PHP. The player app should be:
- **Mobile**: Flutter or React Native (cross-platform)
- **Web**: React/Next.js or Flutter Web
- **State**: Supabase Realtime subscriptions
- **Auth**: `@supabase/supabase-js` or `supabase_flutter`
- **Game Logic**: Depends on game (chess.js for chess, custom for others)
- **Real-time multiplayer**: Supabase Realtime channels + Presence
---
## 24. GAME-SPECIFIC DATA STRUCTURES
### Chess (game_key: 'chess')
```json
// matches.moves JSONB example:
[
{"from":"e2","to":"e4","san":"e4","ts":1234567890},
{"from":"e7","to":"e5","san":"e5","ts":1234567891}
]
// matches.game_state JSONB:
{
"fen": "current position FEN",
"lastMove": {"from":"e2","to":"e4"},
"inCheck": false,
"drawOffer": null
}
```
### Match moves JSONB format (for all games)
```json
// Generic structure game_state holds game-specific state
// moves[] holds ordered move history
```
---
## 25. CHESS BOT SYSTEM (Stockfish API)
### Connection
| Key | Value |
|-----|-------|
| **API URL** | `https://stockfishapi.caprover.al-arcade.com` |
| **API Key** | `sk-alarc-stockfish-mgmt-2024` (admin only — move/analyze/bots endpoints are public, no key needed) |
### Public Endpoints (No Auth — Player App uses these directly)
| Endpoint | Method | Body | Returns |
|----------|--------|------|---------|
| `/api/chess/bots` | GET | — | List all bots with personalities |
| `/api/chess/move` | POST | `{fen, bot_id}` | Bot's best move for that position |
| `/api/chess/analyze` | POST | `{fen, depth}` | Multi-line analysis (top 3 moves) |
### Admin Endpoints (Requires `X-API-Key` header)
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/manage/bots` | GET | List all bots (admin detail) |
| `/api/manage/bots` | POST | Create new bot |
| `/api/manage/bots/{id}` | GET | Get bot details |
| `/api/manage/bots/{id}` | PATCH | Update bot config |
| `/api/manage/bots/{id}` | DELETE | Delete bot |
| `/api/manage/bots/{id}/portrait` | POST | Upload bot portrait image |
| `/api/manage/pool` | GET | Stockfish engine pool status |
### Bot Data Structure
```json
{
"id": "nour",
"name": "Nour",
"name_ar": "نور المهاجمة",
"style": "aggressive",
"style_ar": "هجومية",
"bio": "Loves to attack! Sacrifices pieces for a shot at your king.",
"bio_ar": "بتحب الهجوم! بتضحي بقطع عشان توصل للملك.",
"elo_min": 1000,
"elo_max": 1200,
"skill_level": 8,
"depth": 10,
"contempt": 50,
"blunder_chance": 0.08,
"think_time_min_ms": 800,
"think_time_max_ms": 3000,
"opening_book": ["kings_gambit", "sicilian_dragon", "evans_gambit"],
"avatar_id": "bot-nour",
"portrait_url": "/portraits/nour.png"
}
```
### All 7 Bots (Production Data)
| ID | Name | Name AR | Style | ELO Range | Skill Level | Depth | Blunder % |
|----|------|---------|-------|-----------|-------------|-------|-----------|
| `amina` | Amina | أمينة المبتدئة | beginner | 400-600 | 1 | 3 | 30% |
| `tarek` | Tarek | طارق المتحفظ | defensive | 800-1000 | 5 | 6 | 15% |
| `nour` | Nour | نور المهاجمة | aggressive | 1000-1200 | 8 | 10 | 8% |
| `omar` | Omar | عمر الاستراتيجي | positional | 1200-1400 | 11 | 12 | 4% |
| `layla` | Layla | ليلى المبدعة | creative | 1400-1600 | 14 | 14 | 2% |
| `ziad` | Ziad | زياد الصلب | solid | 1600-1800 | 17 | 16 | 1% |
| `grandmaster` | Grandmaster Bot | الجراند ماستر | near_perfect | 2000-2200 | 20 | 20 | 0% |
### Move Response Format
```json
// POST /api/chess/move
// Request: {"fen": "...", "bot_id": "nour"}
// Response:
{
"best_move": "e7e5", // UCI format (from+to, e.g. "e2e4", "e1g1" for castling)
"evaluation": -0.32, // Centipawn eval from bot's perspective
"depth": 10, // Search depth used
"nodes": 1307, // Nodes searched
"think_time_ms": 1147, // Actual think time
"pv": "e7e5 d2d4 f8b4..." // Principal variation (space-separated UCI moves)
}
```
### Analysis Response Format
```json
// POST /api/chess/analyze
// Request: {"fen": "...", "depth": 15}
// Response:
{
"depth": 15,
"fen": "...",
"lines": [
{"rank": 1, "move": "e7e5", "evaluation": -0.33, "depth": 15, "pv": "e7e5 g1f3 b8c6..."},
{"rank": 2, "move": "e7e6", "evaluation": -0.36, "depth": 15, "pv": "e7e6 d2d4 d7d5..."},
{"rank": 3, "move": "c7c5", "evaluation": -0.45, "depth": 15, "pv": "c7c5 g1f3 e7e6..."}
]
}
```
### How Bot Matches Work
1. Player selects bot from list (shown with personality, ELO range, style)
2. Player creates match with `match_type: 'bot'`, `bot_id: 'nour'`, `bot_difficulty: 'aggressive'`
3. Player makes a move → app sends current FEN to `/api/chess/move` with `bot_id`
4. API returns bot's reply move → app updates match state
5. Match is stored in `matches` table like any other match (with `bot_id` field set)
6. Bot matches can be rated or unrated depending on player choice
7. Portrait URL: `https://stockfishapi.caprover.al-arcade.com/portraits/{bot_id}.png`
---
## 26. NOTIFICATION TYPES
| Type | When | Data |
|------|------|------|
| `friend_request` | Someone sends friend request | `{sender_id, sender_name}` |
| `friend_accepted` | Friend request accepted | `{friend_id, friend_name}` |
| `match_invite` | Invited to play | `{match_id, game_key, sender_id}` |
| `match_result` | Game completed | `{match_id, result, rating_change}` |
| `tournament_start` | Tournament starting | `{tournament_id, tournament_name}` |
| `tournament_round` | New round paired | `{tournament_id, round_number}` |
| `achievement_unlocked` | Achievement completed | `{achievement_id, achievement_name}` |
| `level_up` | Player leveled up | `{new_level, title}` |
| `daily_reward` | Daily reward available | `{coins, streak}` |
| `org_invite` | Invited to organization | `{org_id, org_name, inviter_id}` |
| `system` | System announcement | `{message}` |
---
## 27. DOMINO GAME SYSTEM (Dedicated Tables)
Domino does NOT use the generic `matches` table. It has its own complete game state system.
### domino_matches
```sql
-- Self-contained domino game — all state in one row
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
room_code VARCHAR(6) -- shareable room code for friends to join
status VARCHAR(20) DEFAULT 'waiting' -- waiting, in_progress, completed
mode VARCHAR(20) DEFAULT '2p' -- '2p' or '4p_teams'
player_count INT DEFAULT 2
players JSONB DEFAULT '[]' -- [{id, name, avatar_url, team?, connected?}]
current_turn INT DEFAULT 0 -- index into players array
board JSONB DEFAULT '[]' -- dominoes placed on table [{left, right, placed_by}]
hands JSONB DEFAULT '{}' -- {player_id: [[3,5],[6,6],...]}
boneyard JSONB DEFAULT '[]' -- remaining undrawn pieces
spinner JSONB -- the double that opens multi-direction play
scores JSONB DEFAULT '{}' -- {player_id: cumulative_score}
round_number INT DEFAULT 1
round_history JSONB DEFAULT '[]' -- [{round, winner, points}]
target_score INT DEFAULT 100 -- first to reach this wins the match
moves JSONB DEFAULT '[]' -- [{player_id, piece, side, timestamp}]
game_state JSONB DEFAULT '{}' -- extra state (pass count, blocked flag)
chat JSONB DEFAULT '[]' -- in-game chat messages
host_id UUID -- player who created the room
result VARCHAR(50) -- final result description
winner_id UUID -- winning player
created_at TIMESTAMPTZ DEFAULT now()
completed_at TIMESTAMPTZ
```
### domino_queue
```sql
-- Matchmaking queue for domino
-- Realtime: enabled
id UUID PK DEFAULT gen_random_uuid()
user_id UUID NOT NULL
user_name VARCHAR(100)
mode VARCHAR(20) DEFAULT '2p' -- '2p' or '4p_teams'
match_id UUID -- set when matched
created_at TIMESTAMPTZ DEFAULT now()
```
### Domino Game Config (from game_plugins)
```json
{
"modes": ["2p", "4p_teams"],
"difficulties": [
{"id": "easy", "label": "سهل"},
{"id": "medium", "label": "متوسط"},
{"id": "hard", "label": "صعب"}
],
"target_scores": {"2p": 100, "4p_teams": 150}
}
```
### How Domino Works
1. Player creates room (gets `room_code`) OR enters queue
2. When enough players join → status becomes 'in_progress'
3. All game state lives in one row — updated via Supabase Realtime
4. `hands` holds each player's tiles (keyed by player_id)
5. `board` holds placed dominoes in order
6. `boneyard` is the draw pile
7. `scores` accumulate across rounds; first to `target_score` wins
8. Supports bots (easy/medium/hard difficulty)
---
## 28. LUDO GAME SYSTEM (Dedicated Tables)
Ludo also has its own dedicated tables, separate from the generic `matches` system.
### ludo_matches
```sql
-- Self-contained ludo game — all state in one row
-- Realtime: enabled
-- RLS: open read + open write
id UUID PK DEFAULT gen_random_uuid()
room_code TEXT UNIQUE -- shareable room code
status TEXT NOT NULL DEFAULT 'waiting' -- waiting, in_progress, completed
player_count INT NOT NULL DEFAULT 4
players JSONB NOT NULL DEFAULT '[]' -- [{id, name, avatar_url, color, is_bot?}]
current_turn INT NOT NULL DEFAULT 0
dice_value INT -- last rolled dice (1-6)
positions JSONB NOT NULL DEFAULT '{}' -- {player_id: {piece_0: pos, piece_1: pos, ...}}
moves JSONB NOT NULL DEFAULT '[]' -- [{player_id, piece, from, to, timestamp, killed?}]
winners JSONB NOT NULL DEFAULT '[]' -- ordered list of players who finished [1st, 2nd, ...]
game_state JSONB NOT NULL DEFAULT '{}' -- {extra_turn, rolled_six_count, etc.}
chat JSONB NOT NULL DEFAULT '[]'
host_id UUID
created_at TIMESTAMPTZ DEFAULT now()
completed_at TIMESTAMPTZ
result TEXT
```
### ludo_queue
```sql
-- Matchmaking queue for ludo
-- Realtime: enabled
-- RLS: open
id UUID PK DEFAULT gen_random_uuid()
user_id UUID NOT NULL
user_name TEXT NOT NULL DEFAULT ''
match_id UUID -- set when matched to a game
queued_at TIMESTAMPTZ DEFAULT now()
```
### Ludo Game Config (from game_plugins)
```json
{
"max_bots": 3,
"difficulties": [
{"id": "easy", "label": "سهل"},
{"id": "hard", "label": "صعب"}
],
"supports_room": true,
"supports_local": true
}
```
### How Ludo Works
1. Player creates room (gets `room_code`) OR enters queue
2. Can add bots to fill empty slots (up to 3 bots)
3. When ready → status becomes 'in_progress'
4. `positions` tracks each player's 4 pieces on the board
5. `dice_value` is the current roll
6. `winners` fills as players get all 4 pieces home (1st, 2nd, 3rd...)
7. Supports local play (same device, multiple players)
8. Supports room-based play (friends join via code)
---
## 29. TRIVIA QUESTIONS TABLE
```sql
-- Table: trivia_questions
-- NOTE: Trivia game is currently DISABLED (is_enabled=false in game_plugins)
-- Realtime: enabled
id UUID PK DEFAULT uuid_generate_v4()
text_ar TEXT NOT NULL -- Arabic question text
text_en TEXT -- English question text (optional)
options_ar JSONB NOT NULL -- ["خيار١", "خيار٢", "خيار٣", "خيار٤"]
options_en JSONB -- ["Option1", "Option2", "Option3", "Option4"]
correct_index INT NOT NULL -- 0-based index of correct answer
category TEXT NOT NULL -- e.g. 'science', 'history', 'sports', 'general'
difficulty TEXT NOT NULL CHECK (IN ('easy', 'medium', 'hard'))
is_active BOOLEAN DEFAULT true
times_shown INT DEFAULT 0 -- how many times this question appeared
times_correct INT DEFAULT 0 -- how many times answered correctly
created_at TIMESTAMPTZ DEFAULT now()
```
---
## 30. THEME_SETTINGS TABLE
```sql
-- Platform-wide theme configuration (admin-managed)
-- Realtime: enabled
-- RLS: public read
id UUID PK DEFAULT gen_random_uuid()
key TEXT NOT NULL UNIQUE -- e.g. 'primary_color', 'font_family'
value TEXT NOT NULL -- the setting value
category TEXT NOT NULL DEFAULT 'colors' -- colors, fonts, spacing, etc.
label TEXT -- human-readable label for admin UI
updated_at TIMESTAMPTZ DEFAULT now()
```
---
## 31. GAME ARCHITECTURE — IMPORTANT
The player app MUST handle two different game architectures:
### Pattern A: Generic Matches (Chess, Backgammon)
- Uses `matches` table + `matchmaking_queue`
- All game types share the same schema
- `game_key` field distinguishes the game
- Rating changes stored in `matches` row directly
- Uses `white_player_id` / `black_player_id` naming
### Pattern B: Dedicated Tables (Domino, Ludo)
- Each game has its own `{game}_matches` + `{game}_queue` tables
- Self-contained: all state in one row (board, hands, positions)
- Room-code based joining (not rating-range matchmaking)
- `players` is a JSONB array (supports 2-4+ players)
- Built-in chat in the same row
- Bot support built into the players array
### Pattern C: Trivia (Not Yet Implemented)
- Questions stored in `trivia_questions`
- Game is currently disabled
- No dedicated match table yet
---
## 32. UPDATED SYSTEM CONFIG (17 Settings)
| Key | Value | Description | Category |
|-----|-------|-------------|----------|
| platform_name | "EL3AB" | Platform name | general |
| platform_name_ar | "إل٣اب" | Arabic name | general |
| elo_default_rating | 1200 | Starting rating | rating |
| elo_k_factor_new | 40 | K for < 30 games | rating |
| elo_k_factor_established | 20 | K for 30+ games | rating |
| elo_k_factor_master | 10 | K for 2400+ | rating |
| matchmaking_range_expansion | 10 | Range expand/sec | matchmaking |
| matchmaking_max_wait_sec | 60 | Max wait time | matchmaking |
| coins_per_win_ranked | 25 | Ranked win reward | economy |
| coins_per_win_casual | 10 | Casual win reward | economy |
| xp_per_game | 50 | XP per game | economy |
| xp_per_win_bonus | 30 | Win XP bonus | economy |
| **daily_reward_base** | **50** | **Base daily coins** | **economy** |
| **daily_reward_streak_bonus** | **10** | **Extra per streak day** | **economy** |
| tournament_max_players | 256 | Max tournament size | tournaments |
| anticheat_cpl_threshold | 15 | CPL threshold | anticheat |
| anticheat_correlation_threshold | 85 | Engine correlation % | anticheat |
---
## 33. UPDATED GAME PLUGINS (Current State)
| game_key | name_ar | enabled | supports_bot | Dedicated Table | Config |
|----------|---------|---------|--------------|-----------------|--------|
| chess | شطرنج | YES | YES | NO (uses `matches`) | matchmaking, custom games |
| backgammon | طاولة | YES | NO | NO (uses `matches`) | — |
| domino | دومينو | YES | YES | YES (`domino_matches`) | modes: 2p, 4p_teams; bots: easy/medium/hard |
| ludo | لودو | YES | YES | YES (`ludo_matches`) | bots: easy/hard; room + local + queue |
| trivia | تريفيا بارتي | **NO (disabled)** | NO | NO | — |
---
## 34. PGVECTOR EXTENSION (AI-Ready)
The database now has pgvector installed with:
- Vector similarity search (cosine, L2, inner product)
- halfvec, sparsevec types
- HNSW and IVFFlat index support
This means the platform is prepared for AI features like:
- Semantic search (find similar players, similar games)
- Recommendation engine
- Content embeddings
No tables currently use vector columns — this is infrastructure for future features.
---
## END OF DOCUMENT
This document represents the complete, verified state of the El3ab production database as of May 2026. Every table schema, enum value, RPC function, and configuration setting was queried directly from the live server. Use this as the single source of truth — do not invent tables, columns, or values that are not listed here.
**Last full scan: 2026-05-26** — 104 tables, 29 enums, 11 RPC functions, 17 system config values, 9 storage buckets.
......@@ -95,6 +95,12 @@ return [
'tournaments/{id}/players/import' => ['module' => 'tournaments', 'action' => 'importPlayers'],
'tournaments/{id}/players/{playerId}/withdraw' => ['module' => 'tournaments', 'action' => 'withdrawPlayer'],
// Tournaments - Bot Testing
'tournaments/{id}/bots/populate' => ['module' => 'tournaments', 'action' => 'populateBots'],
'tournaments/{id}/bots/cleanup' => ['module' => 'tournaments', 'action' => 'cleanupBots'],
'tournaments/{id}/rounds/{roundId}/auto-play' => ['module' => 'tournaments', 'action' => 'autoPlayRound'],
'tournaments/{id}/simulate-round' => ['module' => 'tournaments', 'action' => 'simulateRound'],
// Tournaments - Tiebreaks & Export
'tournaments/{id}/tiebreaks/update' => ['module' => 'tournaments', 'action' => 'updateTiebreaks'],
'tournaments/{id}/export/{format}' => ['module' => 'tournaments', 'action' => 'export'],
......
-- Migration 005: Bot Tournament Testing
-- Adds is_bot flag to tournament_registrations for test player identification
-- Adds swiss_round_id to el3ab_tournament_rounds for linking to Swiss API rounds
ALTER TABLE tournament_registrations
ADD COLUMN IF NOT EXISTS is_bot BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS bot_metadata JSONB DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_tournament_registrations_bot
ON tournament_registrations(tournament_id) WHERE is_bot = TRUE;
ALTER TABLE el3ab_tournament_rounds
ADD COLUMN IF NOT EXISTS swiss_round_id UUID;
......@@ -1143,4 +1143,67 @@ class TournamentsController
Response::json(array_merge($info, ['standings' => $standings]));
}
// ===== Bot Testing =====
public function populateBots(array $params, string $method): void
{
Auth::requireCsrf();
$tournamentId = $params['id'];
$count = (int)($_POST['bot_count'] ?? 16);
$centerRating = (int)($_POST['center_rating'] ?? 1500);
$stdDev = (int)($_POST['std_dev'] ?? 300);
$count = max(4, min(128, $count));
$centerRating = max(400, min(2400, $centerRating));
$stdDev = max(50, min(600, $stdDev));
require_once __DIR__ . '/services/BotSimulationService.php';
$result = BotSimulationService::populateWithBots($tournamentId, $count, $centerRating, $stdDev);
if ($result['success']) {
Response::success("تم إضافة {$result['count']} بوت بنجاح", "/tournaments/{$tournamentId}?tab=arbiter");
} else {
Response::error($result['error'], "/tournaments/{$tournamentId}?tab=arbiter");
}
}
public function cleanupBots(array $params, string $method): void
{
Auth::requireCsrf();
$tournamentId = $params['id'];
require_once __DIR__ . '/services/BotSimulationService.php';
$result = BotSimulationService::cleanupBots($tournamentId);
Response::success("تم حذف {$result['count']} بوت", "/tournaments/{$tournamentId}?tab=arbiter");
}
public function autoPlayRound(array $params, string $method): void
{
Auth::requireCsrf();
$tournamentId = $params['id'];
$roundId = $params['roundId'];
$drawRate = (float)($_POST['draw_rate'] ?? 15) / 100;
$drawRate = max(0, min(0.5, $drawRate));
require_once __DIR__ . '/services/BotSimulationService.php';
$result = BotSimulationService::autoPlayRound($tournamentId, $roundId, $drawRate);
Response::json($result);
}
public function simulateRound(array $params, string $method): void
{
Auth::requireCsrf();
$tournamentId = $params['id'];
$drawRate = (float)($_POST['draw_rate'] ?? 15) / 100;
$drawRate = max(0, min(0.5, $drawRate));
require_once __DIR__ . '/services/BotSimulationService.php';
$result = BotSimulationService::simulateNextRound($tournamentId, $drawRate);
Response::json($result);
}
}
<?php
class BotSimulationService
{
private static array $maleNames = [
'أحمد', 'محمد', 'علي', 'عمر', 'حسن', 'طارق', 'زياد', 'ياسين', 'كريم', 'سامي',
'يوسف', 'ابراهيم', 'خالد', 'مصطفى', 'عادل', 'هشام', 'ماجد', 'فارس', 'بلال', 'أنس',
'رامي', 'شريف', 'وليد', 'حازم', 'عمرو', 'باسم', 'ايهاب', 'محمود', 'عبدالله', 'أيمن',
'تامر', 'مروان', 'سيف', 'عبدالرحمن', 'نبيل'
];
private static array $femaleNames = [
'سارة', 'نور', 'ليلى', 'فاطمة', 'مريم', 'هبة', 'ياسمين', 'دينا', 'رنا', 'منى',
'هالة', 'سلمى', 'لينا', 'ريم', 'آية', 'جنى', 'ملك', 'تسنيم', 'زينب', 'أمينة',
'شيماء', 'نادية', 'عبير', 'سمر', 'روان', 'مها', 'داليا', 'حنان', 'نجوى', 'أسماء',
'رقية', 'هند', 'لمياء', 'إيمان', 'غادة'
];
private static array $familyNames = [
'الشريف', 'المصري', 'العربي', 'الحسيني', 'السعيد', 'الفقي', 'الجمال', 'البدوي',
'الحداد', 'النجار', 'الخطيب', 'القاضي', 'المنصور', 'الهلالي', 'السيد', 'العزب',
'البنا', 'الغريب', 'الطيب', 'الرفاعي', 'المهدي', 'الصاوي', 'الحكيم', 'السباعي',
'المعلم', 'الشامي', 'الفاروق', 'الجندي', 'العطار', 'الدسوقي', 'الأنصاري', 'الزهراني',
'العمري', 'الحربي', 'الشهري'
];
public static function generateBotPlayers(int $count, int $centerRating = 1500, int $stdDev = 300): array
{
$players = [];
$usedNames = [];
$ratings = self::generateRatings($count, $centerRating, $stdDev);
for ($i = 0; $i < $count; $i++) {
$attempts = 0;
do {
$gender = random_int(0, 1) === 0 ? 'm' : 'f';
$firstNames = $gender === 'm' ? self::$maleNames : self::$femaleNames;
$firstName = $firstNames[array_rand($firstNames)];
$familyName = self::$familyNames[array_rand(self::$familyNames)];
$fullName = $firstName . ' ' . $familyName;
$attempts++;
} while (isset($usedNames[$fullName]) && $attempts < 50);
$usedNames[$fullName] = true;
$players[] = [
'name' => $fullName,
'rating' => $ratings[$i],
'gender' => $gender,
];
}
return $players;
}
private static function generateRatings(int $count, int $center, int $stdDev): array
{
$ratings = [];
for ($i = 0; $i < $count; $i++) {
$u1 = max(0.0001, (float)random_int(1, 10000) / 10000);
$u2 = (float)random_int(1, 10000) / 10000;
$z = sqrt(-2 * log($u1)) * cos(2 * M_PI * $u2);
$rating = (int)round($center + $z * $stdDev);
$rating = max(400, min(2400, $rating));
$ratings[] = $rating;
}
return $ratings;
}
public static function populateWithBots(string $tournamentId, int $count, int $centerRating = 1500, int $stdDev = 300): array
{
$db = Database::getInstance();
$tournament = $db->selectOne('el3ab_tournaments', ['id' => "eq.{$tournamentId}"]);
if (!$tournament) {
return ['success' => false, 'error' => 'البطولة غير موجودة'];
}
if (!$tournament['swiss_api_tournament_id']) {
return ['success' => false, 'error' => 'البطولة غير مرتبطة بـ Swiss API'];
}
$players = self::generateBotPlayers($count, $centerRating, $stdDev);
$swissPlayers = array_map(fn($p) => [
'name' => $p['name'],
'fideRatingStandard' => $p['rating'],
], $players);
$response = SwissApiService::bulkImportPlayers($tournament['swiss_api_tournament_id'], $swissPlayers);
if (!SwissApiService::isSuccess($response)) {
return ['success' => false, 'error' => 'فشل في إضافة اللاعبين للـ Swiss API: ' . SwissApiService::getError($response)];
}
$inserted = 0;
foreach ($players as $player) {
$db->insert('tournament_registrations', [
'tournament_id' => $tournamentId,
'player_id' => 'bot_' . uniqid(),
'status' => 'registered',
'is_bot' => true,
'bot_metadata' => json_encode([
'name' => $player['name'],
'rating' => $player['rating'],
'gender' => $player['gender'],
]),
]);
$inserted++;
}
return ['success' => true, 'count' => $inserted];
}
public static function autoPlayRound(string $tournamentId, string $roundId, float $drawRate = 0.15): array
{
$db = Database::getInstance();
$round = $db->selectOne('el3ab_tournament_rounds', ['id' => "eq.{$roundId}"]);
if (!$round) {
return ['success' => false, 'error' => 'الجولة غير موجودة'];
}
$tournament = $db->selectOne('el3ab_tournaments', ['id' => "eq.{$tournamentId}"]);
if (!$tournament || !$tournament['swiss_api_tournament_id']) {
return ['success' => false, 'error' => 'البطولة غير موجودة أو غير مرتبطة'];
}
$swissRoundId = $round['swiss_round_id'] ?? null;
if (!$swissRoundId) {
return ['success' => false, 'error' => 'الجولة غير مرتبطة بـ Swiss API'];
}
$pairingsResponse = SwissApiService::getPairings($swissRoundId);
if (!SwissApiService::isSuccess($pairingsResponse)) {
return ['success' => false, 'error' => 'فشل في جلب المواجهات'];
}
$pairings = SwissApiService::getBody($pairingsResponse);
if (empty($pairings)) {
return ['success' => false, 'error' => 'لا توجد مواجهات في هذه الجولة'];
}
$results = [];
$summary = ['white_wins' => 0, 'black_wins' => 0, 'draws' => 0, 'byes' => 0];
foreach ($pairings as $pairing) {
$pairingId = $pairing['id'];
if (!empty($pairing['is_bye']) || empty($pairing['black_player_id'])) {
$results[] = ['pairingId' => $pairingId, 'result' => 'bye_full'];
$summary['byes']++;
continue;
}
$ratingWhite = $pairing['white_player']['fide_rating_standard'] ?? $pairing['white_player']['rating'] ?? 1200;
$ratingBlack = $pairing['black_player']['fide_rating_standard'] ?? $pairing['black_player']['rating'] ?? 1200;
$result = self::simulateResult($ratingWhite, $ratingBlack, $drawRate);
$results[] = ['pairingId' => $pairingId, 'result' => $result];
if ($result === 'white_wins') $summary['white_wins']++;
elseif ($result === 'black_wins') $summary['black_wins']++;
else $summary['draws']++;
}
$submitResponse = SwissApiService::submitBatchResults($swissRoundId, $results);
if (!SwissApiService::isSuccess($submitResponse)) {
return ['success' => false, 'error' => 'فشل في إرسال النتائج: ' . SwissApiService::getError($submitResponse)];
}
$db->update('el3ab_tournament_rounds', ['id' => "eq.{$roundId}"], [
'status' => 'completed',
'results' => json_encode($results),
'completed_at' => date('c'),
]);
return ['success' => true, 'summary' => $summary, 'total_pairings' => count($pairings)];
}
private static function simulateResult(int $ratingWhite, int $ratingBlack, float $drawRate): string
{
$expectedWhite = 1.0 / (1.0 + pow(10, ($ratingBlack - $ratingWhite) / 400.0));
$roll = (float)random_int(0, 10000) / 10000;
if ($roll < $drawRate) {
return 'draw';
}
$adjustedRoll = ($roll - $drawRate) / (1.0 - $drawRate);
if ($adjustedRoll < $expectedWhite) {
return 'white_wins';
}
return 'black_wins';
}
public static function simulateNextRound(string $tournamentId, float $drawRate = 0.15): array
{
$db = Database::getInstance();
$tournament = $db->selectOne('el3ab_tournaments', ['id' => "eq.{$tournamentId}"]);
if (!$tournament || !$tournament['swiss_api_tournament_id']) {
return ['success' => false, 'error' => 'البطولة غير موجودة أو غير مرتبطة'];
}
$totalRounds = $tournament['rounds_total'] ?? $tournament['swiss_rounds'] ?? 7;
$currentRound = $tournament['current_round'] ?? 0;
if ($currentRound >= $totalRounds) {
return ['success' => true, 'completed' => true, 'round_number' => $currentRound, 'total_rounds' => $totalRounds];
}
$genResponse = SwissApiService::generateRound($tournament['swiss_api_tournament_id']);
if (!SwissApiService::isSuccess($genResponse)) {
return ['success' => false, 'error' => 'فشل في إنشاء الجولة: ' . SwissApiService::getError($genResponse)];
}
$roundData = SwissApiService::getBody($genResponse);
$swissRoundId = $roundData['id'] ?? $roundData['round_id'] ?? null;
$roundNumber = $roundData['round_number'] ?? ($currentRound + 1);
$roundRecord = $db->insert('el3ab_tournament_rounds', [
'tournament_id' => $tournamentId,
'round_number' => $roundNumber,
'swiss_round_id' => $swissRoundId,
'status' => 'in_progress',
'started_at' => date('c'),
]);
$roundId = $roundRecord[0]['id'] ?? $roundRecord['id'] ?? null;
$db->update('el3ab_tournaments', ['id' => "eq.{$tournamentId}"], [
'current_round' => $roundNumber,
]);
if (!$roundId) {
$localRound = $db->selectOne('el3ab_tournament_rounds', [
'tournament_id' => "eq.{$tournamentId}",
'round_number' => "eq.{$roundNumber}",
]);
$roundId = $localRound['id'] ?? null;
}
if ($roundId) {
$playResult = self::autoPlayRound($tournamentId, $roundId, $drawRate);
if (!$playResult['success']) {
return $playResult;
}
}
$isComplete = $roundNumber >= $totalRounds;
if ($isComplete) {
$db->update('el3ab_tournaments', ['id' => "eq.{$tournamentId}"], [
'status' => 'completed',
]);
}
return [
'success' => true,
'completed' => $isComplete,
'round_number' => $roundNumber,
'total_rounds' => $totalRounds,
];
}
public static function cleanupBots(string $tournamentId): array
{
$db = Database::getInstance();
$bots = $db->select('tournament_registrations', [
'tournament_id' => "eq.{$tournamentId}",
'is_bot' => 'eq.true',
'select' => 'id',
]);
$count = count($bots);
if ($count > 0) {
$db->delete('tournament_registrations', [
'tournament_id' => "eq.{$tournamentId}",
'is_bot' => 'eq.true',
]);
}
return ['success' => true, 'count' => $count];
}
}
......@@ -67,4 +67,11 @@
</div>
</div>
<?php endif; ?>
<!-- Bot Testing -->
<?php if (!empty($tournament['swiss_api_tournament_id'])): ?>
<div class="mt-4">
<?php include __DIR__ . '/_bot_testing.php'; ?>
</div>
<?php endif; ?>
</div>
<?php
/**
* Bot Testing Panel
* Expects: $tournament, $rounds
*/
$botCount = 0;
$bots = Database::getInstance()->select('tournament_registrations', [
'tournament_id' => "eq.{$tournament['id']}",
'is_bot' => 'eq.true',
'select' => 'id',
]);
$botCount = count($bots);
$totalRounds = $tournament['rounds_total'] ?? $tournament['swiss_rounds'] ?? 7;
$currentRound = $tournament['current_round'] ?? 0;
$hasActiveRound = false;
if (!empty($rounds)) {
foreach ($rounds as $r) {
if ($r['status'] !== 'completed') {
$hasActiveRound = true;
break;
}
}
}
?>
<div class="card">
<div class="card-header">
<h3 class="card-title" style="display:flex;align-items:center;gap:8px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="3"/><path d="M12 8v3"/><circle cx="8" cy="16" r="1"/><circle cx="16" cy="16" r="1"/></svg>
اختبار البوتات
</h3>
<?php if ($botCount > 0): ?>
<span class="badge badge-info"><?= $botCount ?> بوت مسجل</span>
<?php endif; ?>
</div>
<div class="p-4">
<!-- Section 1: Populate -->
<div class="mb-5">
<h4 class="text-sm font-medium mb-3">ملء البطولة بلاعبين وهميين</h4>
<form method="POST" action="/tournaments/<?= $tournament['id'] ?>/bots/populate">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="flex gap-3 items-end flex-wrap">
<div class="form-group" style="margin-bottom:0;width:120px;">
<label class="form-label">العدد</label>
<select name="bot_count" class="form-input">
<option value="8">8</option>
<option value="16" selected>16</option>
<option value="32">32</option>
<option value="64">64</option>
<option value="128">128</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0;width:140px;">
<label class="form-label">متوسط التقييم</label>
<input type="number" name="center_rating" class="form-input" value="1500" min="400" max="2400" step="50">
</div>
<div class="form-group" style="margin-bottom:0;width:140px;">
<label class="form-label">الانحراف المعياري</label>
<input type="number" name="std_dev" class="form-input" value="300" min="50" max="600" step="50">
</div>
<button type="submit" class="btn btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
ملء بالبوتات
</button>
</div>
</form>
<?php if ($botCount > 0): ?>
<div class="mt-3">
<form method="POST" action="/tournaments/<?= $tournament['id'] ?>/bots/cleanup" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('حذف كل البوتات (<?= $botCount ?>) من البطولة؟')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
حذف كل البوتات (<?= $botCount ?>)
</button>
</form>
</div>
<?php endif; ?>
</div>
<?php if ($tournament['status'] === 'in_progress'): ?>
<hr style="border-color:var(--border-color);margin:16px 0;">
<!-- Section 2: Auto-Play Round -->
<?php if ($hasActiveRound): ?>
<div class="mb-5">
<h4 class="text-sm font-medium mb-3">تشغيل الجولة الحالية تلقائياً</h4>
<div class="flex gap-3 items-end flex-wrap">
<div class="form-group" style="margin-bottom:0;width:140px;">
<label class="form-label">نسبة التعادل %</label>
<input type="number" id="autoPlayDrawRate" class="form-input" value="15" min="0" max="50" step="5">
</div>
<button type="button" class="btn btn-success" onclick="autoPlayCurrentRound()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
تشغيل الجولة تلقائياً
</button>
</div>
</div>
<?php endif; ?>
<!-- Section 3: Full Simulation -->
<div>
<h4 class="text-sm font-medium mb-3">محاكاة البطولة بالكامل</h4>
<div class="flex gap-3 items-end flex-wrap">
<div class="form-group" style="margin-bottom:0;width:140px;">
<label class="form-label">نسبة التعادل %</label>
<input type="number" id="simDrawRate" class="form-input" value="15" min="0" max="50" step="5">
</div>
<button type="button" class="btn btn-primary" id="simulateBtn" onclick="simulateFullTournament()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
محاكاة البطولة بالكامل
</button>
</div>
<!-- Progress -->
<div id="simProgress" style="display:none;margin-top:16px;">
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
<span id="simStatusText">جاري المحاكاة...</span>
<span id="simRoundText">الجولة 0 من <?= $totalRounds ?></span>
</div>
<div style="height:8px;background:var(--bg-secondary);border-radius:4px;overflow:hidden;">
<div id="simProgressBar" style="height:100%;width:0%;background:var(--brand-blue);border-radius:4px;transition:width 0.3s;"></div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<script>
function autoPlayCurrentRound() {
const drawRate = document.getElementById('autoPlayDrawRate').value;
const roundId = '<?= !empty($rounds) ? end($rounds)['id'] : '' ?>';
if (!roundId) { alert('لا توجد جولة نشطة'); return; }
if (!confirm('تشغيل نتائج عشوائية للجولة الحالية؟')) return;
fetch('/tournaments/<?= $tournament['id'] ?>/rounds/' + roundId + '/auto-play', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: '_csrf=<?= Auth::csrfToken() ?>&draw_rate=' + drawRate
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('تم تشغيل الجولة: ' + data.total_pairings + ' مباراة');
location.reload();
} else {
alert('خطأ: ' + data.error);
}
})
.catch(e => alert('خطأ في الاتصال'));
}
function simulateFullTournament() {
const drawRate = document.getElementById('simDrawRate').value;
const totalRounds = <?= $totalRounds ?>;
if (!confirm('محاكاة كل الجولات المتبقية؟ هذا قد يستغرق بضع ثوانٍ.')) return;
document.getElementById('simProgress').style.display = 'block';
document.getElementById('simulateBtn').disabled = true;
runNextRound(drawRate, totalRounds);
}
function runNextRound(drawRate, totalRounds) {
fetch('/tournaments/<?= $tournament['id'] ?>/simulate-round', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: '_csrf=<?= Auth::csrfToken() ?>&draw_rate=' + drawRate
})
.then(r => r.json())
.then(data => {
if (!data.success) {
document.getElementById('simStatusText').textContent = 'خطأ: ' + data.error;
document.getElementById('simulateBtn').disabled = false;
return;
}
const pct = (data.round_number / data.total_rounds) * 100;
document.getElementById('simProgressBar').style.width = pct + '%';
document.getElementById('simRoundText').textContent = 'الجولة ' + data.round_number + ' من ' + data.total_rounds;
if (data.completed) {
document.getElementById('simStatusText').textContent = 'اكتملت المحاكاة!';
setTimeout(() => location.reload(), 1500);
} else {
setTimeout(() => runNextRound(drawRate, totalRounds), 500);
}
})
.catch(e => {
document.getElementById('simStatusText').textContent = 'خطأ في الاتصال';
document.getElementById('simulateBtn').disabled = false;
});
}
</script>
......@@ -241,7 +241,10 @@ $tabs['arbiter'] = 'أدوات الحكم';
<?php foreach ($players as $i => $player): ?>
<tr>
<td><?= $i + 1 ?></td>
<td class="font-medium"><?= View::e($player['player_name'] ?? $player['player_id']) ?></td>
<td class="font-medium">
<?= View::e($player['player_name'] ?? $player['player_id']) ?>
<?php if (!empty($player['is_bot'])): ?><span class="badge badge-ghost" style="font-size:10px;margin-right:4px;">🤖</span><?php endif; ?>
</td>
<td class="tabular-nums"><?= $player['rating'] ?? 1500 ?></td>
<td class="text-xs text-muted"><?= $player['registered_at'] ? date('Y/m/d H:i', strtotime($player['registered_at'])) : '-' ?></td>
<td>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment