Commit aa75a0c8 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(tournaments): a double no-show is no longer a free point for Black

When neither player opened their board, the sweep called
tournamentForfeitPairing with the white player's id as the absentee. That
helper awards the point to whichever seat is NOT the one named, so every game
nobody turned up for was recorded as black_wins — a full point, in a rated
event, for a player who also did not appear. It fired on all four boards of a
round during testing.

An unnamed absentee now records the pairing as 'aborted', which
chessResultPoints already scores 0-0, and the standings count it as a loss for
both rather than falling through to the draw branch.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 62db8358
......@@ -134,10 +134,12 @@ if (is_array($running) && !isset($running['error'])) {
// Nobody opened the board at all: forfeit once the grace period is up.
if (!$match) {
if (time() - $roundStarted > $NO_SHOW_SECONDS) {
// Neither player showed. Forfeit the pairing to nobody so the
// round can close; both players score zero.
if (tournamentForfeitPairing($db, $t['id'], $roundNumber, $p['pairing_id'] ?? '', $p['white_id'])) {
$report['forfeited'][] = ['tournament' => $t['id'], 'board' => $p['board'] ?? null, 'reason' => 'no_show'];
// Neither player showed: no board was ever opened. Passing a
// player id here names them as the absentee and hands their
// opponent a full point, so a game nobody turned up for used to
// be a free win for Black. Name nobody; both score zero.
if (tournamentForfeitPairing($db, $t['id'], $roundNumber, $p['pairing_id'] ?? '', '')) {
$report['forfeited'][] = ['tournament' => $t['id'], 'board' => $p['board'] ?? null, 'reason' => 'double_no_show'];
}
}
continue;
......
......@@ -219,7 +219,15 @@ function tournamentStandings($sdb, array $tournament, ?array $rounds = null, ?ar
$state[$white]['opponents'][$black] = ($state[$white]['opponents'][$black] ?? 0) + $wp;
$state[$black]['opponents'][$white] = ($state[$black]['opponents'][$white] ?? 0) + $bp;
if ($wp === 1.0) { $state[$white]['wins']++; $state[$black]['losses']++; }
if ($result === 'aborted') {
// Nobody played it. Zero points each, and it counts as a loss for
// both rather than a draw — chessResultPoints returns 0/0, which the
// draw branch below would otherwise have recorded as half a point's
// worth of standing for two players who never appeared.
$state[$white]['losses']++;
$state[$black]['losses']++;
}
elseif ($wp === 1.0) { $state[$white]['wins']++; $state[$black]['losses']++; }
elseif ($bp === 1.0) { $state[$black]['wins']++; $state[$white]['losses']++; }
else { $state[$white]['draws']++; $state[$black]['draws']++; }
......@@ -801,9 +809,20 @@ function tournamentForfeitPairing($sdb, string $tournamentId, int $roundNumber,
foreach ($pairings as &$p) {
if (($p['pairing_id'] ?? null) !== $pairingId) continue;
if (($p['result'] ?? null) !== null) return false;
$p['result'] = ($p['white_id'] === $absentPlayerId) ? 'black_wins' : 'white_wins';
$p['reason'] = 'forfeit';
$p['forfeited_by'] = $absentPlayerId;
// No absentee named means NEITHER player turned up. Awarding the point to
// whichever seat happened not to be passed in handed a free win to Black on
// every double no-show — a real scoring error in a rated event. A game
// nobody played is worth nothing to anybody.
if ($absentPlayerId === '') {
$p['result'] = 'aborted';
$p['reason'] = 'double_forfeit';
$p['forfeited_by'] = null;
} else {
$p['result'] = ($p['white_id'] === $absentPlayerId) ? 'black_wins' : 'white_wins';
$p['reason'] = 'forfeit';
$p['forfeited_by'] = $absentPlayerId;
}
$p['completed_at'] = gmdate('c');
$changed = true;
break;
......
......@@ -257,6 +257,16 @@ check('rapid_10_0 initial', chessTimeControlMs('rapid_10_0')['initial'] === 6000
check('bogus tc normalised', chessNormaliseTimeControl('standard') === 'rapid_10_0');
check('valid tc preserved', chessNormaliseTimeControl('blitz_5_0') === 'blitz_5_0');
echo "\n=== Forfeits and double no-shows ===\n";
// A game nobody turned up for must be worth nothing to either player. Naming an
// absentee used to be mandatory, which meant a double no-show was recorded as a
// win for whichever seat was not passed in — a free point in a rated event.
[$wp, $bp] = chessResultPoints('aborted');
check('an aborted game scores nothing for white', $wp === 0.0, (string)$wp);
check('an aborted game scores nothing for black', $bp === 0.0, (string)$bp);
check('a one-sided forfeit still awards the point', chessResultPoints('black_wins') === [0.0, 1.0]);
echo "\n";
if ($FAILURES) {
echo count($FAILURES) . " FAILURE(S)\n";
......
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