Commit bc768f8b authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(live): give Swiss tournaments their player names back

The external Swiss service returns pairings carrying whitePlayerId and
blackPlayerId and no names at all, so every board on a Swiss tournament
rendered "لاعب" against "لاعب" and the rounds timeline showed a column of
question marks. Its standings response does carry names, so the two are joined
in LiveDataService and both engines' pairings are normalised to one shape
before any view sees them — which is also where the array-into-string fatals
on this page came from.

Its stats were hardcoded zeros: a finished five-round, 64-player championship
reported that no game had ever been played, and the vitals section fell through
to "nothing has been played yet". The counts are now derived from the rounds.

Tiebreaks arrive as an ordered tiebreakValues list rather than named columns;
read either, and drop the column entirely when a tournament has none rather
than printing a column of em-dashes.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 4a2cb04f
...@@ -103,6 +103,89 @@ class LiveDataService ...@@ -103,6 +103,89 @@ class LiveDataService
return []; return [];
} }
/** id => display name, for whichever engine is running this tournament. */
private array $nameCache = [];
private function nameMap(array $tournament): array
{
$id = $tournament['id'];
if (isset($this->nameCache[$id])) return $this->nameCache[$id];
$map = [];
// The Swiss service returns pairings carrying player ids and nothing else
// — no names at all — so every board on a Swiss tournament rendered as
// "لاعب" versus "لاعب". Its standings response does carry names, so the
// two are joined here rather than in each view.
if (!empty($tournament['swiss_api_tournament_id'])) {
$response = SwissApiService::getStandings($tournament['swiss_api_tournament_id']);
if (SwissApiService::isSuccess($response)) {
foreach (self::unwrap($response['body'] ?? []) as $row) {
if (!is_array($row)) continue;
$pid = (string)($row['playerId'] ?? $row['player_id'] ?? $row['id'] ?? '');
$name = (string)($row['name'] ?? $row['playerName'] ?? '');
if ($pid !== '' && $name !== '') $map[$pid] = $name;
}
}
}
// Registered players fill any gap — a player who has not been paired yet
// is absent from standings but still needs a name in the field list.
foreach ($this->native->players($id) as $pid => $p) {
if (!isset($map[$pid]) && !empty($p['name'])) $map[$pid] = $p['name'];
}
return $this->nameCache[$id] = $map;
}
/**
* Bring the Swiss service's pairing rows into the one shape the views read.
*
* The two engines disagree about everything: whitePlayerId vs a white object,
* boardNumber vs board, isBye vs is_bye. Normalising here means a view handles
* one shape instead of guessing, which is what the array-into-string fatals on
* this page came from.
*/
private function normaliseSwissPairings(array $tournament, array $rows): array
{
$names = $this->nameMap($tournament);
$out = [];
foreach ($rows as $i => $r) {
if (!is_array($r)) continue;
$whiteId = (string)($r['whitePlayerId'] ?? $r['white_player_id'] ?? '');
$blackId = (string)($r['blackPlayerId'] ?? $r['black_player_id'] ?? '');
$out[] = [
'board' => (int)($r['boardNumber'] ?? $r['board_number'] ?? $r['board'] ?? ($i + 1)),
'white' => ['id' => $whiteId, 'name' => $names[$whiteId] ?? 'لاعب'],
'black' => $blackId === ''
? null
: ['id' => $blackId, 'name' => $names[$blackId] ?? 'لاعب'],
'white_id' => $whiteId,
'black_id' => $blackId,
'result' => $this->normaliseResult($r['result'] ?? null),
'is_bye' => !empty($r['isBye']) || !empty($r['is_bye']),
'reason' => !empty($r['isForfeit']) ? 'forfeit' : null,
];
}
return $out;
}
/** Every spelling either engine uses, reduced to the native codes. */
private function normaliseResult($result): ?string
{
if ($result === null || $result === '' || $result === 'not_played') return null;
return match ((string)$result) {
'white_wins', '1-0', '1 - 0', 'white', 'white_forfeit_win' => 'white_wins',
'black_wins', '0-1', '0 - 1', 'black', 'black_forfeit_win' => 'black_wins',
'draw', '1/2-1/2', '0.5-0.5', '½-½' => 'draw',
default => null,
};
}
public function getCurrentPairings(array $tournament): array public function getCurrentPairings(array $tournament): array
{ {
if ($this->native->hasNativeRounds($tournament['id'])) { if ($this->native->hasNativeRounds($tournament['id'])) {
...@@ -125,7 +208,7 @@ class LiveDataService ...@@ -125,7 +208,7 @@ class LiveDataService
return [ return [
'round_number' => $currentRound['round_number'], 'round_number' => $currentRound['round_number'],
'round_id' => $currentRound['id'], 'round_id' => $currentRound['id'],
'pairings' => self::unwrap($response['body'] ?? []), 'pairings' => $this->normaliseSwissPairings($tournament, self::unwrap($response['body'] ?? [])),
]; ];
} }
} }
...@@ -154,7 +237,7 @@ class LiveDataService ...@@ -154,7 +237,7 @@ class LiveDataService
if (!empty($tournament['swiss_api_tournament_id']) && !empty($round['swiss_round_id'])) { if (!empty($tournament['swiss_api_tournament_id']) && !empty($round['swiss_round_id'])) {
$response = SwissApiService::getPairings($round['swiss_round_id']); $response = SwissApiService::getPairings($round['swiss_round_id']);
if (SwissApiService::isSuccess($response)) { if (SwissApiService::isSuccess($response)) {
$pairings = self::unwrap($response['body'] ?? []); $pairings = $this->normaliseSwissPairings($tournament, self::unwrap($response['body'] ?? []));
} }
} }
$allRounds[] = [ $allRounds[] = [
...@@ -253,13 +336,32 @@ class LiveDataService ...@@ -253,13 +336,32 @@ class LiveDataService
$completedRounds = count(array_filter($rounds, fn($r) => ($r['status'] ?? '') === 'completed')); $completedRounds = count(array_filter($rounds, fn($r) => ($r['status'] ?? '') === 'completed'));
// Zeroes here meant a finished five-round Swiss tournament reported that
// no game had ever been played. The counts come from the rounds.
$games = 0; $done = 0; $decisive = 0; $draws = 0; $byes = 0;
foreach ($this->getAllRoundsPairings($tournament) as $round) {
foreach ($round['pairings'] ?? [] as $p) {
if (!empty($p['is_bye'])) { $byes++; continue; }
$games++;
$result = $p['result'] ?? null;
if ($result === null) continue;
$done++;
if ($result === 'draw') $draws++; else $decisive++;
}
}
return [ return [
'players' => count($players), 'players' => count($players),
'rounds_total' => (int)($tournament['swiss_rounds'] ?? $tournament['rounds_total'] ?? count($rounds)), 'rounds_total' => (int)($tournament['swiss_rounds'] ?? $tournament['rounds_total'] ?? count($rounds)),
'rounds_played' => $completedRounds, 'rounds_played' => $completedRounds,
'current_round' => (int)($tournament['current_round'] ?? 0), 'current_round' => (int)($tournament['current_round'] ?? 0),
'games_total' => 0, 'games_completed' => 0, 'games_live' => 0, 'games_total' => $games,
'decisive' => 0, 'draws' => 0, 'byes' => 0, 'progress' => 0, 'games_completed' => $done,
'games_live' => max(0, $games - $done),
'decisive' => $decisive,
'draws' => $draws,
'byes' => $byes,
'progress' => $games > 0 ? (int)round($done / $games * 100) : 0,
]; ];
} }
...@@ -280,7 +382,8 @@ class LiveDataService ...@@ -280,7 +382,8 @@ class LiveDataService
if (empty($tournament['swiss_api_tournament_id']) || empty($round['swiss_round_id'])) continue; if (empty($tournament['swiss_api_tournament_id']) || empty($round['swiss_round_id'])) continue;
$response = SwissApiService::getPairings($round['swiss_round_id']); $response = SwissApiService::getPairings($round['swiss_round_id']);
if (!SwissApiService::isSuccess($response)) continue; if (!SwissApiService::isSuccess($response)) continue;
foreach (self::unwrap($response['body'] ?? []) as $pairing) { $normalised = $this->normaliseSwissPairings($tournament, self::unwrap($response['body'] ?? []));
foreach ($normalised as $pairing) {
if (!empty($pairing['result'])) { if (!empty($pairing['result'])) {
$results[] = array_merge($pairing, ['round_number' => $round['round_number']]); $results[] = array_merge($pairing, ['round_number' => $round['round_number']]);
} }
......
...@@ -32,7 +32,10 @@ foreach ($standings as $i => $row) { ...@@ -32,7 +32,10 @@ foreach ($standings as $i => $row) {
'wins' => (int)$pick($row, ['wins'], 0), 'wins' => (int)$pick($row, ['wins'], 0),
'draws' => (int)$pick($row, ['draws'], 0), 'draws' => (int)$pick($row, ['draws'], 0),
'losses' => (int)$pick($row, ['losses'], 0), 'losses' => (int)$pick($row, ['losses'], 0),
'tb' => $pick($row, ['tiebreak1', 'buchholzCut1', 'buchholz'], null), // The Swiss service reports tiebreaks as an ordered list; the native engine
// as named columns. Read whichever is present.
'tb' => $pick($row, ['tiebreak1', 'buchholzCut1', 'buchholz'], null)
?? (is_array($row['tiebreakValues'] ?? null) ? ($row['tiebreakValues'][0] ?? null) : null),
'is_bot' => (bool)$pick($row, ['is_bot'], false), 'is_bot' => (bool)$pick($row, ['is_bot'], false),
'form' => is_array($pick($row, ['results_by_round'], null)) ? $row['results_by_round'] : [], 'form' => is_array($pick($row, ['results_by_round'], null)) ? $row['results_by_round'] : [],
]; ];
...@@ -44,6 +47,13 @@ $initial = function (string $name): string { ...@@ -44,6 +47,13 @@ $initial = function (string $name): string {
}; };
$medals = [1 => '🥇', 2 => '🥈', 3 => '🥉']; $medals = [1 => '🥇', 2 => '🥈', 3 => '🥉'];
$TOP_VISIBLE = 20; $TOP_VISIBLE = 20;
// A whole column of em-dashes is noise. Only show tiebreaks when there are any:
// the external service leaves tiebreakValues empty for plenty of tournaments.
$hasTiebreaks = false;
foreach ($rows as $r) {
if ($r['tb'] !== null && $r['tb'] !== '') { $hasTiebreaks = true; break; }
}
?> ?>
<?php if (empty($rows)): ?> <?php if (empty($rows)): ?>
...@@ -80,7 +90,7 @@ $TOP_VISIBLE = 20; ...@@ -80,7 +90,7 @@ $TOP_VISIBLE = 20;
<th class="col-wins">ف</th> <th class="col-wins">ف</th>
<th class="col-draws">ت</th> <th class="col-draws">ت</th>
<th class="col-losses">خ</th> <th class="col-losses">خ</th>
<th class="col-tb">TB</th> <?php if ($hasTiebreaks): ?><th class="col-tb">TB</th><?php endif; ?>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -133,7 +143,9 @@ $TOP_VISIBLE = 20; ...@@ -133,7 +143,9 @@ $TOP_VISIBLE = 20;
<td class="col-wins"><?= $p['wins'] ?></td> <td class="col-wins"><?= $p['wins'] ?></td>
<td class="col-draws"><?= $p['draws'] ?></td> <td class="col-draws"><?= $p['draws'] ?></td>
<td class="col-losses"><?= $p['losses'] ?></td> <td class="col-losses"><?= $p['losses'] ?></td>
<td class="col-tb"><?= $p['tb'] === null ? '—' : htmlspecialchars((string)$p['tb']) ?></td> <?php if ($hasTiebreaks): ?>
<td class="col-tb"><?= $p['tb'] === null ? '—' : htmlspecialchars((string)$p['tb']) ?></td>
<?php endif; ?>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
......
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