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>
207 lines
5.8 KiB
PHP
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,
|
|
};
|
|
}
|
|
}
|