Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Component/Chart/PlayerPuzzleTimesChart.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public function getChart(): Chart
{
$labels = [];
$chartData = [];
$results = $this->puzzlesSorter->sortByFinishedAt($this->results);
$results = $this->puzzlesSorter->sortByFinishedAt(
array_filter($this->results, static fn (SolvedPuzzle $r): bool => $r->time !== null),
);

foreach ($results as $result) {
$chartData[] = $result->time;
Expand Down
56 changes: 56 additions & 0 deletions src/Controller/PlayerPuzzleResultsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace SpeedPuzzling\Web\Controller;

use SpeedPuzzling\Web\Query\GetPlayerProfile;
use SpeedPuzzling\Web\Query\GetPlayerSolvedPuzzles;
use SpeedPuzzling\Web\Query\GetRanking;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class PlayerPuzzleResultsController extends AbstractController
{
public function __construct(
readonly private GetPlayerSolvedPuzzles $getPlayerSolvedPuzzles,
readonly private GetRanking $getRanking,
readonly private GetPlayerProfile $getPlayerProfile,
) {
}

#[Route(
path: '/en/player/{playerId}/puzzle/{puzzleId}/results/{category}',
name: 'player_puzzle_results',
requirements: ['category' => 'solo|duo|team'],
)]
public function __invoke(string $playerId, string $puzzleId, string $category, Request $request): Response
{
$player = $this->getPlayerProfile->byId($playerId);
$results = $this->getPlayerSolvedPuzzles->byPlayerIdPuzzleIdAndCategory($playerId, $puzzleId, $category);

if ($results === []) {
throw $this->createNotFoundException();
}

$ranking = null;
if ($category === 'solo') {
$ranking = $this->getRanking->ofPuzzleForPlayer($puzzleId, $playerId);
}

$templateData = [
'player' => $player,
'results' => $results,
'ranking' => $ranking,
'category' => $category,
];

if ($request->headers->get('Turbo-Frame') === 'modal-frame') {
return $this->render('player-puzzle-results/_modal.html.twig', $templateData);
}

return $this->render('player-puzzle-results/detail.html.twig', $templateData);
}
}
323 changes: 323 additions & 0 deletions src/Query/GetPlayerSolvedPuzzles.php
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,329 @@ public function teamByPlayerId(
}, $data);
}

/**
* @return array<SolvedPuzzle>
* @throws PlayerNotFound
*/
public function byPlayerIdPuzzleIdAndCategory(string $playerId, string $puzzleId, string $category): array
{
return match ($category) {
'solo' => $this->soloByPlayerIdAndPuzzleId($playerId, $puzzleId),
'duo' => $this->duoByPlayerIdAndPuzzleId($playerId, $puzzleId),
'team' => $this->teamByPlayerIdAndPuzzleId($playerId, $puzzleId),
default => [],
};
}

/**
* @return array<SolvedPuzzle>
* @throws PlayerNotFound
*/
private function soloByPlayerIdAndPuzzleId(string $playerId, string $puzzleId): array
{
if (Uuid::isValid($playerId) === false || Uuid::isValid($puzzleId) === false) {
throw new PlayerNotFound();
}

$query = <<<SQL
WITH solved_counts AS (
SELECT
puzzle_id,
COUNT(id) AS solved_times
FROM puzzle_solving_time
WHERE puzzling_type = 'solo'
AND player_id = :playerId
GROUP BY puzzle_id
)
SELECT
puzzle_solving_time.id as time_id,
puzzle.id AS puzzle_id,
puzzle.name AS puzzle_name,
puzzle.alternative_name AS puzzle_alternative_name,
puzzle.image AS puzzle_image,
puzzle_solving_time.seconds_to_solve AS time,
puzzle_solving_time.player_id AS player_id,
pieces_count,
player.name AS player_name,
player.code AS player_code,
player.country AS player_country,
puzzle.identification_number AS puzzle_identification_number,
puzzle_solving_time.comment,
puzzle_solving_time.tracked_at,
finished_at,
manufacturer.name AS manufacturer_name,
puzzle_solving_time.finished_puzzle_photo AS finished_puzzle_photo,
first_attempt,
puzzle_solving_time.unboxed,
solved_counts.solved_times AS solved_times,
competition.id AS competition_id,
competition.shortcut AS competition_shortcut,
competition.name AS competition_name,
competition.slug AS competition_slug,
puzzle_solving_time.suspicious
FROM puzzle_solving_time
INNER JOIN puzzle ON puzzle.id = puzzle_solving_time.puzzle_id
INNER JOIN player ON puzzle_solving_time.player_id = player.id
INNER JOIN manufacturer ON manufacturer.id = puzzle.manufacturer_id
LEFT JOIN solved_counts ON solved_counts.puzzle_id = puzzle_solving_time.puzzle_id
LEFT JOIN competition ON competition.id = puzzle_solving_time.competition_id
WHERE
puzzle_solving_time.player_id = :playerId
AND puzzle_solving_time.puzzle_id = :puzzleId
AND puzzle_solving_time.puzzling_type = 'solo'
ORDER BY COALESCE(puzzle_solving_time.finished_at, puzzle_solving_time.tracked_at) DESC
SQL;

$data = $this->database
->executeQuery($query, [
'playerId' => $playerId,
'puzzleId' => $puzzleId,
])
->fetchAllAssociative();

return array_map(static function (array $row): SolvedPuzzle {
/**
* @var array{
* time_id: string,
* player_id: string,
* player_name: null|string,
* player_code: string,
* player_country: null|string,
* puzzle_id: string,
* puzzle_name: string,
* puzzle_alternative_name: null|string,
* manufacturer_name: string,
* puzzle_image: null|string,
* time: int,
* pieces_count: int,
* comment: null|string,
* tracked_at: string,
* finished_puzzle_photo: null|string,
* puzzle_identification_number: null|string,
* finished_at: null|string,
* first_attempt: bool,
* unboxed: bool,
* solved_times: int,
* competition_id: null|string,
* competition_name: null|string,
* competition_shortcut: null|string,
* competition_slug: null|string,
* suspicious: bool,
* } $row
*/

return SolvedPuzzle::fromDatabaseRow($row);
}, $data);
}

/**
* @return array<SolvedPuzzle>
* @throws PlayerNotFound
*/
private function duoByPlayerIdAndPuzzleId(string $playerId, string $puzzleId): array
{
if (Uuid::isValid($playerId) === false || Uuid::isValid($puzzleId) === false) {
throw new PlayerNotFound();
}

$query = <<<SQL
WITH filtered_pst_ids AS (
SELECT id
FROM puzzle_solving_time
WHERE
(team::jsonb -> 'puzzlers') @> jsonb_build_array(jsonb_build_object('player_id', CAST(:playerId AS UUID)))
AND puzzling_type = 'duo'
AND puzzle_id = :puzzleId
)
SELECT
pst.id as time_id,
puzzle.id AS puzzle_id,
puzzle.name AS puzzle_name,
puzzle.alternative_name AS puzzle_alternative_name,
puzzle.image AS puzzle_image,
pst.seconds_to_solve AS time,
pst.player_id AS player_id,
pieces_count,
finished_puzzle_photo,
tracked_at,
finished_at,
puzzle.identification_number AS puzzle_identification_number,
pst.comment,
manufacturer.name AS manufacturer_name,
pst.team ->> 'team_id' AS team_id,
first_attempt,
pst.unboxed,
competition.id AS competition_id,
competition.shortcut AS competition_shortcut,
competition.name AS competition_name,
competition.slug AS competition_slug,
pst.suspicious
FROM filtered_pst_ids fids
INNER JOIN puzzle_solving_time pst ON pst.id = fids.id
INNER JOIN puzzle ON puzzle.id = pst.puzzle_id
INNER JOIN manufacturer ON manufacturer.id = puzzle.manufacturer_id
LEFT JOIN competition ON competition.id = pst.competition_id
ORDER BY COALESCE(pst.finished_at, pst.tracked_at) DESC
SQL;

$data = $this->database
->executeQuery($query, [
'playerId' => $playerId,
'puzzleId' => $puzzleId,
])
->fetchAllAssociative();

/** @var array<string> $timeIds */
$timeIds = array_column($data, 'time_id');

$players = $this->getTeamPlayers->byIds($timeIds);

return array_map(static function (array $row) use ($players): SolvedPuzzle {
/**
* @var array{
* time_id: string,
* team_id: null|string,
* player_id: string,
* player_name: null,
* player_code: string,
* player_country: null,
* puzzle_id: string,
* puzzle_name: string,
* puzzle_alternative_name: null|string,
* manufacturer_name: string,
* puzzle_image: null|string,
* time: int,
* pieces_count: int,
* comment: null|string,
* finished_puzzle_photo: null|string,
* puzzle_identification_number: null|string,
* tracked_at: string,
* finished_at: null|string,
* first_attempt: bool,
* unboxed: bool,
* competition_id: null|string,
* competition_name: null|string,
* competition_shortcut: null|string,
* competition_slug: null|string,
* suspicious: bool,
* } $row
*/

$row['players'] = $players[$row['time_id']] ?? null;

// Dummy placeholder values
$row['player_name'] = null;
$row['player_code'] = '';
$row['player_country'] = null;

return SolvedPuzzle::fromDatabaseRow($row);
}, $data);
}

/**
* @return array<SolvedPuzzle>
* @throws PlayerNotFound
*/
private function teamByPlayerIdAndPuzzleId(string $playerId, string $puzzleId): array
{
if (Uuid::isValid($playerId) === false || Uuid::isValid($puzzleId) === false) {
throw new PlayerNotFound();
}

$query = <<<SQL
WITH filtered_pst_ids AS (
SELECT id
FROM puzzle_solving_time
WHERE
(team::jsonb -> 'puzzlers') @> jsonb_build_array(jsonb_build_object('player_id', CAST(:playerId AS UUID)))
AND puzzling_type = 'team'
AND puzzle_id = :puzzleId
)
SELECT
pst.id as time_id,
puzzle.id AS puzzle_id,
puzzle.name AS puzzle_name,
puzzle.alternative_name AS puzzle_alternative_name,
puzzle.image AS puzzle_image,
pst.seconds_to_solve AS time,
pst.player_id AS player_id,
pieces_count,
finished_puzzle_photo,
tracked_at,
finished_at,
puzzle.identification_number AS puzzle_identification_number,
pst.comment,
manufacturer.name AS manufacturer_name,
pst.team ->> 'team_id' AS team_id,
first_attempt,
pst.unboxed,
competition.id AS competition_id,
competition.shortcut AS competition_shortcut,
competition.name AS competition_name,
competition.slug AS competition_slug,
pst.suspicious
FROM filtered_pst_ids fids
INNER JOIN puzzle_solving_time pst ON pst.id = fids.id
INNER JOIN puzzle ON puzzle.id = pst.puzzle_id
INNER JOIN manufacturer ON manufacturer.id = puzzle.manufacturer_id
LEFT JOIN competition ON competition.id = pst.competition_id
ORDER BY COALESCE(pst.finished_at, pst.tracked_at) DESC
SQL;

$data = $this->database
->executeQuery($query, [
'playerId' => $playerId,
'puzzleId' => $puzzleId,
])
->fetchAllAssociative();

/** @var array<string> $timeIds */
$timeIds = array_column($data, 'time_id');

$players = $this->getTeamPlayers->byIds($timeIds);

return array_map(static function (array $row) use ($players): SolvedPuzzle {
/**
* @var array{
* time_id: string,
* team_id: null|string,
* player_id: string,
* player_name: null,
* player_code: string,
* player_country: null,
* puzzle_id: string,
* puzzle_name: string,
* puzzle_alternative_name: null|string,
* manufacturer_name: string,
* puzzle_image: null|string,
* time: int,
* pieces_count: int,
* comment: null|string,
* finished_puzzle_photo: null|string,
* puzzle_identification_number: null|string,
* tracked_at: string,
* finished_at: null|string,
* first_attempt: bool,
* unboxed: bool,
* competition_id: null|string,
* competition_name: null|string,
* competition_shortcut: null|string,
* competition_slug: null|string,
* suspicious: bool,
* } $row
*/

$row['players'] = $players[$row['time_id']] ?? null;

// Dummy placeholder values
$row['player_name'] = null;
$row['player_code'] = '';
$row['player_country'] = null;

return SolvedPuzzle::fromDatabaseRow($row);
}, $data);
}

/**
* Count distinct puzzles solved by player (solo or in team)
*
Expand Down
Loading