Files
ltbxd-actorle/src/Service/GameGridGenerator.php
thibaud-leclere 273ea49ed0 fix: generate hints about the row actor, not the main actor
Hints should help identify the row actor (to find the highlighted letter),
not reveal the main actor directly. Simplified hint generation: no shared
exclusion pools needed since each row has a different actor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 22:51:22 +02:00

207 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Actor;
use App\Entity\Game;
use App\Entity\GameRow;
use App\Entity\User;
use App\Repository\ActorRepository;
use App\Repository\MovieRepository;
use App\Repository\MovieRoleRepository;
use Doctrine\ORM\EntityManagerInterface;
class GameGridGenerator
{
public function __construct(
private readonly ActorRepository $actorRepository,
private readonly MovieRoleRepository $movieRoleRepository,
private readonly MovieRepository $movieRepository,
private readonly WikidataAwardGateway $wikidataAwardGateway,
private readonly EntityManagerInterface $em,
) {}
public function generate(?User $user = null): Game
{
$mainActor = $this->actorRepository->findOneRandom(4);
$game = new Game();
$game->setMainActor($mainActor);
$game->setUser($user);
$usedActors = [$mainActor->getId()];
$rowOrder = 0;
foreach (str_split(strtolower($mainActor->getName())) as $char) {
if (!preg_match('/[a-z]/', $char)) {
continue;
}
$tryFindActor = 0;
do {
$actor = $this->actorRepository->findOneRandom(4, $char);
++$tryFindActor;
} while (
in_array($actor->getId(), $usedActors)
|| $tryFindActor < 5
);
$usedActors[] = $actor->getId();
$row = new GameRow();
$row->setActor($actor);
$row->setPosition(strpos(strtolower($actor->getName()), $char));
$row->setRowOrder($rowOrder);
$hint = $this->generateHint($actor);
if ($hint !== null) {
$row->setHintType($hint['type']);
$row->setHintData($hint['data']);
}
$game->addRow($row);
++$rowOrder;
}
$this->em->persist($game);
$this->em->flush();
return $game;
}
/**
* Compute display data (grid, width, middle) from a Game entity for the React component.
*
* @return array{grid: list<array{actorName: string, actorId: int, pos: int, hintType: ?string, hintText: ?string}>, width: int, middle: int}
*/
public function computeGridData(Game $game): array
{
$leftSize = 0;
$rightSize = 0;
$grid = [];
$mainActorChars = str_split($game->getMainActor()->getName());
$rows = $game->getRows()->toArray();
$rowIndex = 0;
foreach ($mainActorChars as $char) {
if (!preg_match('/[a-zA-Z]/', $char)) {
$grid[] = [
'separator' => $char,
];
continue;
}
$row = $rows[$rowIndex] ?? null;
++$rowIndex;
if ($row === null) {
continue;
}
$actor = $row->getActor();
$pos = $row->getPosition();
if ($leftSize < $pos) {
$leftSize = $pos;
}
$rightSizeActor = strlen($actor->getName()) - $pos - 1;
if ($rightSize < $rightSizeActor) {
$rightSize = $rightSizeActor;
}
$hintText = $this->resolveHintText($row);
$grid[] = [
'actorName' => $actor->getName(),
'actorId' => $actor->getId(),
'pos' => $pos,
'hintType' => $row->getHintType(),
'hintText' => $hintText,
];
}
return [
'grid' => $grid,
'width' => $rightSize + $leftSize + 1,
'middle' => $leftSize,
];
}
/**
* Generate a single hint for a row actor.
*
* @return array{type: string, data: string}|null
*/
private function generateHint(Actor $rowActor): ?array
{
$types = ['film', 'character', 'award'];
shuffle($types);
foreach ($types as $type) {
$hint = $this->resolveHint($type, $rowActor);
if ($hint !== null) {
return $hint;
}
}
return null;
}
/**
* @return array{type: string, data: string}|null
*/
private function resolveHint(string $type, Actor $rowActor): ?array
{
switch ($type) {
case 'film':
$role = $this->movieRoleRepository->findOneRandomByActor($rowActor->getId());
if ($role === null) {
return null;
}
return ['type' => 'film', 'data' => (string) $role->getMovie()->getId()];
case 'character':
$role = $this->movieRoleRepository->findOneRandomByActor($rowActor->getId());
if ($role === null) {
return null;
}
return ['type' => 'character', 'data' => (string) $role->getId()];
case 'award':
try {
$awards = $this->wikidataAwardGateway->getAwards($rowActor);
} catch (\Throwable) {
return null;
}
if (!empty($awards)) {
$award = $awards[array_rand($awards)];
return ['type' => 'award', 'data' => $award['name'] . ' (' . $award['year'] . ')'];
}
return null;
}
return null;
}
private function resolveHintText(GameRow $row): ?string
{
$type = $row->getHintType();
$data = $row->getHintData();
if ($type === null || $data === null) {
return null;
}
return match ($type) {
'film' => $this->movieRepository->find((int) $data)?->getTitle(),
'character' => $this->movieRoleRepository->find((int) $data)?->getCharacter(),
'award' => $data,
default => null,
};
}
}