# Game Configuration Panel 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:** Add a configuration panel above the "Commencer une partie" button that lets players filter actors by watched films, choose hint types, and select which award categories to use. **Architecture:** New repository method to query eligible AwardTypes, config parameters passed through the existing POST form to `GameGridProvider::generate()` which gains retry logic. A Stimulus controller handles UI interactions (toggle visibility, prevent last-unchecked). CSS-only toggle switch. **Tech Stack:** Symfony 8 / Twig / Stimulus.js / CSS --- ### File Map | Action | File | Responsibility | |--------|------|----------------| | Modify | `src/Repository/AwardTypeRepository.php` | Add `findWithMinActors()` query | | Modify | `src/Repository/ActorRepository.php` | Add `findOneRandomInWatchedFilms()` method | | Modify | `src/Repository/AwardRepository.php` | Add `findOneRandomByActorAndTypes()` method | | Modify | `src/Controller/HomepageController.php` | Pass eligible AwardTypes to template | | Modify | `src/Controller/GameController.php` | Extract config from POST, pass to generator | | Modify | `src/Provider/GameGridProvider.php` | Accept config, filter hints, retry logic | | Modify | `templates/homepage/index.html.twig` | Render config panel in form | | Create | `assets/controllers/game_config_controller.js` | Stimulus controller for UI interactions | | Modify | `assets/styles/app.css` | Toggle switch, config panel styles | | Modify | `tests/Provider/GameGridProviderTest.php` | Test hint filtering and retry logic | --- ### Task 1: AwardTypeRepository — `findWithMinActors()` **Files:** - Modify: `src/Repository/AwardTypeRepository.php` - [ ] **Step 1: Write the failing test** Create `tests/Repository/AwardTypeRepositoryTest.php`: ```php em = self::getContainer()->get(EntityManagerInterface::class); $this->repo = self::getContainer()->get(AwardTypeRepository::class); // Clean slate $this->em->createQuery('DELETE FROM App\Entity\Award')->execute(); $this->em->createQuery('DELETE FROM App\Entity\AwardType')->execute(); $this->em->createQuery('DELETE FROM App\Entity\Actor')->execute(); } public function testFindWithMinActorsFiltersCorrectly(): void { // Create AwardType with 3 distinct actors (below threshold of 5) $smallType = new AwardType(); $smallType->setName('Small Award')->setPattern('small'); $this->em->persist($smallType); // Create AwardType with 6 distinct actors (above threshold) $bigType = new AwardType(); $bigType->setName('Big Award')->setPattern('big'); $this->em->persist($bigType); for ($i = 0; $i < 6; $i++) { $actor = new Actor(); $actor->setName("Actor $i"); $this->em->persist($actor); $award = new Award(); $award->setName("Big Award $i"); $award->setActor($actor); $award->setAwardType($bigType); $this->em->persist($award); if ($i < 3) { $awardSmall = new Award(); $awardSmall->setName("Small Award $i"); $awardSmall->setActor($actor); $awardSmall->setAwardType($smallType); $this->em->persist($awardSmall); } } $this->em->flush(); $result = $this->repo->findWithMinActors(5); $this->assertCount(1, $result); $this->assertSame('Big Award', $result[0]->getName()); } } ``` - [ ] **Step 2: Run test to verify it fails** Run: `php bin/phpunit tests/Repository/AwardTypeRepositoryTest.php -v` Expected: FAIL — method `findWithMinActors` does not exist. - [ ] **Step 3: Implement `findWithMinActors()`** In `src/Repository/AwardTypeRepository.php`, add this method after the existing `findAll()`: ```php /** @return list */ public function findWithMinActors(int $minActors): array { return $this->createQueryBuilder('at') ->join('at.awards', 'a') ->groupBy('at.id') ->having('COUNT(DISTINCT a.actor) >= :minActors') ->setParameter('minActors', $minActors) ->orderBy('at.name', 'ASC') ->getQuery() ->getResult(); } ``` - [ ] **Step 4: Run test to verify it passes** Run: `php bin/phpunit tests/Repository/AwardTypeRepositoryTest.php -v` Expected: PASS - [ ] **Step 5: Commit** ```bash git add src/Repository/AwardTypeRepository.php tests/Repository/AwardTypeRepositoryTest.php git commit -m "feat: add AwardTypeRepository::findWithMinActors()" ``` --- ### Task 2: ActorRepository — `findOneRandomInWatchedFilms()` **Files:** - Modify: `src/Repository/ActorRepository.php` - [ ] **Step 1: Write the failing test** Create `tests/Repository/ActorRepositoryTest.php`: ```php em = self::getContainer()->get(EntityManagerInterface::class); $this->repo = self::getContainer()->get(ActorRepository::class); } public function testFindOneRandomInWatchedFilmsReturnsOnlyWatchedActors(): void { // Create user $user = new User(); $user->setEmail('test-watched-' . uniqid() . '@example.com'); $user->setPassword('test'); $this->em->persist($user); // Actor in a watched film $watchedActor = new Actor(); $watchedActor->setName('Watched Actor'); $watchedActor->setPopularity(10.0); $this->em->persist($watchedActor); $movie = new Movie(); $movie->setTmdbId(99990); $movie->setLtbxdRef('watched-test'); $movie->setTitle('Watched Film'); $this->em->persist($movie); $role = new MovieRole(); $role->setActor($watchedActor); $role->setMovie($movie); $role->setCharacter('Hero'); $this->em->persist($role); $userMovie = new UserMovie(); $userMovie->setUser($user); $userMovie->setMovie($movie); $this->em->persist($userMovie); // Actor NOT in any watched film $unwatchedActor = new Actor(); $unwatchedActor->setName('Unwatched Actor'); $unwatchedActor->setPopularity(10.0); $this->em->persist($unwatchedActor); $movie2 = new Movie(); $movie2->setTmdbId(99991); $movie2->setLtbxdRef('unwatched-test'); $movie2->setTitle('Unwatched Film'); $this->em->persist($movie2); $role2 = new MovieRole(); $role2->setActor($unwatchedActor); $role2->setMovie($movie2); $role2->setCharacter('Villain'); $this->em->persist($role2); $this->em->flush(); // Run many times to be sure — should always return watchedActor for ($i = 0; $i < 10; $i++) { $result = $this->repo->findOneRandomInWatchedFilms($user, 0, 'w'); $this->assertNotNull($result); $this->assertSame($watchedActor->getId(), $result->getId()); } } } ``` - [ ] **Step 2: Run test to verify it fails** Run: `php bin/phpunit tests/Repository/ActorRepositoryTest.php -v` Expected: FAIL — method `findOneRandomInWatchedFilms` does not exist. - [ ] **Step 3: Implement `findOneRandomInWatchedFilms()`** In `src/Repository/ActorRepository.php`, add after `findOneRandom()`: ```php public function findOneRandomInWatchedFilms(User $user, ?float $popularity = null, ?string $char = null): ?Actor { $qb = $this->createQueryBuilder('a') ->join('a.movieRoles', 'mr') ->join('mr.movie', 'm') ->join(UserMovie::class, 'um', 'WITH', 'um.movie = m AND um.user = :user') ->setParameter('user', $user); if (!empty($popularity)) { $qb->andWhere('a.popularity >= :popularity') ->setParameter('popularity', $popularity); } if (!empty($char)) { $qb->andWhere('a.name LIKE :name') ->setParameter('name', '%' . $char . '%'); } return $qb ->orderBy('RANDOM()') ->setMaxResults(1) ->getQuery() ->getOneOrNullResult(); } ``` Add the missing import at the top of the file: ```php use App\Entity\User; use App\Entity\UserMovie; ``` - [ ] **Step 4: Run test to verify it passes** Run: `php bin/phpunit tests/Repository/ActorRepositoryTest.php -v` Expected: PASS - [ ] **Step 5: Commit** ```bash git add src/Repository/ActorRepository.php tests/Repository/ActorRepositoryTest.php git commit -m "feat: add ActorRepository::findOneRandomInWatchedFilms()" ``` --- ### Task 3: AwardRepository — `findOneRandomByActorAndTypes()` **Files:** - Modify: `src/Repository/AwardRepository.php` - [ ] **Step 1: Write the failing test** Create `tests/Repository/AwardRepositoryTest.php`: ```php em = self::getContainer()->get(EntityManagerInterface::class); $this->repo = self::getContainer()->get(AwardRepository::class); } public function testFindOneRandomByActorAndTypesFiltersCorrectly(): void { $actor = new Actor(); $actor->setName('Award Actor Test'); $this->em->persist($actor); $oscarType = new AwardType(); $oscarType->setName('Oscar')->setPattern('oscar'); $this->em->persist($oscarType); $globeType = new AwardType(); $globeType->setName('Golden Globe')->setPattern('globe'); $this->em->persist($globeType); $oscar = new Award(); $oscar->setName('Best Actor Oscar'); $oscar->setActor($actor); $oscar->setAwardType($oscarType); $this->em->persist($oscar); $globe = new Award(); $globe->setName('Best Actor Globe'); $globe->setActor($actor); $globe->setAwardType($globeType); $this->em->persist($globe); $this->em->flush(); // Filter to only Oscar type for ($i = 0; $i < 10; $i++) { $result = $this->repo->findOneRandomByActorAndTypes($actor->getId(), [$oscarType->getId()]); $this->assertNotNull($result); $this->assertSame('Best Actor Oscar', $result->getName()); } // Null awardTypeIds = all types allowed $result = $this->repo->findOneRandomByActorAndTypes($actor->getId(), null); $this->assertNotNull($result); } } ``` - [ ] **Step 2: Run test to verify it fails** Run: `php bin/phpunit tests/Repository/AwardRepositoryTest.php -v` Expected: FAIL — method `findOneRandomByActorAndTypes` does not exist. - [ ] **Step 3: Implement `findOneRandomByActorAndTypes()`** In `src/Repository/AwardRepository.php`, add after `findOneRandomByActor()`: ```php /** * @param list|null $awardTypeIds null means all types */ public function findOneRandomByActorAndTypes(int $actorId, ?array $awardTypeIds): ?Award { $qb = $this->createQueryBuilder('a') ->andWhere('a.actor = :actorId') ->setParameter('actorId', $actorId); if ($awardTypeIds !== null) { $qb->andWhere('a.awardType IN (:typeIds)') ->setParameter('typeIds', $awardTypeIds); } return $qb ->orderBy('RANDOM()') ->setMaxResults(1) ->getQuery() ->getOneOrNullResult(); } ``` - [ ] **Step 4: Run test to verify it passes** Run: `php bin/phpunit tests/Repository/AwardRepositoryTest.php -v` Expected: PASS - [ ] **Step 5: Commit** ```bash git add src/Repository/AwardRepository.php tests/Repository/AwardRepositoryTest.php git commit -m "feat: add AwardRepository::findOneRandomByActorAndTypes()" ``` --- ### Task 4: GameGridProvider — accept config and add retry logic **Files:** - Modify: `src/Provider/GameGridProvider.php` - Modify: `tests/Provider/GameGridProviderTest.php` - [ ] **Step 1: Write the failing test for hint type filtering** Add to `tests/Provider/GameGridProviderTest.php`: ```php public function testGenerateHintRespectsAllowedTypes(): void { $movieRoleRepo = $this->createMock(MovieRoleRepository::class); $movieRoleRepo->method('findOneRandomByActor')->willReturn(null); $awardRepo = $this->createMock(AwardRepository::class); $awardRepo->method('findOneRandomByActor')->willReturn(null); $awardRepo->method('findOneRandomByActorAndTypes')->willReturn(null); $generator = new GameGridProvider( $this->createMock(ActorRepository::class), $movieRoleRepo, $this->createMock(MovieRepository::class), $awardRepo, $this->createMock(EntityManagerInterface::class), ); $actor = new Actor(); $actor->setName('Test'); // Only allow 'award' type, but no awards exist → should return null $method = new \ReflectionMethod($generator, 'generateHint'); $result = $method->invoke($generator, $actor, ['award'], null); $this->assertNull($result); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `php bin/phpunit tests/Provider/GameGridProviderTest.php --filter=testGenerateHintRespectsAllowedTypes -v` Expected: FAIL — `generateHint` signature doesn't accept extra arguments. - [ ] **Step 3: Update `generateHint()` and `resolveHint()` signatures** In `src/Provider/GameGridProvider.php`, change `generateHint()`: ```php /** * @param list $allowedTypes * @param list|null $awardTypeIds * @return array{type: string, data: string}|null */ private function generateHint(Actor $rowActor, array $allowedTypes = ['film', 'character', 'award'], ?array $awardTypeIds = null): ?array { $types = $allowedTypes; shuffle($types); foreach ($types as $type) { $hint = $this->resolveHint($type, $rowActor, $awardTypeIds); if ($hint !== null) { return $hint; } } return null; } ``` Change `resolveHint()`: ```php /** * @param list|null $awardTypeIds * @return array{type: string, data: string}|null */ private function resolveHint(string $type, Actor $rowActor, ?array $awardTypeIds = null): ?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': $award = $this->awardRepository->findOneRandomByActorAndTypes($rowActor->getId(), $awardTypeIds); if ($award === null) { return null; } return ['type' => 'award', 'data' => (string) $award->getId()]; } return null; } ``` - [ ] **Step 4: Run test to verify it passes** Run: `php bin/phpunit tests/Provider/GameGridProviderTest.php -v` Expected: PASS (both old and new tests) - [ ] **Step 5: Update `generate()` to accept config and add retry logic** Replace the `generate()` method in `src/Provider/GameGridProvider.php`: ```php /** * @param array{watchedOnly?: bool, hintTypes?: list, awardTypeIds?: list|null} $config */ public function generate(?User $user = null, array $config = []): ?Game { $watchedOnly = $config['watchedOnly'] ?? false; $hintTypes = $config['hintTypes'] ?? ['film', 'character', 'award']; $awardTypeIds = $config['awardTypeIds'] ?? null; for ($attempt = 0; $attempt < 5; $attempt++) { $game = $this->tryGenerate($user, $watchedOnly, $hintTypes, $awardTypeIds); if ($game !== null) { return $game; } } return null; } /** * @param list $hintTypes * @param list|null $awardTypeIds */ private function tryGenerate(?User $user, bool $watchedOnly, array $hintTypes, ?array $awardTypeIds): ?Game { if ($watchedOnly && $user !== null) { $mainActor = $this->actorRepository->findOneRandomInWatchedFilms($user, 4); } else { $mainActor = $this->actorRepository->findOneRandom(4); } if ($mainActor === null) { return null; } $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; } $actor = null; for ($try = 0; $try < 5; $try++) { if ($watchedOnly && $user !== null) { $candidate = $this->actorRepository->findOneRandomInWatchedFilms($user, 4, $char); } else { $candidate = $this->actorRepository->findOneRandom(4, $char); } if ($candidate !== null && !in_array($candidate->getId(), $usedActors)) { $actor = $candidate; break; } } if ($actor === null) { return null; } $usedActors[] = $actor->getId(); $row = new GameRow(); $row->setActor($actor); $row->setPosition(strpos(strtolower($actor->getName()), $char)); $row->setRowOrder($rowOrder); $hint = $this->generateHint($actor, $hintTypes, $awardTypeIds); if ($hint === null) { return null; // Every row must have a hint } $row->setHintType($hint['type']); $row->setHintData($hint['data']); $game->addRow($row); ++$rowOrder; } $this->em->persist($game); $this->em->flush(); return $game; } ``` - [ ] **Step 6: Run all tests** Run: `php bin/phpunit -v` Expected: All PASS - [ ] **Step 7: Commit** ```bash git add src/Provider/GameGridProvider.php tests/Provider/GameGridProviderTest.php git commit -m "feat: GameGridProvider accepts config for hint types, watched-only, and retry logic" ``` --- ### Task 5: GameController — extract config from POST **Files:** - Modify: `src/Controller/GameController.php` - [ ] **Step 1: Update the `start()` method** Replace the `$game = $generator->generate($user);` line (line 45) and surrounding logic. The full updated `start()` method: ```php #[Route('/game/start', name: 'app_game_start', methods: ['POST'])] public function start( Request $request, GameGridProvider $generator, GameRepository $gameRepository, ): Response { $this->validateCsrfToken('game_start', $request); /** @var User|null $user */ $user = $this->getUser(); // Check no game already in progress if ($user) { $existing = $gameRepository->findActiveForUser($user); } else { $gameId = $request->getSession()->get('current_game_id'); $existing = $gameId ? $gameRepository->find($gameId) : null; if ($existing && $existing->getStatus() !== Game::STATUS_IN_PROGRESS) { $existing = null; } } if ($existing) { return $this->redirectToRoute('app_homepage'); } // Build config from form parameters $config = []; if ($user && $request->request->getBoolean('watched_only')) { $config['watchedOnly'] = true; } $hintTypes = []; if ($request->request->getBoolean('hint_film', true)) { $hintTypes[] = 'film'; } if ($request->request->getBoolean('hint_character', true)) { $hintTypes[] = 'character'; } if ($request->request->getBoolean('hint_award', true)) { $hintTypes[] = 'award'; } if (empty($hintTypes)) { $hintTypes = ['film', 'character', 'award']; } $config['hintTypes'] = $hintTypes; /** @var list $awardTypeIds */ $awardTypeIds = $request->request->all('award_types'); if (!empty($awardTypeIds) && in_array('award', $hintTypes)) { $config['awardTypeIds'] = array_map('intval', $awardTypeIds); } $game = $generator->generate($user, $config); if ($game === null) { $this->addFlash('error', 'Impossible de générer une grille avec ces paramètres. Essayez avec des critères moins restrictifs.'); return $this->redirectToRoute('app_homepage'); } if (!$user) { $request->getSession()->set('current_game_id', $game->getId()); } return $this->redirectToRoute('app_homepage'); } ``` - [ ] **Step 2: Run existing tests to check nothing is broken** Run: `php bin/phpunit -v` Expected: All PASS - [ ] **Step 3: Commit** ```bash git add src/Controller/GameController.php git commit -m "feat: extract game config from POST and pass to generator" ``` --- ### Task 6: HomepageController — pass AwardTypes to template **Files:** - Modify: `src/Controller/HomepageController.php` - [ ] **Step 1: Add AwardTypeRepository and pass eligible types to template** Update the `index()` method. Add `AwardTypeRepository` as a parameter and pass the eligible types when rendering the start screen: ```php use App\Repository\AwardTypeRepository; ``` Change the method signature to: ```php public function index( Request $request, GameRepository $gameRepository, GameGridProvider $gridGenerator, AwardTypeRepository $awardTypeRepository, ): Response { ``` Change the no-game render block (around line 43) from: ```php return $this->render('homepage/index.html.twig', [ 'game' => null, ]); ``` To: ```php return $this->render('homepage/index.html.twig', [ 'game' => null, 'awardTypes' => $awardTypeRepository->findWithMinActors(5), ]); ``` - [ ] **Step 2: Run tests** Run: `php bin/phpunit -v` Expected: All PASS - [ ] **Step 3: Commit** ```bash git add src/Controller/HomepageController.php git commit -m "feat: pass eligible AwardTypes to homepage template" ``` --- ### Task 7: Twig template — render config panel **Files:** - Modify: `templates/homepage/index.html.twig` - [ ] **Step 1: Add the config panel HTML inside the form** Replace the `{% else %}` block (lines 57-76) with: ```twig {% else %}
{% if app.user %}
{% endif %}
Paramètres des indices
Récompenses
{% for awardType in awardTypes %} {% endfor %}
{% if not app.user %} {% endif %} {% for message in app.flashes('error') %}
{{ message }}
{% endfor %}
{% endif %} ``` - [ ] **Step 2: Verify the page loads in the browser** Run: `symfony serve` (or existing dev server) and visit the homepage. Verify the config panel renders above the button. - [ ] **Step 3: Commit** ```bash git add templates/homepage/index.html.twig git commit -m "feat: render game config panel in start form" ``` --- ### Task 8: Stimulus controller — `game_config_controller.js` **Files:** - Create: `assets/controllers/game_config_controller.js` - [ ] **Step 1: Create the Stimulus controller** ```js import { Controller } from '@hotwired/stimulus'; export default class extends Controller { static targets = ['hintType', 'awardSection', 'allAwards', 'awardType']; toggleAwardSection() { const awardChecked = this.hintTypeTargets.find( (el) => el.name === 'hint_award' )?.checked; this.awardSectionTarget.style.display = awardChecked ? '' : 'none'; } toggleAllAwards() { const checked = this.allAwardsTarget.checked; this.awardTypeTargets.forEach((el) => { el.checked = checked; }); } syncAllAwards() { const allChecked = this.awardTypeTargets.every((el) => el.checked); this.allAwardsTarget.checked = allChecked; } hintTypeTargetConnected() { this.#bindMinOneChecked(); } #bindMinOneChecked() { this.hintTypeTargets.forEach((el) => { el.addEventListener('change', () => { const checked = this.hintTypeTargets.filter((e) => e.checked); if (checked.length === 0) { el.checked = true; } this.toggleAwardSection(); }); }); } } ``` - [ ] **Step 2: Verify in browser** Visit homepage, test: - Toggle "Récompense" off → award list disappears - Toggle "Récompense" on → award list appears - Uncheck all AwardTypes → "Toutes" unchecked - Check "Toutes" → all checked - Try to uncheck the last hint type → stays checked - [ ] **Step 3: Commit** ```bash git add assets/controllers/game_config_controller.js git commit -m "feat: add Stimulus game-config controller" ``` --- ### Task 9: CSS — config panel styles and toggle switch **Files:** - Modify: `assets/styles/app.css` - [ ] **Step 1: Add config panel CSS** Add the following CSS after the `.btn-start` block (after line 733 in `app.css`): ```css /* ── Game config panel ── */ .config-panel { width: 100%; max-width: 360px; margin-bottom: 24px; } .config-section { padding: 12px 0; } .config-section + .config-section { border-top: 1px solid var(--border); } .config-section-title { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 10px; } .config-section-subtitle { font-size: 11px; font-weight: 600; color: var(--text-muted); margin: 10px 0 6px; } /* Toggle switch */ .config-toggle { display: flex; align-items: center; justify-content: space-between; cursor: pointer; } .config-toggle-label { font-size: 14px; font-weight: 500; color: var(--text); } .toggle-switch { appearance: none; width: 40px; height: 22px; background: var(--border); border-radius: 100px; position: relative; cursor: pointer; transition: background 0.2s; } .toggle-switch::before { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; background: white; border-radius: 50%; transition: transform 0.2s; } .toggle-switch:checked { background: var(--orange); } .toggle-switch:checked::before { transform: translateX(18px); } /* Hint type checkboxes */ .config-hint-types { display: flex; gap: 16px; } .config-checkbox { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text); cursor: pointer; } .config-checkbox input[type="checkbox"] { accent-color: var(--orange); } /* Award type list */ .config-award-types { margin-top: 4px; } .config-award-list { border: 1px solid var(--border); border-radius: var(--radius-sm); max-height: 150px; overflow-y: auto; padding: 6px 0; } .config-award-list .config-checkbox { padding: 4px 12px; } .config-award-list .config-checkbox:first-child { padding-bottom: 6px; margin-bottom: 2px; border-bottom: 1px solid var(--border); } /* Flash messages */ .flash-error { margin-top: 16px; padding: 10px 16px; background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; border-radius: var(--radius-sm); font-size: 13px; } ``` - [ ] **Step 2: Verify in browser** Visit homepage and verify: - Toggle switch slides with orange color when on - Checkboxes are orange-accented - Award list scrolls if content overflows - Panel is centered and aligned above the start button - Responsive on mobile widths - [ ] **Step 3: Commit** ```bash git add assets/styles/app.css git commit -m "feat: add config panel CSS with toggle switch and award list styles" ``` --- ### Task 10: Final integration test **Files:** - None new — manual verification - [ ] **Step 1: Run all tests** Run: `php bin/phpunit -v` Expected: All PASS - [ ] **Step 2: Manual integration test — anonymous user** 1. Log out 2. Visit homepage 3. Verify: "Films vus" toggle is NOT shown 4. Verify: Three hint type checkboxes are shown, all checked 5. Verify: Award list is shown (since "Récompense" is checked) 6. Uncheck "Film" and "Rôle", try to uncheck "Récompense" → prevented 7. Click "Commencer une partie" → game starts - [ ] **Step 3: Manual integration test — authenticated user** 1. Log in 2. Visit homepage 3. Verify: "Films vus" toggle IS shown 4. Enable "Films vus", uncheck "Film" hint type 5. Click "Commencer une partie" → game starts with only character/award hints 6. Verify hints match selected types - [ ] **Step 4: Manual integration test — error case** 1. Log in, enable "Films vus" 2. Uncheck "Film" and "Rôle" (keep only "Récompense") 3. Select only a rare award type 4. Click "Commencer une partie" 5. If generation fails: verify flash error message appears - [ ] **Step 5: Commit any final fixes if needed** ```bash git add -A git commit -m "fix: integration fixes for game config panel" ```