docs: add game hints implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
571
docs/superpowers/plans/2026-03-30-game-hints.md
Normal file
571
docs/superpowers/plans/2026-03-30-game-hints.md
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
# Game Hints System Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Each game row displays a typed hint (film, character, award) about the main actor, replacing the "?" popover button with a type-specific icon.
|
||||||
|
|
||||||
|
**Architecture:** Two new columns (`hint_type`, `hint_data`) on `GameRow`. Hints are generated at game creation time in `GameGridGenerator`. A new `WikidataAwardGateway` service fetches awards via SPARQL. The React `ActorPopover` component shows a type icon instead of "?" and displays the resolved hint text in the popover.
|
||||||
|
|
||||||
|
**Tech Stack:** Symfony 8 / Doctrine ORM (backend), React 19 (frontend), Wikidata SPARQL API (awards), Font Awesome (icons)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add `hintType` and `hintData` columns to `GameRow` entity
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Entity/GameRow.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add properties and mapping to `GameRow`**
|
||||||
|
|
||||||
|
Add after the `$rowOrder` property (line 30):
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Column(length: 20)]
|
||||||
|
private ?string $hintType = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $hintData = null;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add getters and setters after `setRowOrder()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getHintType(): ?string
|
||||||
|
{
|
||||||
|
return $this->hintType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHintType(string $hintType): static
|
||||||
|
{
|
||||||
|
$this->hintType = $hintType;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHintData(): ?string
|
||||||
|
{
|
||||||
|
return $this->hintData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHintData(string $hintData): static
|
||||||
|
{
|
||||||
|
$this->hintData = $hintData;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Generate and run migration**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console doctrine:migrations:diff
|
||||||
|
php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: new migration adding `hint_type` and `hint_data` columns to `game_row`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Entity/GameRow.php migrations/
|
||||||
|
git commit -m "feat: add hintType and hintData columns to GameRow entity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add `MovieRoleRepository::findOneRandomByActor()` method
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Repository/MovieRoleRepository.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the repository method**
|
||||||
|
|
||||||
|
Replace the commented-out methods with:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @param list<int> $excludeMovieRoleIds MovieRole IDs to exclude
|
||||||
|
* @return MovieRole|null
|
||||||
|
*/
|
||||||
|
public function findOneRandomByActor(int $actorId, array $excludeMovieRoleIds = []): ?MovieRole
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('mr')
|
||||||
|
->andWhere('mr.actor = :actorId')
|
||||||
|
->setParameter('actorId', $actorId);
|
||||||
|
|
||||||
|
if (!empty($excludeMovieRoleIds)) {
|
||||||
|
$qb->andWhere('mr.id NOT IN (:excludeIds)')
|
||||||
|
->setParameter('excludeIds', $excludeMovieRoleIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->orderBy('RANDOM()')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Repository/MovieRoleRepository.php
|
||||||
|
git commit -m "feat: add findOneRandomByActor to MovieRoleRepository"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Create `WikidataAwardGateway` service
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/Service/WikidataAwardGateway.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the gateway service**
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Actor;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
class WikidataAwardGateway
|
||||||
|
{
|
||||||
|
private const SPARQL_ENDPOINT = 'https://query.wikidata.org/sparql';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly HttpClientInterface $httpClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch awards for an actor from Wikidata.
|
||||||
|
*
|
||||||
|
* @return list<array{name: string, year: int}>
|
||||||
|
*/
|
||||||
|
public function getAwards(Actor $actor): array
|
||||||
|
{
|
||||||
|
$sparql = $this->buildQuery($actor->getName());
|
||||||
|
|
||||||
|
$response = $this->httpClient->request('GET', self::SPARQL_ENDPOINT, [
|
||||||
|
'query' => [
|
||||||
|
'query' => $sparql,
|
||||||
|
'format' => 'json',
|
||||||
|
],
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/sparql-results+json',
|
||||||
|
'User-Agent' => 'LtbxdActorle/1.0',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->toArray();
|
||||||
|
$awards = [];
|
||||||
|
|
||||||
|
foreach ($data['results']['bindings'] ?? [] as $binding) {
|
||||||
|
$name = $binding['awardLabel']['value'] ?? null;
|
||||||
|
$year = $binding['year']['value'] ?? null;
|
||||||
|
|
||||||
|
if ($name && $year) {
|
||||||
|
$awards[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'year' => (int) substr($year, 0, 4),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $awards;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildQuery(string $actorName): string
|
||||||
|
{
|
||||||
|
$escaped = str_replace('"', '\\"', $actorName);
|
||||||
|
|
||||||
|
return <<<SPARQL
|
||||||
|
SELECT ?awardLabel ?year WHERE {
|
||||||
|
?person rdfs:label "{$escaped}"@en .
|
||||||
|
?person wdt:P31 wd:Q5 .
|
||||||
|
?person p:P166 ?awardStatement .
|
||||||
|
?awardStatement ps:P166 ?award .
|
||||||
|
?awardStatement pq:P585 ?date .
|
||||||
|
BIND(YEAR(?date) AS ?year)
|
||||||
|
SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en" . }
|
||||||
|
}
|
||||||
|
ORDER BY DESC(?year)
|
||||||
|
SPARQL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify the service is autowired**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console debug:container WikidataAwardGateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: service `App\Service\WikidataAwardGateway` is listed.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Service/WikidataAwardGateway.php
|
||||||
|
git commit -m "feat: add WikidataAwardGateway for actor awards from Wikidata SPARQL"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Integrate hint generation into `GameGridGenerator`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Service/GameGridGenerator.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add dependencies to constructor**
|
||||||
|
|
||||||
|
Update the constructor at line 16-19:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function __construct(
|
||||||
|
private readonly ActorRepository $actorRepository,
|
||||||
|
private readonly MovieRoleRepository $movieRoleRepository,
|
||||||
|
private readonly WikidataAwardGateway $wikidataAwardGateway,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the import at the top:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Repository\MovieRoleRepository;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add hint generation methods**
|
||||||
|
|
||||||
|
Add these private methods after `computeGridData()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @param list<int> $usedMovieRoleIds MovieRole IDs already used (for DB exclusion)
|
||||||
|
* @param list<string> $usedHintKeys Semantic keys like "film:42" to avoid duplicate hints
|
||||||
|
* @return array{type: string, data: string}|null
|
||||||
|
*/
|
||||||
|
private function generateHint(Actor $mainActor, array &$usedMovieRoleIds, array &$usedHintKeys): ?array
|
||||||
|
{
|
||||||
|
$types = ['film', 'character', 'award'];
|
||||||
|
shuffle($types);
|
||||||
|
|
||||||
|
foreach ($types as $type) {
|
||||||
|
$hint = $this->resolveHint($type, $mainActor, $usedMovieRoleIds, $usedHintKeys);
|
||||||
|
if ($hint !== null) {
|
||||||
|
return $hint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int> $usedMovieRoleIds
|
||||||
|
* @param list<string> $usedHintKeys
|
||||||
|
* @return array{type: string, data: string}|null
|
||||||
|
*/
|
||||||
|
private function resolveHint(string $type, Actor $mainActor, array &$usedMovieRoleIds, array &$usedHintKeys): ?array
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case 'film':
|
||||||
|
$role = $this->movieRoleRepository->findOneRandomByActor(
|
||||||
|
$mainActor->getId(),
|
||||||
|
$usedMovieRoleIds,
|
||||||
|
);
|
||||||
|
if ($role === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$movieId = (string) $role->getMovie()->getId();
|
||||||
|
$key = 'film:' . $movieId;
|
||||||
|
if (in_array($key, $usedHintKeys)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$usedMovieRoleIds[] = $role->getId();
|
||||||
|
$usedHintKeys[] = $key;
|
||||||
|
return ['type' => 'film', 'data' => $movieId];
|
||||||
|
|
||||||
|
case 'character':
|
||||||
|
$role = $this->movieRoleRepository->findOneRandomByActor(
|
||||||
|
$mainActor->getId(),
|
||||||
|
$usedMovieRoleIds,
|
||||||
|
);
|
||||||
|
if ($role === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$roleId = (string) $role->getId();
|
||||||
|
$key = 'character:' . $roleId;
|
||||||
|
if (in_array($key, $usedHintKeys)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$usedMovieRoleIds[] = $role->getId();
|
||||||
|
$usedHintKeys[] = $key;
|
||||||
|
return ['type' => 'character', 'data' => $roleId];
|
||||||
|
|
||||||
|
case 'award':
|
||||||
|
try {
|
||||||
|
$awards = $this->wikidataAwardGateway->getAwards($mainActor);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
foreach ($awards as $award) {
|
||||||
|
$text = $award['name'] . ' (' . $award['year'] . ')';
|
||||||
|
$key = 'award:' . $text;
|
||||||
|
if (!in_array($key, $usedHintKeys)) {
|
||||||
|
$usedHintKeys[] = $key;
|
||||||
|
return ['type' => 'award', 'data' => $text];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Wire hint generation into `generate()`**
|
||||||
|
|
||||||
|
Add `$usedMovieRoleIds = [];` and `$usedHintKeys = [];` after `$rowOrder = 0;` (line 31), and add hint assignment after `$row->setRowOrder($rowOrder);` (line 51):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$hint = $this->generateHint($mainActor, $usedMovieRoleIds, $usedHintKeys);
|
||||||
|
if ($hint !== null) {
|
||||||
|
$row->setHintType($hint['type']);
|
||||||
|
$row->setHintData($hint['data']);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Service/GameGridGenerator.php
|
||||||
|
git commit -m "feat: generate hints per row in GameGridGenerator"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Resolve hint display text in `computeGridData()`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Service/GameGridGenerator.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add repositories needed for resolution**
|
||||||
|
|
||||||
|
Add import:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Repository\MovieRepository;
|
||||||
|
```
|
||||||
|
|
||||||
|
Update constructor:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function __construct(
|
||||||
|
private readonly ActorRepository $actorRepository,
|
||||||
|
private readonly MovieRoleRepository $movieRoleRepository,
|
||||||
|
private readonly MovieRepository $movieRepository,
|
||||||
|
private readonly WikidataAwardGateway $wikidataAwardGateway,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add hint data to grid output in `computeGridData()`**
|
||||||
|
|
||||||
|
In the `computeGridData()` method, update the `$grid[]` assignment (around line 105-109) to include hint data:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$hintText = $this->resolveHintText($row);
|
||||||
|
|
||||||
|
$grid[] = [
|
||||||
|
'actorName' => $actor->getName(),
|
||||||
|
'actorId' => $actor->getId(),
|
||||||
|
'pos' => $pos,
|
||||||
|
'hintType' => $row->getHintType(),
|
||||||
|
'hintText' => $hintText,
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `resolveHintText()` method**
|
||||||
|
|
||||||
|
```php
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update the return type docblock**
|
||||||
|
|
||||||
|
Update the `@return` annotation of `computeGridData()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 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}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Service/GameGridGenerator.php src/Repository/MovieRepository.php
|
||||||
|
git commit -m "feat: resolve hint display text in computeGridData"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Install Font Awesome and update `ActorPopover` component
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `assets/react/controllers/ActorPopover.jsx`
|
||||||
|
- Modify: `assets/react/controllers/GameGrid.jsx`
|
||||||
|
- Modify: `assets/react/controllers/GameRow.jsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Install Font Awesome**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/thibaud/ltbxd-actorle && npm install @fortawesome/fontawesome-free
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Import Font Awesome CSS in the app entrypoint**
|
||||||
|
|
||||||
|
Check the main JS entrypoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
head -5 assets/app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Add at the top of `assets/app.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update `ActorPopover` to accept hint props**
|
||||||
|
|
||||||
|
Replace the full content of `assets/react/controllers/ActorPopover.jsx`:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useFloating, useClick, useDismiss, useInteractions, offset, flip, shift } from '@floating-ui/react';
|
||||||
|
|
||||||
|
const HINT_ICONS = {
|
||||||
|
film: 'fa-solid fa-film',
|
||||||
|
character: 'fa-solid fa-masks-theater',
|
||||||
|
award: 'fa-solid fa-trophy',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ActorPopover({ hintType, hintText }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
middleware: [offset(8), flip(), shift()],
|
||||||
|
placement: 'left',
|
||||||
|
});
|
||||||
|
|
||||||
|
const click = useClick(context);
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
|
||||||
|
|
||||||
|
const iconClass = HINT_ICONS[hintType] || 'fa-solid fa-circle-question';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
ref={refs.setReference}
|
||||||
|
{...getReferenceProps()}
|
||||||
|
className="popover-trigger"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i className={iconClass}></i>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={floatingStyles}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
className="actor-popover"
|
||||||
|
>
|
||||||
|
{hintText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update `GameRow` to pass hint props**
|
||||||
|
|
||||||
|
In `assets/react/controllers/GameRow.jsx`, update the component signature (line 9):
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
export default function GameRow({ actorName, pos, colStart, totalWidth, hintType, hintText }) {
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the `ActorPopover` usage (line 33):
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<ActorPopover hintType={hintType} hintText={hintText} />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Update `GameGrid` to pass hint props**
|
||||||
|
|
||||||
|
In `assets/react/controllers/GameGrid.jsx`, update the `GameRow` rendering (lines 27-33):
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<GameRow
|
||||||
|
key={rowIndex}
|
||||||
|
actorName={row.actorName}
|
||||||
|
pos={row.pos}
|
||||||
|
colStart={middle - row.pos}
|
||||||
|
totalWidth={width}
|
||||||
|
hintType={row.hintType}
|
||||||
|
hintText={row.hintText}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add assets/ package.json package-lock.json
|
||||||
|
git commit -m "feat: replace ? button with hint type icons in ActorPopover"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Manual end-to-end verification
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build frontend assets**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/thibaud/ltbxd-actorle && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Start a new game and verify**
|
||||||
|
|
||||||
|
Open the app in a browser, start a new game. Verify:
|
||||||
|
- Each row has an icon (film, masks, or trophy) instead of "?"
|
||||||
|
- Clicking the icon opens a popover with the hint text
|
||||||
|
- Different rows can have different hint types
|
||||||
|
- Award hints show the format "Nom du prix (année)"
|
||||||
|
|
||||||
|
- [ ] **Step 3: Final commit if any adjustments needed**
|
||||||
Reference in New Issue
Block a user