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

feat: Lichess time controls + ELO from 0 + bot games unrated + premove fix

Time Controls:
- 4 categories: Bullet (1+0, 1+1, 2+1), Blitz (3+0, 3+2, 5+0, 5+3),
  Rapid (10+0, 10+5, 15+10, 20+0), Classical (30+0, 30+20, 45+0, 60+0)
- Category tabs with dynamic option switching
- Both multiplayer and custom game sections

ELO:
- Default starts at 0 (was 1200)
- DB columns altered: DEFAULT 0
- Floor at 0 (was 100)
- FIDE calculation unchanged (K-factor based on games played)

Bot games:
- Always rated=false (removed toggle)
- Shows "مباراة تدريبية (غير مصنفة)" instead of rated toggle
- Quick match also unrated

Premove:
- Selected square now visually highlighted during opponent's turn
- premove-from class applied immediately for visual feedback
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 6e2c4b66
...@@ -221,7 +221,7 @@ switch ($action) { ...@@ -221,7 +221,7 @@ switch ($action) {
$tcType = 'rapid'; $tcType = 'rapid';
} }
$playerElo = (int)($profile[$eloField] ?? 1200); $playerElo = (int)($profile[$eloField] ?? 0);
$gamesPlayed = (int)($profile['games_played'] ?? $profile['total_games_played'] ?? 0); $gamesPlayed = (int)($profile['games_played'] ?? $profile['total_games_played'] ?? 0);
$kFactor = ($gamesPlayed < 30) ? $kNew : $kEstablished; $kFactor = ($gamesPlayed < 30) ? $kNew : $kEstablished;
...@@ -251,7 +251,7 @@ switch ($action) { ...@@ -251,7 +251,7 @@ switch ($action) {
$expected = 1.0 / (1.0 + pow(10, ($botElo - $playerElo) / 400.0)); $expected = 1.0 / (1.0 + pow(10, ($botElo - $playerElo) / 400.0));
$actual = ($result === 'win') ? 1.0 : (($result === 'draw') ? 0.5 : 0.0); $actual = ($result === 'win') ? 1.0 : (($result === 'draw') ? 0.5 : 0.0);
$ratingChange = (int)round($kFactor * ($actual - $expected)); $ratingChange = (int)round($kFactor * ($actual - $expected));
$newElo = max(100, $playerElo + $ratingChange); $newElo = max(0, $playerElo + $ratingChange);
$profileUpdate[$eloField] = $newElo; $profileUpdate[$eloField] = $newElo;
// INSERT into rating_history // INSERT into rating_history
......
<?php $pageTitle = 'EL3AB - العب'; ?> <?php $pageTitle = 'EL3AB - العب'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<style>
.tc-categories {
display: flex;
gap: 4px;
margin-bottom: 8px;
}
.tc-cat {
flex: 1;
padding: 6px 4px;
font-size: 11px;
font-weight: 600;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-3);
color: var(--text-2);
cursor: pointer;
text-align: center;
transition: all 0.15s;
}
.tc-cat.active {
background: var(--gold);
color: var(--text-inverse);
border-color: var(--gold);
}
.tc-cat:hover:not(.active) {
background: var(--bg-2);
}
.tc-options {
flex-wrap: wrap;
}
</style>
<div class="lobby-page"> <div class="lobby-page">
<a href="/games" class="breadcrumb"> <a href="/games" class="breadcrumb">
...@@ -28,11 +60,17 @@ ...@@ -28,11 +60,17 @@
</div> </div>
</div> </div>
<div class="tab-group" id="mp-time-tabs"> <!-- Time Control Categories -->
<button class="tab" data-tc="bullet_1_0" data-time="60000" data-inc="0">1 د</button> <div class="tc-categories" id="mp-tc-categories">
<button class="tab" data-tc="blitz_3_0" data-time="180000" data-inc="0">3 د</button> <button class="tc-cat active" data-cat="bullet">⚡ Bullet</button>
<button class="tab active" data-tc="blitz_5_0" data-time="300000" data-inc="0">5 د</button> <button class="tc-cat" data-cat="blitz">🔥 Blitz</button>
<button class="tab" data-tc="rapid_10_0" data-time="600000" data-inc="0">10 د</button> <button class="tc-cat" data-cat="rapid">⏱ Rapid</button>
<button class="tc-cat" data-cat="classical">♟ Classical</button>
</div>
<div class="tab-group tc-options" id="mp-time-tabs">
<button class="tab" data-tc="bullet_1_0" data-time="60000" data-inc="0">1+0</button>
<button class="tab active" data-tc="bullet_1_1" data-time="60000" data-inc="1000">1+1</button>
<button class="tab" data-tc="bullet_2_1" data-time="120000" data-inc="1000">2+1</button>
</div> </div>
<button class="btn btn-gold btn-block btn-lg" onclick="startMultiplayer()"> <button class="btn btn-gold btn-block btn-lg" onclick="startMultiplayer()">
...@@ -78,11 +116,16 @@ ...@@ -78,11 +116,16 @@
<!-- Time Control --> <!-- Time Control -->
<div> <div>
<label class="input-label">التوقيت</label> <label class="input-label">التوقيت</label>
<div class="tab-group" id="time-tabs"> <div class="tc-categories" id="custom-tc-categories">
<button class="tab" data-time="180" data-inc="0">3 د</button> <button class="tc-cat active" data-cat="bullet">⚡ Bullet</button>
<button class="tab active" data-time="300" data-inc="0">5 د</button> <button class="tc-cat" data-cat="blitz">🔥 Blitz</button>
<button class="tab" data-time="600" data-inc="0">10 د</button> <button class="tc-cat" data-cat="rapid">⏱ Rapid</button>
<button class="tab" data-time="300" data-inc="3">5|3</button> <button class="tc-cat" data-cat="classical">♟ Classical</button>
</div>
<div class="tab-group tc-options" id="time-tabs">
<button class="tab" data-time="60" data-inc="0">1+0</button>
<button class="tab active" data-time="60" data-inc="1">1+1</button>
<button class="tab" data-time="120" data-inc="1">2+1</button>
</div> </div>
</div> </div>
...@@ -104,14 +147,8 @@ ...@@ -104,14 +147,8 @@
</select> </select>
</div> </div>
<!-- Rated Toggle --> <!-- Note: Bot games are never rated -->
<div class="lobby-toggle-row"> <p class="text-muted text-sm" style="text-align:center;">مباراة تدريبية (غير مصنفة)</p>
<span>مباراة مصنفة</span>
<label class="toggle">
<input type="checkbox" id="rated-toggle" checked>
<span class="toggle-slider"></span>
</label>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startCustomGame()"> <button class="btn btn-gold btn-block btn-lg" onclick="startCustomGame()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg> <svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
...@@ -180,16 +217,77 @@ function startQuickMatch() { ...@@ -180,16 +217,77 @@ function startQuickMatch() {
: ['nour']; : ['nour'];
const bot = bots[Math.floor(Math.random() * bots.length)]; const bot = bots[Math.floor(Math.random() * bots.length)];
const color = Math.random() < 0.5 ? 'w' : 'b'; const color = Math.random() < 0.5 ? 'w' : 'b';
window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=300&inc=0&rated=true'; window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=300&inc=0&rated=false';
} }
function startCustomGame() { function startCustomGame() {
const { time, inc } = getSelectedTime(); const { time, inc } = getSelectedTime();
const color = getSelectedColor(); const color = getSelectedColor();
const bot = document.getElementById('bot-select').value; const bot = document.getElementById('bot-select').value;
const rated = document.getElementById('rated-toggle').checked; window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=' + time + '&inc=' + inc + '&rated=false';
window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=' + time + '&inc=' + inc + '&rated=' + rated;
} }
// Time control category switching
const TC_OPTIONS = {
bullet: [
{ time: '60', inc: '0', label: '1+0', tc: 'bullet_1_0', timeMs: '60000', incMs: '0' },
{ time: '60', inc: '1', label: '1+1', tc: 'bullet_1_1', timeMs: '60000', incMs: '1000' },
{ time: '120', inc: '1', label: '2+1', tc: 'bullet_2_1', timeMs: '120000', incMs: '1000' }
],
blitz: [
{ time: '180', inc: '0', label: '3+0', tc: 'blitz_3_0', timeMs: '180000', incMs: '0' },
{ time: '180', inc: '2', label: '3+2', tc: 'blitz_3_2', timeMs: '180000', incMs: '2000' },
{ time: '300', inc: '0', label: '5+0', tc: 'blitz_5_0', timeMs: '300000', incMs: '0' },
{ time: '300', inc: '3', label: '5+3', tc: 'blitz_5_3', timeMs: '300000', incMs: '3000' }
],
rapid: [
{ time: '600', inc: '0', label: '10+0', tc: 'rapid_10_0', timeMs: '600000', incMs: '0' },
{ time: '600', inc: '5', label: '10+5', tc: 'rapid_10_5', timeMs: '600000', incMs: '5000' },
{ time: '900', inc: '10', label: '15+10', tc: 'rapid_15_10', timeMs: '900000', incMs: '10000' },
{ time: '1200', inc: '0', label: '20+0', tc: 'rapid_20_0', timeMs: '1200000', incMs: '0' }
],
classical: [
{ time: '1800', inc: '0', label: '30+0', tc: 'classical_30_0', timeMs: '1800000', incMs: '0' },
{ time: '1800', inc: '20', label: '30+20', tc: 'classical_30_20', timeMs: '1800000', incMs: '20000' },
{ time: '2700', inc: '0', label: '45+0', tc: 'classical_45_0', timeMs: '2700000', incMs: '0' },
{ time: '3600', inc: '0', label: '60+0', tc: 'classical_60_0', timeMs: '3600000', incMs: '0' }
]
};
function setupTcCategories(catContainerId, tabContainerId, isMultiplayer) {
const catContainer = document.getElementById(catContainerId);
const tabContainer = document.getElementById(tabContainerId);
if (!catContainer || !tabContainer) return;
catContainer.querySelectorAll('.tc-cat').forEach(function(cat) {
cat.addEventListener('click', function() {
catContainer.querySelectorAll('.tc-cat').forEach(function(c) { c.classList.remove('active'); });
cat.classList.add('active');
var category = cat.dataset.cat;
var options = TC_OPTIONS[category];
var html = '';
options.forEach(function(opt, i) {
var cls = i === 0 ? ' active' : '';
if (isMultiplayer) {
html += '<button class="tab' + cls + '" data-tc="' + opt.tc + '" data-time="' + opt.timeMs + '" data-inc="' + opt.incMs + '">' + opt.label + '</button>';
} else {
html += '<button class="tab' + cls + '" data-time="' + opt.time + '" data-inc="' + opt.inc + '">' + opt.label + '</button>';
}
});
tabContainer.innerHTML = html;
// Rebind tab clicks
tabContainer.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
tabContainer.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
});
});
});
});
}
setupTcCategories('mp-tc-categories', 'mp-time-tabs', true);
setupTcCategories('custom-tc-categories', 'time-tabs', false);
</script> </script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
...@@ -377,11 +377,16 @@ const Board = { ...@@ -377,11 +377,16 @@ const Board = {
if (isPlayerTurn) { if (isPlayerTurn) {
this.legalMoves = this.position ? this.position.moves({ square: sqName, verbose: true }) : []; this.legalMoves = this.position ? this.position.moves({ square: sqName, verbose: true }) : [];
} else { } else {
// Pre-move: show all pseudo-legal squares (we highlight them differently)
this.legalMoves = []; this.legalMoves = [];
} }
this.updateHighlights(); this.updateHighlights();
// For premove: if not player's turn, mark the square as selected visually
if (!isPlayerTurn) {
const selSq = this.getSquareEl(sqName);
if (selSq) selSq.classList.add('premove-from');
}
// Start drag // Start drag
isDragging = true; isDragging = true;
this.dragStartSquare = sqName; this.dragStartSquare = sqName;
......
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