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
This diff is collapsed.
......@@ -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);
}
}
This diff is collapsed.
......@@ -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