Commit 4d7736a1 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: complete rebuild — all 16 phases, modular game engine

- Phase 0: Core engine (scene manager, store, bus, net, tween, audio, canvas, HUD, i18n)
- Phase 1: Auth (splash, login, register, token management)
- Phase 2: Play World (game carousel, mode picker, bot/time selectors, queue)
- Phase 3-4: Chess (canvas board, drag/drop, chess.js, Stockfish bots, clock, live matchmaking)
- Phase 5: Domino (tile chain logic, bot AI, pip scoring, multiplayer)
- Phase 6: Ludo (cross-board, dice, captures, 4-player bots)
- Phase 7: Social (friends, notifications, realtime)
- Phase 8: Organizations (browse, join, members, detail)
- Phase 9: Rewards (daily claims, streak, coins)
- Phase 10: Profile (player card, stats, settings, logout)
- Phase 11: Shop (cosmetics, purchase flow, equip)
- Phase 12: Rank (leaderboard, tournaments, registration)
- Phase 13: Puzzles (chess training with rating)
- Phase 14-16: All APIs, polish, deployment config
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1b09a424
{
"permissions": {
"allow": [
"Bash(*)",
"Read(*)",
"Write(*)",
"Edit(*)",
"Agent(*)",
"Workflow(*)",
"WebFetch(*)",
"TodoWrite(*)",
"NotebookEdit(*)"
],
"defaultMode": "bypassPermissions"
}
}
RewriteEngine On RewriteEngine On
# Pass Authorization header to PHP (Apache strips it by default) # Serve existing files/dirs directly
RewriteCond %{HTTP:Authorization} . RewriteCond %{REQUEST_URI} ^/public/ [OR]
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteCond %{REQUEST_URI} ^/api/
RewriteRule ^ - [L]
# Force HTTPS (CapRover handles SSL termination via X-Forwarded-Proto) # Everything else -> index.php
RewriteCond %{HTTP:X-Forwarded-Proto} =http
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Route all non-file requests to index.php
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?route=$1 [QSA,L] RewriteRule ^ index.php [L]
# Security headers # Security headers
<IfModule mod_headers.c> <IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff" Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN" Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block" Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "strict-origin-when-cross-origin" Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Methods "GET, POST, PATCH, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization, apikey"
</IfModule> </IfModule>
# Deny access to sensitive files # Handle OPTIONS preflight
<FilesMatch "\.(pem|md|gitignore)$"> RewriteCond %{REQUEST_METHOD} OPTIONS
Require all denied RewriteRule ^(.*)$ $1 [R=200,L]
</FilesMatch>
<Files "config/*">
Require all denied
</Files>
<Files "storage/*">
Require all denied
</Files>
This diff is collapsed.
This diff is collapsed.
# EL3AB Database Quick Reference
**Full audit:** [docs/supabase_master_audit.md](docs/supabase_master_audit.md) (992 lines, every table/column/FK/policy)
---
## Connection
| Key | Value |
|-----|-------|
| API Gateway | https://safe-supabase-kong.caprover.al-arcade.com |
| REST endpoint | /rest/v1/{table} |
| Auth endpoint | /auth/v1/ |
| Storage endpoint | /storage/v1/ |
| Realtime WS | wss://safe-supabase-kong.caprover.al-arcade.com/realtime/v1/websocket |
| Anon Key | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84 |
| Service Key | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4 |
---
## Tables Used by Player App (23 core)
### Player Identity
| Table | Key Columns | PK |
|-------|-------------|-----|
| profiles | username, display_name, elo_*, coins, gems, xp, level, is_online, is_banned | id (uuid, = auth.users.id) |
| player_game_ratings | player_id, game_key, rating, games_played, wins, losses, draws | id (uuid) |
| user_profiles | full_name, fide_id, fide_rating_*, nationality | id (uuid) |
### Games
| Table | Key Columns | PK |
|-------|-------------|-----|
| matches | game_key, white/black_player_id, status, result, time_control, moves (jsonb), current_fen | id (uuid) |
| domino_matches | room_code, status, players (jsonb), board (jsonb), hands (jsonb), scores (jsonb) | id (uuid) |
| ludo_matches | room_code, status, players (jsonb), positions (jsonb), dice_value, winners (jsonb) | id (uuid) |
| matchmaking_queue | player_id, game_key, time_control, rating, status, matched_with, match_id | id (uuid) |
| domino_queue | user_id, mode, match_id | id (uuid) |
| ludo_queue | user_id, match_id | id (uuid) |
| game_plugins | game_key (PK), name, name_ar, is_enabled, supports_* | game_key (text) |
| puzzles | fen, moves, rating, themes | id (serial) |
### Competitive
| Table | Key Columns | PK |
|-------|-------------|-----|
| leaderboards | player_id, game_key, time_control_type, period, rank, rating | id (uuid) |
| rating_history | player_id, game_key, rating_before/after, match_id, opponent_id | id (uuid) |
| el3ab_tournaments | org_id, game_key, format, time_control, status, max_players, starts_at | id (uuid) |
| tournament_registrations | tournament_id, player_id, status | id (uuid) |
### Social
| Table | Key Columns | PK |
|-------|-------------|-----|
| friendships | requester_id, addressee_id, status (pending/accepted) | id (uuid) |
| notifications | user_id, type, title, title_ar, body, is_read | id (uuid) |
| chat_messages | channel_type, channel_id, sender_id, content | id (uuid) |
| activity_feed | actor_id, action, target_type, target_id | id (uuid) |
### Economy
| Table | Key Columns | PK |
|-------|-------------|-----|
| economy_transactions | player_id, type, currency (coins/gems), amount, balance_after | id (uuid) |
| cosmetics | name, type (enum), rarity (enum), price_coins, price_gems | id (text) |
| player_cosmetics | player_id, cosmetic_id, is_equipped | id (uuid) |
| achievements | name, category, condition (jsonb), xp_reward, coins_reward | id (text) |
| player_achievements | player_id, achievement_id, progress, is_completed | id (uuid) |
| xp_levels | level (PK), xp_required, reward_coins, reward_gems | level (int) |
---
## Key Enums
```
match_status: waiting, ready, in_progress, paused, completed, aborted, abandoned
match_result: 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
time_control: 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
cosmetic_type: avatar_frame, board_theme, piece_set, profile_banner, chat_emoji, victory_animation, title_badge, trail_effect, sound_pack
cosmetic_rarity: common, uncommon, rare, epic, legendary
```
---
## RLS Rules (Player App perspective)
### Can read without auth (anon key, no JWT)
profiles, matches, leaderboards, achievements, cosmetics, game_plugins, puzzles, feature_flags, el3ab_tournaments, xp_levels, clubs, platform_assets, platform_theme, rating_history
### Must be authenticated (anon key + JWT)
- **Own data:** notifications, economy_transactions, player_cosmetics, player_achievements, matchmaking_queue, friendships
- **Can INSERT:** matches (own player), friendships (own requester), matchmaking_queue (own), chat_messages (own sender)
- **Can UPDATE:** profiles (own), notifications (own, mark read), matches (own player), matchmaking_queue (own)
- **Can DELETE:** matchmaking_queue (own)
### Requires service key (PHP backend)
All org_* tables (28), bracket_matches, tournament_brackets, tournament_phases, player_frames, profile_frames
---
## Key Foreign Keys (Player App)
```
profiles.id ← auth.users.id (trigger creates on signup)
matches.white_player_id → profiles.id
matches.black_player_id → profiles.id
matches.rematch_of → matches.id
matchmaking_queue.player_id → profiles.id
matchmaking_queue.match_id → matches.id
friendships.requester_id → profiles.id
friendships.addressee_id → profiles.id
notifications.user_id → profiles.id
economy_transactions.player_id → profiles.id
player_cosmetics.player_id → profiles.id
player_cosmetics.cosmetic_id → cosmetics.id
player_achievements.player_id → profiles.id
player_achievements.achievement_id → achievements.id
leaderboards.player_id → profiles.id
rating_history.player_id → profiles.id
rating_history.match_id → matches.id
el3ab_tournaments.org_id → el3ab_organizations.id
tournament_registrations.tournament_id → el3ab_tournaments.id
```
---
## RPC Functions Available
| Function | Args | Returns | Purpose |
|----------|------|---------|---------|
| register_tournament_player | tournament_id, player_auth_id | json | Register for tournament (validates capacity/status) |
| cancel_tournament_registration | tournament_id | json | Cancel registration |
| get_all_tournaments | — | json | Get tournaments + players + brackets |
| get_tournament_by_id | id | json | Single tournament detail |
| get_tournament_room | tournament_id, bracket_id | json | Get lobby ID for match |
| check_opponent_timeout | bracket_id, user_numeric_id | json | Check if opponent timed out |
| check_app_version | client_version | json | Mobile app version check |
| get_friends_by_auth_ids | auth_ids[] | json | Get friend profiles (Unity) |
---
## Storage Buckets
| Bucket | Public | Use in Player App |
|--------|--------|-------------------|
| profile-images | YES | Avatar upload/display |
| avatars | YES | Legacy avatars |
| profile-frames | YES | Frame overlays |
| org-logos | YES | Org logos in listings |
| org-banners | YES | Org banners |
---
## Realtime Subscription Patterns
```javascript
// Match state (chess/domino/ludo)
supabase.channel('game')
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'matches',
filter: `id=eq.${matchId}`
}, handleGameUpdate)
// Matchmaking
supabase.channel('queue')
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'matchmaking_queue',
filter: `player_id=eq.${userId}`
}, handleMatchFound)
// Notifications
supabase.channel('notifs')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'notifications',
filter: `user_id=eq.${userId}`
}, handleNewNotification)
```
FROM php:8.3-apache FROM php:8.3-apache
RUN apt-get update && apt-get install -y libpq-dev \ RUN apt-get update && apt-get install -y libpq-dev libcurl4-openssl-dev \
&& docker-php-ext-install pdo pdo_pgsql pgsql \ && docker-php-ext-install pdo pdo_pgsql pgsql curl \
&& apt-get clean && rm -rf /var/lib/apt/lists/* && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN a2enmod rewrite headers RUN a2enmod rewrite headers
...@@ -10,6 +10,9 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html ...@@ -10,6 +10,9 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
RUN echo '<Directory /var/www/html>\n AllowOverride All\n Require all granted\n</Directory>' > /etc/apache2/conf-available/allowoverride.conf \
&& a2enconf allowoverride
COPY . /var/www/html/ COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html RUN chown -R www-data:www-data /var/www/html
......
This diff is collapsed.
<?php <?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
$token = get_auth_token(); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if (!$token) { require_once __DIR__ . '/../includes/supabase.php';
http_response_code(401); require_once __DIR__ . '/../includes/auth.php';
echo json_encode(['error' => 'unauthorized']);
exit;
}
$category = $_GET['category'] ?? null;
$endpoint = 'achievements?select=*&order=category.asc,tier.asc'; $token = requireAuth();
if ($category) { $userId = getUserId($token);
$endpoint .= '&category=eq.' . $category; $db = supabase($token);
}
$achRes = supabase_rest('GET', $endpoint, [], $token); $achievements = $db->get('achievements', ['select' => 'id,name,name_ar,description,description_ar,category,xp_reward,coins_reward,icon']);
$achievements = ($achRes['status'] === 200 && is_array($achRes['data']) && !isset($achRes['data']['code'])) ? $achRes['data'] : []; $playerAchievements = $db->get('player_achievements', ['player_id' => 'eq.' . $userId, 'select' => 'achievement_id,progress,is_completed,completed_at']);
if (!empty($achievements)) { $playerMap = [];
$unlockedRes = supabase_rest('GET', 'player_achievements?select=achievement_id,unlocked_at', [], $token); if (is_array($playerAchievements) && !isset($playerAchievements['error'])) {
$unlocked = []; foreach ($playerAchievements as $pa) {
if ($unlockedRes['status'] === 200 && is_array($unlockedRes['data'])) { $playerMap[$pa['achievement_id']] = $pa;
foreach ($unlockedRes['data'] as $row) {
$unlocked[$row['achievement_id']] = $row['unlocked_at'];
}
} }
}
foreach ($achievements as &$ach) { $result = [];
$ach['unlocked'] = isset($unlocked[$ach['id']]); if (is_array($achievements) && !isset($achievements['error'])) {
$ach['unlocked_at'] = $unlocked[$ach['id']] ?? null; foreach ($achievements as $a) {
$ach['progress'] = 0; $a['player_progress'] = $playerMap[$a['id']]['progress'] ?? 0;
$a['is_completed'] = $playerMap[$a['id']]['is_completed'] ?? false;
$result[] = $a;
} }
} }
echo json_encode(['achievements' => $achievements]); jsonResponse(['achievements' => $result]);
<?php <?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
$token = get_auth_token(); header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if (!$token) {
http_response_code(401); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
echo json_encode(['error' => 'unauthorized']);
exit; require_once __DIR__ . '/../includes/auth.php';
} require_once __DIR__ . '/../config/constants.php';
$method = $_SERVER['REQUEST_METHOD']; $token = requireAuth();
$input = getInput();
if ($method === 'GET') { $fen = $input['fen'] ?? '';
$gameId = $_GET['id'] ?? ''; $depth = intval($input['depth'] ?? 18);
if (!$gameId) { $lines = intval($input['lines'] ?? 3);
http_response_code(400);
echo json_encode(['error' => 'missing game id']); if (!$fen) jsonError('fen is required');
exit;
} $payload = json_encode(['fen' => $fen, 'depth' => min($depth, 25), 'lines' => min($lines, 5)]);
$res = supabase_rest('GET', "matches?id=eq.{$gameId}&select=*", [], $token); $ch = curl_init(STOCKFISH_API . '/api/chess/analyze');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($res['status'] >= 200 && $res['status'] < 300 && !empty($res['data'])) { curl_setopt($ch, CURLOPT_POST, true);
$game = $res['data'][0]; curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
// If analysis already cached in game_state, return it curl_setopt($ch, CURLOPT_TIMEOUT, 35);
$gameState = $game['game_state'] ?? null; $response = curl_exec($ch);
if (is_string($gameState)) { $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$gameState = json_decode($gameState, true); curl_close($ch);
}
$hasAnalysis = isset($gameState['analysis']) && !empty($gameState['analysis']); if ($httpCode !== 200) {
$err = json_decode($response, true);
echo json_encode([ jsonError($err['error'] ?? 'Analysis failed', $httpCode);
'game' => $game,
'has_analysis' => $hasAnalysis,
'analysis' => $hasAnalysis ? $gameState['analysis'] : null
]);
} else {
http_response_code(404);
echo json_encode(['error' => 'game not found']);
}
exit;
} }
// POST requests jsonResponse(json_decode($response, true));
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
switch ($action) {
case 'analyze':
$gameId = $input['game_id'] ?? '';
$positions = $input['positions'] ?? []; // Array of FEN strings
if (!$gameId || empty($positions)) {
http_response_code(400);
echo json_encode(['error' => 'missing game_id or positions']);
exit;
}
$evaluations = [];
$apiUrl = STOCKFISH_API . '/api/chess/move';
foreach ($positions as $index => $fen) {
$payload = json_encode([
'bot_id' => 'grandmaster',
'fen' => $fen,
'wtime' => 120000,
'btime' => 120000
]);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
unset($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
$evaluations[] = [
'move_index' => $index,
'fen' => $fen,
'best_move' => $data['best_move'] ?? null,
'evaluation' => $data['evaluation'] ?? 0,
'depth' => $data['depth'] ?? 20
];
} else {
$evaluations[] = [
'move_index' => $index,
'fen' => $fen,
'best_move' => null,
'evaluation' => 0,
'depth' => 0,
'error' => true
];
}
// Small delay to avoid overwhelming the API
if ($index < count($positions) - 1) {
usleep(100000); // 100ms
}
}
// Cache analysis in game_state
$analysisData = [
'evaluations' => $evaluations,
'analyzed_at' => date('c')
];
// Get existing game_state
$gameRes = supabase_rest('GET', "matches?id=eq.{$gameId}&select=game_state", [], $token);
$existingState = [];
if (!empty($gameRes['data'][0]['game_state'])) {
$existingState = $gameRes['data'][0]['game_state'];
if (is_string($existingState)) {
$existingState = json_decode($existingState, true) ?? [];
}
}
$existingState['analysis'] = $analysisData;
supabase_rest('PATCH', "matches?id=eq.{$gameId}", [
'game_state' => json_encode($existingState)
], $token);
echo json_encode([
'ok' => true,
'analysis' => $analysisData
]);
break;
case 'analyze_single':
// Analyze a single position (used for real-time partial analysis)
$fen = $input['fen'] ?? '';
if (!$fen) {
http_response_code(400);
echo json_encode(['error' => 'missing fen']);
exit;
}
$apiUrl = STOCKFISH_API . '/api/chess/move';
$payload = json_encode([
'bot_id' => 'grandmaster',
'fen' => $fen,
'wtime' => 120000,
'btime' => 120000
]);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
unset($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
echo json_encode([
'best_move' => $data['best_move'] ?? null,
'evaluation' => $data['evaluation'] ?? 0,
'depth' => $data['depth'] ?? 20
]);
} else {
http_response_code(502);
echo json_encode(['error' => 'engine unavailable']);
}
break;
case 'cache':
$gameId = $input['game_id'] ?? '';
$evaluations = $input['evaluations'] ?? [];
if (!$gameId || empty($evaluations)) {
http_response_code(400);
echo json_encode(['error' => 'missing data']);
exit;
}
$analysisData = [
'evaluations' => $evaluations,
'analyzed_at' => date('c')
];
$gameRes = supabase_rest('GET', "matches?id=eq.{$gameId}&select=game_state", [], SUPABASE_SERVICE_KEY);
$existingState = [];
if (!empty($gameRes['data'][0]['game_state'])) {
$existingState = $gameRes['data'][0]['game_state'];
if (is_string($existingState)) {
$existingState = json_decode($existingState, true) ?? [];
}
}
$existingState['analysis'] = $analysisData;
supabase_rest('PATCH', "matches?id=eq.{$gameId}", [
'game_state' => json_encode($existingState)
], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'invalid action']);
}
<?php <?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
$input = json_decode(file_get_contents('php://input'), true); require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
$input = getInput();
$action = $input['action'] ?? ''; $action = $input['action'] ?? '';
if ($action === 'login') { switch ($action) {
$email = $input['email'] ?? ''; case 'signup':
handleSignup($input);
break;
case 'login':
handleLogin($input);
break;
case 'refresh':
handleRefresh($input);
break;
case 'logout':
handleLogout();
break;
default:
jsonError('Invalid action', 400);
}
function handleSignup(array $input): void {
$email = trim($input['email'] ?? '');
$password = $input['password'] ?? ''; $password = $input['password'] ?? '';
$username = trim($input['username'] ?? '');
if (!$email || !$password) { if (!$email || !$password || !$username) {
http_response_code(400); jsonError('Email, password and username are required');
echo json_encode(['error' => 'البريد وكلمة المرور مطلوبين']); }
exit; if (strlen($password) < 6) {
jsonError('Password must be at least 6 characters');
}
if (strlen($username) < 3 || strlen($username) > 20) {
jsonError('Username must be 3-20 characters');
} }
$result = supabase_auth('token?grant_type=password', [ $result = supabaseAuth('POST', 'signup', [
'email' => $email, 'email' => $email,
'password' => $password, 'password' => $password,
'data' => ['username' => $username]
]); ]);
if ($result['status'] !== 200) { if (isset($result['error'])) {
$msg = $result['data']['error_description'] ?? $result['data']['msg'] ?? 'بيانات الدخول غير صحيحة'; jsonError($result['error'], $result['code'] ?? 400);
http_response_code(401);
echo json_encode(['error' => $msg]);
exit;
} }
echo json_encode([ if (isset($result['access_token'])) {
'access_token' => $result['data']['access_token'], $db = supabase($result['access_token']);
'refresh_token' => $result['data']['refresh_token'], $db->update('profiles', [
'user' => $result['data']['user'], 'username' => $username,
]); 'display_name' => $username
], ['id' => 'eq.' . $result['user']['id']]);
jsonResponse([
'access_token' => $result['access_token'],
'refresh_token' => $result['refresh_token'],
'user' => $result['user']
]);
}
} elseif ($action === 'register') { jsonResponse($result);
$email = $input['email'] ?? ''; }
$password = $input['password'] ?? '';
$username = $input['username'] ?? '';
if (!$email || !$password || !$username) { function handleLogin(array $input): void {
http_response_code(400); $email = trim($input['email'] ?? '');
echo json_encode(['error' => 'جميع الحقول مطلوبة']); $password = $input['password'] ?? '';
exit;
}
if (strlen($password) < 6) { if (!$email || !$password) {
http_response_code(400); jsonError('Email and password are required');
echo json_encode(['error' => 'كلمة المرور يجب ان تكون 6 احرف على الاقل']);
exit;
} }
$result = supabase_auth('signup', [ $result = supabaseAuth('POST', 'token?grant_type=password', [
'email' => $email, 'email' => $email,
'password' => $password, 'password' => $password
'data' => ['username' => $username, 'display_name' => $username],
]); ]);
if ($result['status'] !== 200 && $result['status'] !== 201) { if (isset($result['error'])) {
$msg = $result['data']['error_description'] ?? $result['data']['msg'] ?? 'فشل التسجيل'; jsonError($result['error'], $result['code'] ?? 401);
http_response_code(400);
echo json_encode(['error' => $msg]);
exit;
} }
if (isset($result['data']['access_token'])) { jsonResponse([
echo json_encode([ 'access_token' => $result['access_token'],
'access_token' => $result['data']['access_token'], 'refresh_token' => $result['refresh_token'],
'refresh_token' => $result['data']['refresh_token'], 'user' => $result['user']
'user' => $result['data']['user'], ]);
]); }
} else {
echo json_encode(['message' => 'تم التسجيل بنجاح، يرجى تاكيد البريد']);
}
} elseif ($action === 'refresh') { function handleRefresh(array $input): void {
$refreshToken = $input['refresh_token'] ?? ''; $refreshToken = $input['refresh_token'] ?? '';
if (!$refreshToken) { if (!$refreshToken) {
http_response_code(400); jsonError('Refresh token required');
echo json_encode(['error' => 'refresh_token مطلوب']);
exit;
} }
$result = supabase_auth('token?grant_type=refresh_token', [ $result = supabaseAuth('POST', 'token?grant_type=refresh_token', [
'refresh_token' => $refreshToken, 'refresh_token' => $refreshToken
]); ]);
if ($result['status'] !== 200) { if (isset($result['error'])) {
http_response_code(401); jsonError($result['error'], $result['code'] ?? 401);
echo json_encode(['error' => 'الجلسة منتهية']);
exit;
} }
echo json_encode([ jsonResponse([
'access_token' => $result['data']['access_token'], 'access_token' => $result['access_token'],
'refresh_token' => $result['data']['refresh_token'], 'refresh_token' => $result['refresh_token'],
'user' => $result['user']
]); ]);
}
} else { function handleLogout(): void {
http_response_code(400); $token = getAuthToken();
echo json_encode(['error' => 'action غير معروف']); if ($token) {
supabaseAuth('POST', 'logout', null, $token);
}
jsonResponse(['success' => true]);
} }
This diff is collapsed.
<?php <?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if (!check_feature_flag('bot_games_enabled')) { if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
http_response_code(403);
echo json_encode(['error' => 'feature_disabled', 'message' => 'اللعب ضد البوت معطل حالياً']); require_once __DIR__ . '/../includes/auth.php';
exit; require_once __DIR__ . '/../config/constants.php';
$input = getInput();
$action = $input['action'] ?? '';
switch ($action) {
case 'list':
handleList();
break;
case 'move':
handleMove($input);
break;
default:
jsonError('Invalid action');
} }
$cacheFile = sys_get_temp_dir() . '/el3ab_bots_cache.json'; function handleList(): void {
$cacheTTL = 300; // 5 minutes $ch = curl_init(STOCKFISH_API . '/api/chess/bots');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) { if ($httpCode !== 200) {
echo file_get_contents($cacheFile); jsonError('Failed to fetch bots', 502);
exit; }
$data = json_decode($response, true);
jsonResponse($data);
} }
$ch = curl_init(STOCKFISH_API . '/api/chess/bots'); function handleMove(array $input): void {
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $fen = $input['fen'] ?? '';
curl_setopt($ch, CURLOPT_HTTPHEADER, [ $botId = $input['bot_id'] ?? '';
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]); if (!$fen || !$botId) {
curl_setopt($ch, CURLOPT_TIMEOUT, 10); jsonError('fen and bot_id are required');
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200 && $response) {
file_put_contents($cacheFile, $response);
echo $response;
} else {
if (file_exists($cacheFile)) {
echo file_get_contents($cacheFile);
} else {
http_response_code(502);
echo json_encode(['error' => 'bot_service_unavailable']);
} }
$payload = json_encode(['fen' => $fen, 'bot_id' => $botId]);
$ch = curl_init(STOCKFISH_API . '/api/chess/move');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
$err = json_decode($response, true);
jsonError($err['error'] ?? 'Stockfish error', $httpCode);
}
jsonResponse(json_decode($response, true));
} }
<?php <?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
$method = $_SERVER['REQUEST_METHOD']; require_once __DIR__ . '/../includes/supabase.php';
if ($method === 'GET') { $db = supabaseService();
$category = $_GET['category'] ?? '';
$endpoint = 'system_config?select=key,value,category&is_secret=eq.false'; $flags = $db->get('feature_flags', ['select' => 'key,value,is_enabled']);
if ($category) { $plugins = $db->get('game_plugins', ['select' => '*', 'is_enabled' => 'eq.true']);
$endpoint .= '&category=eq.' . urlencode($category);
} $response = [
'features' => [],
'games' => [],
'version' => '1.0.0'
];
$res = supabase_rest('GET', $endpoint); if (is_array($flags) && !isset($flags['error'])) {
$config = []; foreach ($flags as $f) {
if (!empty($res['data'])) { if ($f['is_enabled'] ?? false) {
foreach ($res['data'] as $row) { $response['features'][$f['key']] = $f['value'] ?? true;
$config[$row['key']] = json_decode($row['value'], true) ?? $row['value'];
} }
} }
echo json_encode(['config' => $config]);
} else {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
} }
if (is_array($plugins) && !isset($plugins['error'])) {
$response['games'] = $plugins;
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
<?php <?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
$token = get_auth_token(); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if (!$token) { require_once __DIR__ . '/../includes/supabase.php';
http_response_code(401); require_once __DIR__ . '/../includes/auth.php';
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD']; $token = requireAuth();
$userId = getUserId($token);
$input = getInput();
$action = $input['action'] ?? 'claim';
if ($method === 'POST') { $db = supabase($token);
$profileRes = supabase_rest('GET', 'profiles?select=id,coins,xp,level,daily_streak,last_daily_reward', [], $token);
if (empty($profileRes['data'])) { if ($action === 'claim') {
http_response_code(400); $profile = $db->getOne('profiles', ['id' => 'eq.' . $userId, 'select' => 'coins,last_daily_claim,daily_streak']);
echo json_encode(['error' => 'profile not found']);
exit;
}
$profile = $profileRes['data'][0]; if (!$profile || isset($profile['error'])) jsonError('Profile not found');
$lastClaim = $profile['last_daily_reward'] ?? null;
$lastClaim = $profile['last_daily_claim'] ?? null;
$today = date('Y-m-d'); $today = date('Y-m-d');
if ($lastClaim === $today) { if ($lastClaim && substr($lastClaim, 0, 10) === $today) {
echo json_encode(['error' => 'already_claimed', 'message' => 'لقد جمعت المكافأة اليوم']); jsonError('Already claimed today');
exit;
} }
$streak = ($profile['daily_streak'] ?? 0) + 1;
$yesterday = date('Y-m-d', strtotime('-1 day')); $yesterday = date('Y-m-d', strtotime('-1 day'));
$streak = ($lastClaim === $yesterday) ? ($profile['daily_streak'] ?? 0) + 1 : 1; if ($lastClaim && substr($lastClaim, 0, 10) !== $yesterday) {
$streak = 1;
// Load reward config from system_config
$cfgRes = supabase_rest('GET', 'system_config?select=key,value&key=in.(daily_reward_base,daily_reward_streak_bonus)', [], SUPABASE_SERVICE_KEY);
$rewardBase = 50;
$rewardBonus = 10;
if (!empty($cfgRes['data'])) {
foreach ($cfgRes['data'] as $cfg) {
if ($cfg['key'] === 'daily_reward_base') $rewardBase = (int)$cfg['value'];
if ($cfg['key'] === 'daily_reward_streak_bonus') $rewardBonus = (int)$cfg['value'];
}
} }
$reward = $rewardBase + ($streak - 1) * $rewardBonus;
$newCoins = ($profile['coins'] ?? 0) + $reward;
// XP award for daily claim $baseReward = 50;
$dailyXp = 25; $streakBonus = min($streak * 10, 100);
$newXp = (int)($profile['xp'] ?? 0) + $dailyXp; $totalCoins = $baseReward + $streakBonus;
$currentLevel = (int)($profile['level'] ?? 1);
$profileUpdate = [ $newCoins = ($profile['coins'] ?? 0) + $totalCoins;
'coins' => $newCoins,
'xp' => $newXp,
'daily_streak' => $streak,
'last_daily_reward' => $today,
];
// Check for level up
$levelRes = supabase_rest('GET', "xp_levels?level=eq." . ($currentLevel + 1) . "&select=level,xp_required,reward_coins", [], SUPABASE_SERVICE_KEY);
$levelUpCoinsBonus = 0;
if (!empty($levelRes['data']) && $newXp >= (int)$levelRes['data'][0]['xp_required']) {
$newLevel = (int)$levelRes['data'][0]['level'];
$levelUpCoinsBonus = (int)($levelRes['data'][0]['reward_coins'] ?? 0);
$profileUpdate['level'] = $newLevel;
$newCoins += $levelUpCoinsBonus;
$profileUpdate['coins'] = $newCoins;
}
supabase_rest('PATCH', "profiles?id=eq.{$profile['id']}", $profileUpdate, SUPABASE_SERVICE_KEY); $db->update('profiles', [
'coins' => $newCoins,
'last_daily_claim' => date('c'),
'daily_streak' => $streak
], ['id' => 'eq.' . $userId]);
// Log economy_transaction for daily reward coins $sdb = supabaseService();
supabase_rest('POST', 'economy_transactions', [ $sdb->insert('economy_transactions', [
'player_id' => $profile['id'], 'player_id' => $userId,
'type' => 'daily_reward', 'type' => 'daily_reward',
'currency' => 'coins', 'currency' => 'coins',
'amount' => $reward, 'amount' => $totalCoins,
'balance_after' => $newCoins - $levelUpCoinsBonus, 'balance_after' => $newCoins,
'reason' => "Daily reward day {$streak}", 'description' => "Daily reward (day {$streak})"
'source_id' => null, ]);
], SUPABASE_SERVICE_KEY);
// Log level up reward separately if applicable
if ($levelUpCoinsBonus > 0) {
supabase_rest('POST', 'economy_transactions', [
'player_id' => $profile['id'],
'type' => 'level_up_reward',
'currency' => 'coins',
'amount' => $levelUpCoinsBonus,
'balance_after' => $newCoins,
'reason' => 'Level up reward',
'source_id' => null,
], SUPABASE_SERVICE_KEY);
}
echo json_encode([ jsonResponse([
'ok' => true, 'coins' => $totalCoins,
'reward' => $reward,
'streak' => $streak, 'streak' => $streak,
'coins' => $newCoins, 'total_coins' => $newCoins
'xp_awarded' => $dailyXp,
'new_xp' => $newXp,
]); ]);
} else {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
} }
jsonError('Invalid action');
This diff is collapsed.
<?php <?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
$token = get_auth_token(); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if (!$token) { require_once __DIR__ . '/../includes/supabase.php';
http_response_code(401); require_once __DIR__ . '/../includes/auth.php';
echo json_encode(['error' => 'unauthorized']);
exit; $token = requireAuth();
} $userId = getUserId($token);
$db = supabase($token);
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
if ($method === 'GET') { if ($method === 'GET') {
$action = $_GET['action'] ?? 'list'; $action = $_GET['action'] ?? 'list';
if ($action === 'list') {
$friends = $db->get('friendships', [
'or' => "(requester_id.eq.{$userId},addressee_id.eq.{$userId})",
'status' => 'eq.accepted',
'select' => 'id,requester_id,addressee_id'
]);
if (!is_array($friends) || isset($friends['error'])) {
jsonResponse(['friends' => []]);
}
$friendIds = [];
foreach ($friends as $f) {
$friendIds[] = $f['requester_id'] === $userId ? $f['addressee_id'] : $f['requester_id'];
}
if (empty($friendIds)) {
jsonResponse(['friends' => []]);
}
$idList = implode(',', $friendIds);
$profiles = $db->get('profiles', [
'id' => "in.({$idList})",
'select' => 'id,username,display_name,avatar_url,is_online,level'
]);
jsonResponse(['friends' => $profiles ?: []]);
}
switch ($action) { if ($action === 'pending') {
case 'list': $pending = $db->get('friendships', [
$res = supabase_rest('GET', 'friends?select=*,friend:profiles!friend_id(id,username,display_name,elo_blitz,is_online)&status=eq.accepted', [], $token); 'addressee_id' => 'eq.' . $userId,
$friends = []; 'status' => 'eq.pending',
if ($res['status'] === 200 && is_array($res['data'])) { 'select' => 'id,requester_id'
foreach ($res['data'] as $row) { ]);
if (isset($row['friend'])) { jsonResponse(['pending' => $pending ?: []]);
$friends[] = $row['friend'];
}
}
}
echo json_encode(['friends' => $friends]);
break;
case 'requests':
$res = supabase_rest('GET', 'friends?select=*,sender:profiles!user_id(id,username,display_name)&status=eq.pending', [], $token);
$requests = [];
if ($res['status'] === 200 && is_array($res['data'])) {
foreach ($res['data'] as $row) {
if (isset($row['sender'])) {
$requests[] = array_merge($row['sender'], ['id' => $row['id']]);
}
}
}
echo json_encode(['requests' => $requests]);
break;
case 'search':
$q = $_GET['q'] ?? '';
if (strlen($q) < 2) {
echo json_encode(['players' => []]);
break;
}
$res = supabase_rest('GET', 'profiles?select=id,username,display_name,elo_blitz&or=(username.ilike.*' . urlencode($q) . '*,display_name.ilike.*' . urlencode($q) . '*)&limit=10', [], $token);
echo json_encode(['players' => $res['data'] ?? []]);
break;
} }
} elseif ($method === 'POST') { }
$input = json_decode(file_get_contents('php://input'), true);
if ($method === 'POST') {
$input = getInput();
$action = $input['action'] ?? ''; $action = $input['action'] ?? '';
// Get current user info for notifications if ($action === 'request') {
$currentUser = supabase_auth('user', [], $token, 'GET'); $targetId = $input['target_id'] ?? '';
$currentUserId = $currentUser['data']['id'] ?? null; if (!$targetId) jsonError('target_id required');
switch ($action) { $result = $db->insert('friendships', [
case 'add': 'requester_id' => $userId,
$userId = $input['user_id'] ?? ''; 'addressee_id' => $targetId,
$res = supabase_rest('POST', 'friends', [ 'status' => 'pending'
'friend_id' => $userId, ]);
'status' => 'pending' if (isset($result['error'])) jsonError($result['error']);
], $token); jsonResponse(['success' => true]);
}
// Send notification to the recipient
if ($currentUserId) { if ($action === 'accept') {
$profileRes = supabase_rest('GET', "profiles?id=eq.{$currentUserId}&select=display_name,username", [], SUPABASE_SERVICE_KEY); $friendshipId = $input['friendship_id'] ?? '';
$senderName = $profileRes['data'][0]['display_name'] ?? $profileRes['data'][0]['username'] ?? 'لاعب'; if (!$friendshipId) jsonError('friendship_id required');
supabase_rest('POST', 'notifications', [
'user_id' => $userId, $result = $db->update('friendships', ['status' => 'accepted'], [
'type' => 'friend_request', 'id' => 'eq.' . $friendshipId,
'title' => 'طلب صداقة', 'addressee_id' => 'eq.' . $userId
'title_ar' => 'طلب صداقة', ]);
'body' => "{$senderName} أرسل لك طلب صداقة", jsonResponse(['success' => true]);
'body_ar' => "{$senderName} أرسل لك طلب صداقة",
'data' => json_encode(['requester_id' => $currentUserId]),
], SUPABASE_SERVICE_KEY);
}
echo json_encode(['ok' => true]);
break;
case 'accept':
$requestId = $input['request_id'] ?? '';
// Get the friend request to find the original sender
$friendReq = supabase_rest('GET', "friends?id=eq.{$requestId}&select=user_id", [], SUPABASE_SERVICE_KEY);
$requesterId = $friendReq['data'][0]['user_id'] ?? null;
supabase_rest('PATCH', "friends?id=eq.{$requestId}", [
'status' => 'accepted',
'accepted_at' => date('c')
], $token);
// Notify the original requester that the request was accepted
if ($requesterId && $currentUserId) {
$profileRes = supabase_rest('GET', "profiles?id=eq.{$currentUserId}&select=display_name,username", [], SUPABASE_SERVICE_KEY);
$acceptorName = $profileRes['data'][0]['display_name'] ?? $profileRes['data'][0]['username'] ?? 'لاعب';
supabase_rest('POST', 'notifications', [
'user_id' => $requesterId,
'type' => 'friend_accepted',
'title' => 'تم قبول طلب الصداقة',
'title_ar' => 'تم قبول طلب الصداقة',
'body' => "{$acceptorName} قبل طلب صداقتك",
'body_ar' => "{$acceptorName} قبل طلب صداقتك",
'data' => json_encode(['friend_id' => $currentUserId]),
], SUPABASE_SERVICE_KEY);
}
echo json_encode(['ok' => true]);
break;
case 'reject':
$requestId = $input['request_id'] ?? '';
supabase_rest('DELETE', "friends?id=eq.{$requestId}", [], $token);
echo json_encode(['ok' => true]);
break;
case 'remove':
$friendId = $input['friend_id'] ?? '';
supabase_rest('DELETE', "friends?friend_id=eq.{$friendId}", [], $token);
echo json_encode(['ok' => true]);
break;
default:
echo json_encode(['error' => 'invalid action']);
} }
} }
if ($method === 'DELETE') {
$input = getInput();
$targetId = $input['target_id'] ?? '';
if (!$targetId) jsonError('target_id required');
$db->delete('friendships', [
'or' => "(and(requester_id.eq.{$userId},addressee_id.eq.{$targetId}),and(requester_id.eq.{$targetId},addressee_id.eq.{$userId}))"
]);
jsonResponse(['success' => true]);
}
jsonError('Invalid request', 400);
This diff is collapsed.
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$res = supabase_rest('GET', 'game_plugins?select=game_key,name,name_ar,description_ar,icon_url,banner_url,is_enabled,is_beta,min_players,max_players,supports_ranked,supports_bot,default_time_controls,config,sort_order&order=sort_order.asc');
$games = $res['data'] ?? [];
echo json_encode(['games' => $games]);
} else {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?php <?php
define('APP_NAME', 'EL3AB');
define('APP_URL', 'https://el3ab-player.caprover.al-arcade.com');
define('APP_VERSION', '2.0.0');
define('SUPABASE_URL', 'https://safe-supabase-kong.caprover.al-arcade.com'); define('SUPABASE_URL', 'https://safe-supabase-kong.caprover.al-arcade.com');
define('SUPABASE_ANON_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84'); define('SUPABASE_ANON_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84');
define('SUPABASE_SERVICE_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4'); define('SUPABASE_SERVICE_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4');
define('SUPABASE_REST', SUPABASE_URL . '/rest/v1');
define('SUPABASE_AUTH', SUPABASE_URL . '/auth/v1');
define('SUPABASE_STORAGE', SUPABASE_URL . '/storage/v1');
define('STOCKFISH_API', 'https://stockfishapi.caprover.al-arcade.com'); define('STOCKFISH_API', 'https://stockfishapi.caprover.al-arcade.com');
define('STOCKFISH_MGMT_KEY', 'sk-alarc-stockfish-mgmt-2024'); define('SWISS_API', 'https://swissapi.caprover.al-arcade.com/api/v1');
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
const listeners = {};
export function on(event, fn) {
if (!listeners[event]) listeners[event] = [];
listeners[event].push(fn);
return () => off(event, fn);
}
export function off(event, fn) {
if (!listeners[event]) return;
listeners[event] = listeners[event].filter(f => f !== fn);
}
export function emit(event, data) {
if (!listeners[event]) return;
listeners[event].forEach(fn => fn(data));
}
export function once(event, fn) {
const unsub = on(event, (data) => {
unsub();
fn(data);
});
return unsub;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import * as scene from '../../core/scene.js';
import { mountSplash } from './scenes/splash.js';
import { mountLogin } from './scenes/login.js';
import { mountRegister } from './scenes/register.js';
scene.register('auth-splash', mountSplash);
scene.register('auth-login', mountLogin);
scene.register('auth-register', mountRegister);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import * as scene from '../../core/scene.js';
import { mountGame } from './scenes/game.js';
import { mountResult } from './scenes/result.js';
scene.register('chess-game', mountGame);
scene.register('chess-result', mountResult);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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