From c273bce8d2219006aae158f65a6b51a88c53f47c Mon Sep 17 00:00:00 2001 From: Markus Reinhold Date: Wed, 4 Dec 2024 21:05:53 +0100 Subject: [PATCH] Allow players to choose game configuration --- assets/css/app.css | 8 ++ assets/js/ConnectFour/OpenButton.js | 63 ------------- config/connect-four/importmap.php | 1 - config/web-interface/config.yml | 2 +- config/web-interface/routing.yml | 12 ++- config/web-interface/services/controller.yml | 8 +- .../Application/Game/Command/OpenCommand.php | 10 +- .../Application/Game/Command/OpenHandler.php | 11 ++- src/ConnectFour/Domain/Game/Board/Size.php | 23 ++--- src/ConnectFour/Domain/Game/Board/Stone.php | 5 + src/ConnectFour/Domain/Game/Configuration.php | 42 ++++++--- .../Domain/Game/Event/GameAborted.php | 14 +-- .../Domain/Game/Event/GameOpened.php | 6 +- .../Domain/Game/Event/PlayerJoined.php | 14 +-- src/ConnectFour/Domain/Game/Game.php | 10 +- src/ConnectFour/Domain/Game/State/Open.php | 28 ++---- src/ConnectFour/Domain/Game/State/Running.php | 4 +- .../Http/ConnectFourController.php | 45 +++++++-- .../Presentation/Http/Form/OpenType.php | 92 +++++++++++++++++++ .../Presentation/Http/PageController.php | 50 +++++----- .../Http/View/challenge.html.twig | 33 +++++++ .../Http/View/layout/forms.html.twig | 22 +++++ .../Presentation/Http/View/lobby.html.twig | 18 +++- tests/acceptance/GameCest.php | 14 ++- .../Domain/Game/Board/SizeTest.php | 2 - .../unit/ConnectFour/Domain/Game/GameTest.php | 5 +- 26 files changed, 343 insertions(+), 199 deletions(-) delete mode 100644 assets/js/ConnectFour/OpenButton.js create mode 100644 src/WebInterface/Presentation/Http/Form/OpenType.php create mode 100644 src/WebInterface/Presentation/Http/View/challenge.html.twig create mode 100644 src/WebInterface/Presentation/Http/View/layout/forms.html.twig diff --git a/assets/css/app.css b/assets/css/app.css index 38d9a734..62e90900 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -172,3 +172,11 @@ notification-list { [data-bs-theme=dark] .gp-game__field { border-color: var(--tblr-blue); } + +/** Fix specificity for checked buttons on hover */ +.btn-check:checked + .btn:hover { + color: var(--tblr-btn-active-color); + background-color: var(--tblr-btn-active-bg); + border-color: var(--tblr-btn-active-border-color); + box-shadow: var(--tblr-btn-active-shadow) +} diff --git a/assets/js/ConnectFour/OpenButton.js b/assets/js/ConnectFour/OpenButton.js deleted file mode 100644 index a570b8cf..00000000 --- a/assets/js/ConnectFour/OpenButton.js +++ /dev/null @@ -1,63 +0,0 @@ -import {service} from './GameService.js' -import {html} from 'uhtml/node.js' - -customElements.define('connect-four-open-button', class extends HTMLElement { - connectedCallback() { - this._onDisconnect = []; - this._button = html` - - `; - - this.replaceChildren(this._button); - - this._currentOpenGameId = ''; - - this._button.addEventListener('click', this._onButtonClick.bind(this)); - - ((n, f) => { - window.addEventListener(n, f); - this._onDisconnect.push(() => window.removeEventListener(n, f)); - })('ConnectFour.PlayerJoined', this._onPlayerJoined.bind(this)); - - ((n, f) => { - window.addEventListener(n, f); - this._onDisconnect.push(() => window.removeEventListener(n, f)); - })('ConnectFour.GameAborted', this._onGameAborted.bind(this)); - } - - disconnectedCallback() { - this._onDisconnect.forEach(f => f()); - } - - async _onButtonClick(event) { - event.preventDefault(); - - this._button.disabled = true; - this._button.classList.add('btn-loading'); - - if (this._currentOpenGameId) { - service.abort(this._currentOpenGameId); - } - - service.open().then((game) => { - this._currentOpenGameId = game.gameId; - this._button.disabled = false; - this._button.classList.remove('btn-loading'); - }).catch(() => { - this._button.disabled = false; - this._button.classList.remove('btn-loading'); - }); - } - - _onPlayerJoined(event) { - if (this._currentOpenGameId === event.detail.gameId) { - service.redirectTo(this._currentOpenGameId); - } - } - - _onGameAborted(event) { - if (this._currentOpenGameId === event.detail.gameId) { - this._currentOpenGameId = ''; - } - } -}); diff --git a/config/connect-four/importmap.php b/config/connect-four/importmap.php index 12f5dbe8..00ee87cb 100644 --- a/config/connect-four/importmap.php +++ b/config/connect-four/importmap.php @@ -6,7 +6,6 @@ 'connect-four-running-games' => ['path' => 'js/ConnectFour/RunningGames.js'], 'connect-four-game-list' => ['path' => 'js/ConnectFour/GameList.js'], 'connect-four-game' => ['path' => 'js/ConnectFour/Game.js'], - 'connect-four-open-button' => ['path' => 'js/ConnectFour/OpenButton.js'], 'connect-four-abort-button' => ['path' => 'js/ConnectFour/AbortButton.js'], 'connect-four-resign-button' => ['path' => 'js/ConnectFour/ResignButton.js'] ]; diff --git a/config/web-interface/config.yml b/config/web-interface/config.yml index cf8a1da9..19b6fc9f 100644 --- a/config/web-interface/config.yml +++ b/config/web-interface/config.yml @@ -8,7 +8,7 @@ framework: csrf_protection: false # CSRF protection is handled by marein/symfony-standard-headers-csrf-bundle. twig: - form_themes: ['bootstrap_5_layout.html.twig'] + form_themes: ['@web-interface/layout/forms.html.twig'] paths: { '%kernel.project_dir%/src/WebInterface/Presentation/Http/View': web-interface } security: diff --git a/config/web-interface/routing.yml b/config/web-interface/routing.yml index 78cdee26..1fdf3d87 100644 --- a/config/web-interface/routing.yml +++ b/config/web-interface/routing.yml @@ -21,6 +21,11 @@ game: methods: [GET] controller: web-interface.page-controller::gameAction +challenge: + path: /challenge/{id} + methods: [GET] + controller: web-interface.page-controller::challengeAction + signup: path: /signup methods: [GET, POST] @@ -73,7 +78,6 @@ open: path: /api/connect-four/games/open methods: [POST] controller: web-interface.connect-four-controller::openAction - defaults: { _format: json } abort: path: /api/connect-four/games/{gameId}/abort @@ -98,3 +102,9 @@ move: methods: [POST] controller: web-interface.connect-four-controller::moveAction defaults: { _format: json } + +challenge_abort: + path: /api/connect-four/challenges/{gameId}/abort + methods: [POST] + controller: web-interface.connect-four-controller::abortChallengeAction + defaults: { _format: json } diff --git a/config/web-interface/services/controller.yml b/config/web-interface/services/controller.yml index a837cda6..0eacf847 100644 --- a/config/web-interface/services/controller.yml +++ b/config/web-interface/services/controller.yml @@ -1,8 +1,9 @@ services: web-interface.page-controller: class: Gaming\WebInterface\Presentation\Http\PageController - arguments: ['@twig', '@connect-four.query-bus', '@web-interface.security'] - tags: ['controller.service_arguments'] + arguments: ['@connect-four.query-bus', '@web-interface.security'] + calls: [[setContainer, ['@Psr\Container\ContainerInterface']]] + tags: ['controller.service_arguments', 'container.service_subscriber'] web-interface.signup-controller: class: Gaming\WebInterface\Presentation\Http\SignupController @@ -34,4 +35,5 @@ services: web-interface.connect-four-controller: class: Gaming\WebInterface\Presentation\Http\ConnectFourController arguments: ['@connect-four.command-bus', '@web-interface.security'] - tags: ['controller.service_arguments'] + calls: [[setContainer, ['@Psr\Container\ContainerInterface']]] + tags: ['controller.service_arguments', 'container.service_subscriber'] diff --git a/src/ConnectFour/Application/Game/Command/OpenCommand.php b/src/ConnectFour/Application/Game/Command/OpenCommand.php index 3d018ffd..1ed3914b 100644 --- a/src/ConnectFour/Application/Game/Command/OpenCommand.php +++ b/src/ConnectFour/Application/Game/Command/OpenCommand.php @@ -12,12 +12,10 @@ final class OpenCommand implements Request { public function __construct( - private readonly string $playerId + public readonly string $playerId, + public readonly int $width, + public readonly int $height, + public readonly int $stone ) { } - - public function playerId(): string - { - return $this->playerId; - } } diff --git a/src/ConnectFour/Application/Game/Command/OpenHandler.php b/src/ConnectFour/Application/Game/Command/OpenHandler.php index 2394e9a8..43c8088d 100644 --- a/src/ConnectFour/Application/Game/Command/OpenHandler.php +++ b/src/ConnectFour/Application/Game/Command/OpenHandler.php @@ -4,9 +4,12 @@ namespace Gaming\ConnectFour\Application\Game\Command; +use Gaming\ConnectFour\Domain\Game\Board\Size; +use Gaming\ConnectFour\Domain\Game\Board\Stone; use Gaming\ConnectFour\Domain\Game\Configuration; use Gaming\ConnectFour\Domain\Game\Game; use Gaming\ConnectFour\Domain\Game\Games; +use Gaming\ConnectFour\Domain\Game\WinningRule\WinningRules; final class OpenHandler { @@ -21,8 +24,12 @@ public function __invoke(OpenCommand $command): string { $game = Game::open( $this->games->nextIdentity(), - Configuration::common(), - $command->playerId() + new Configuration( + new Size($command->width, $command->height), + WinningRules::standard(), + Stone::tryFrom($command->stone) + ), + $command->playerId ); $this->games->add($game); diff --git a/src/ConnectFour/Domain/Game/Board/Size.php b/src/ConnectFour/Domain/Game/Board/Size.php index 5dc186d8..98ea9814 100644 --- a/src/ConnectFour/Domain/Game/Board/Size.php +++ b/src/ConnectFour/Domain/Game/Board/Size.php @@ -8,32 +8,29 @@ final class Size { - private int $width; - - private int $height; - /** * @throws InvalidSizeException */ - public function __construct(int $width, int $height) - { + public function __construct( + public readonly int $width, + public readonly int $height + ) { if ($width < 2 || $height < 2) { throw new InvalidSizeException('Width and height must be greater then 1.'); } - - if (($width * $height) % 2 !== 0) { - throw new InvalidSizeException('Product of width and height must be an even number.'); - } - - $this->height = $height; - $this->width = $width; } + /** + * @deprecated Use property instead. + */ public function width(): int { return $this->width; } + /** + * @deprecated Use property instead. + */ public function height(): int { return $this->height; diff --git a/src/ConnectFour/Domain/Game/Board/Stone.php b/src/ConnectFour/Domain/Game/Board/Stone.php index b49f47c4..ccd1d6eb 100644 --- a/src/ConnectFour/Domain/Game/Board/Stone.php +++ b/src/ConnectFour/Domain/Game/Board/Stone.php @@ -9,4 +9,9 @@ enum Stone: int case None = 0; case Red = 1; case Yellow = 2; + + public static function random(): self + { + return self::from(rand(1, 2)); + } } diff --git a/src/ConnectFour/Domain/Game/Configuration.php b/src/ConnectFour/Domain/Game/Configuration.php index 3d6b1348..3810f414 100644 --- a/src/ConnectFour/Domain/Game/Configuration.php +++ b/src/ConnectFour/Domain/Game/Configuration.php @@ -5,33 +5,28 @@ namespace Gaming\ConnectFour\Domain\Game; use Gaming\ConnectFour\Domain\Game\Board\Size; +use Gaming\ConnectFour\Domain\Game\Board\Stone; +use Gaming\ConnectFour\Domain\Game\Exception\PlayersNotUniqueException; use Gaming\ConnectFour\Domain\Game\WinningRule\WinningRules; final class Configuration { - private Size $size; - - private WinningRules $winningRules; - - private function __construct(Size $size, WinningRules $winningRules) - { - $this->size = $size; - $this->winningRules = $winningRules; + public function __construct( + private readonly Size $size, + private readonly WinningRules $winningRules, + private readonly ?Stone $preferredStone = null + ) { } public static function common(): Configuration { return new self( new Size(7, 6), - WinningRules::standard() + WinningRules::standard(), + Stone::Red // Should be null, but is Red for keeping unit tests green. ); } - public static function custom(Size $size, WinningRules $winningRules): Configuration - { - return new self($size, $winningRules); - } - public function size(): Size { return $this->size; @@ -41,4 +36,23 @@ public function winningRules(): WinningRules { return $this->winningRules; } + + /** + * @throws PlayersNotUniqueException + */ + public function createPlayers(string $playerId, string $joinedPlayerId): Players + { + $players = match ($this->preferredStone ?? Stone::random()) { + Stone::Red => [ + new Player($playerId, Stone::Red), + new Player($joinedPlayerId, Stone::Yellow) + ], + default => [ + new Player($joinedPlayerId, Stone::Red), + new Player($playerId, Stone::Yellow) + ] + }; + + return new Players(...$players); + } } diff --git a/src/ConnectFour/Domain/Game/Event/GameAborted.php b/src/ConnectFour/Domain/Game/Event/GameAborted.php index 9d437102..ce70cf45 100644 --- a/src/ConnectFour/Domain/Game/Event/GameAborted.php +++ b/src/ConnectFour/Domain/Game/Event/GameAborted.php @@ -6,21 +6,17 @@ use Gaming\Common\Domain\DomainEvent; use Gaming\ConnectFour\Domain\Game\GameId; -use Gaming\ConnectFour\Domain\Game\Player; final class GameAborted implements DomainEvent { private string $gameId; - private string $abortedPlayerId; - - private string $opponentPlayerId; - - public function __construct(GameId $gameId, Player $abortedPlayer, ?Player $opponentPlayer = null) - { + public function __construct( + GameId $gameId, + private readonly string $abortedPlayerId, + private readonly string $opponentPlayerId = '' + ) { $this->gameId = $gameId->toString(); - $this->abortedPlayerId = $abortedPlayer->id(); - $this->opponentPlayerId = $opponentPlayer ? $opponentPlayer->id() : ''; } public function aggregateId(): string diff --git a/src/ConnectFour/Domain/Game/Event/GameOpened.php b/src/ConnectFour/Domain/Game/Event/GameOpened.php index f7e57110..8b22bff7 100644 --- a/src/ConnectFour/Domain/Game/Event/GameOpened.php +++ b/src/ConnectFour/Domain/Game/Event/GameOpened.php @@ -7,7 +7,6 @@ use Gaming\Common\Domain\DomainEvent; use Gaming\ConnectFour\Domain\Game\Board\Size; use Gaming\ConnectFour\Domain\Game\GameId; -use Gaming\ConnectFour\Domain\Game\Player; final class GameOpened implements DomainEvent { @@ -17,14 +16,11 @@ final class GameOpened implements DomainEvent private int $height; - private string $playerId; - - public function __construct(GameId $gameId, Size $size, Player $player) + public function __construct(GameId $gameId, Size $size, private readonly string $playerId) { $this->gameId = $gameId->toString(); $this->width = $size->width(); $this->height = $size->height(); - $this->playerId = $player->id(); } public function aggregateId(): string diff --git a/src/ConnectFour/Domain/Game/Event/PlayerJoined.php b/src/ConnectFour/Domain/Game/Event/PlayerJoined.php index c3ac99fa..53bb551e 100644 --- a/src/ConnectFour/Domain/Game/Event/PlayerJoined.php +++ b/src/ConnectFour/Domain/Game/Event/PlayerJoined.php @@ -6,21 +6,17 @@ use Gaming\Common\Domain\DomainEvent; use Gaming\ConnectFour\Domain\Game\GameId; -use Gaming\ConnectFour\Domain\Game\Player; final class PlayerJoined implements DomainEvent { private string $gameId; - private string $joinedPlayerId; - - private string $opponentPlayerId; - - public function __construct(GameId $gameId, Player $joinedPlayer, Player $opponentPlayer) - { + public function __construct( + GameId $gameId, + private readonly string $joinedPlayerId, + private readonly string $opponentPlayerId + ) { $this->gameId = $gameId->toString(); - $this->joinedPlayerId = $joinedPlayer->id(); - $this->opponentPlayerId = $opponentPlayer->id(); } public function aggregateId(): string diff --git a/src/ConnectFour/Domain/Game/Game.php b/src/ConnectFour/Domain/Game/Game.php index 90190bb2..be24374e 100644 --- a/src/ConnectFour/Domain/Game/Game.php +++ b/src/ConnectFour/Domain/Game/Game.php @@ -7,7 +7,6 @@ use Gaming\Common\Domain\AggregateRoot; use Gaming\Common\Domain\DomainEvent; use Gaming\Common\Domain\IsAggregateRoot; -use Gaming\ConnectFour\Domain\Game\Board\Stone; use Gaming\ConnectFour\Domain\Game\Event\ChatAssigned; use Gaming\ConnectFour\Domain\Game\Event\GameOpened; use Gaming\ConnectFour\Domain\Game\Exception\GameException; @@ -48,20 +47,17 @@ public function id(): GameId public static function open(GameId $gameId, Configuration $configuration, string $playerId): Game { - $size = $configuration->size(); - $player = new Player($playerId, Stone::Red); - return new self( $gameId, new Open( $configuration, - $player + $playerId ), [ new GameOpened( $gameId, - $size, - $player + $configuration->size(), + $playerId ) ] ); diff --git a/src/ConnectFour/Domain/Game/State/Open.php b/src/ConnectFour/Domain/Game/State/Open.php index 8f657bfe..e1cffe43 100644 --- a/src/ConnectFour/Domain/Game/State/Open.php +++ b/src/ConnectFour/Domain/Game/State/Open.php @@ -5,51 +5,41 @@ namespace Gaming\ConnectFour\Domain\Game\State; use Gaming\ConnectFour\Domain\Game\Board\Board; -use Gaming\ConnectFour\Domain\Game\Board\Stone; use Gaming\ConnectFour\Domain\Game\Configuration; use Gaming\ConnectFour\Domain\Game\Event\GameAborted; use Gaming\ConnectFour\Domain\Game\Event\PlayerJoined; use Gaming\ConnectFour\Domain\Game\Exception\GameNotRunningException; use Gaming\ConnectFour\Domain\Game\Exception\PlayerNotOwnerException; use Gaming\ConnectFour\Domain\Game\GameId; -use Gaming\ConnectFour\Domain\Game\Player; -use Gaming\ConnectFour\Domain\Game\Players; final class Open implements State { - private Configuration $configuration; - - private Player $player; - - public function __construct(Configuration $configuration, Player $player) - { - $this->configuration = $configuration; - $this->player = $player; + public function __construct( + private readonly Configuration $configuration, + private readonly string $playerId + ) { } public function join(GameId $gameId, string $playerId): Transition { - $joinedPlayer = new Player($playerId, Stone::Yellow); $size = $this->configuration->size(); - $width = $size->width(); - $height = $size->height(); return new Transition( new Running( $this->configuration->winningRules(), - $width * $height, + $size->width * $size->height, Board::empty($size), - new Players($this->player, $joinedPlayer) + $this->configuration->createPlayers($this->playerId, $playerId) ), [ - new PlayerJoined($gameId, $joinedPlayer, $this->player) + new PlayerJoined($gameId, $playerId, $this->playerId) ] ); } public function abort(GameId $gameId, string $playerId): Transition { - if ($this->player->id() !== $playerId) { + if ($this->playerId !== $playerId) { throw new PlayerNotOwnerException(); } @@ -58,7 +48,7 @@ public function abort(GameId $gameId, string $playerId): Transition [ new GameAborted( $gameId, - $this->player + $this->playerId ) ] ); diff --git a/src/ConnectFour/Domain/Game/State/Running.php b/src/ConnectFour/Domain/Game/State/Running.php index 4db13fd9..fe03229e 100644 --- a/src/ConnectFour/Domain/Game/State/Running.php +++ b/src/ConnectFour/Domain/Game/State/Running.php @@ -102,8 +102,8 @@ public function abort(GameId $gameId, string $playerId): Transition [ new GameAborted( $gameId, - $this->players->get($playerId), - $this->players->opponentOf($playerId) + $playerId, + $this->players->opponentOf($playerId)->id() ) ] ); diff --git a/src/WebInterface/Presentation/Http/ConnectFourController.php b/src/WebInterface/Presentation/Http/ConnectFourController.php index 9d98adbe..5278c81e 100644 --- a/src/WebInterface/Presentation/Http/ConnectFourController.php +++ b/src/WebInterface/Presentation/Http/ConnectFourController.php @@ -11,10 +11,13 @@ use Gaming\ConnectFour\Application\Game\Command\OpenCommand; use Gaming\ConnectFour\Application\Game\Command\ResignCommand; use Gaming\WebInterface\Infrastructure\Security\Security; +use Gaming\WebInterface\Presentation\Http\Form\OpenType; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; -final class ConnectFourController +final class ConnectFourController extends AbstractController { public function __construct( private readonly Bus $connectFourCommandBus, @@ -22,15 +25,27 @@ public function __construct( ) { } - public function openAction(Request $request): JsonResponse + public function openAction(Request $request): Response { - return new JsonResponse( - [ - 'gameId' => $this->connectFourCommandBus->handle( - new OpenCommand($this->security->getUser()->getUserIdentifier()) + $form = $this->createForm(OpenType::class) + ->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + [$width, $height] = explode('x', $form->get('size')->getData()); + + return $this->redirectToRoute('challenge', [ + 'id' => $this->connectFourCommandBus->handle( + new OpenCommand( + $this->security->getUser()->getUserIdentifier(), + (int)$width, + (int)$height, + (int)$form->get('color')->getData() + ) ) - ] - ); + ]); + } + + return $this->redirectToRoute('lobby'); } public function joinAction(Request $request, string $gameId): JsonResponse @@ -57,6 +72,20 @@ public function abortAction(Request $request, string $gameId): JsonResponse return new JsonResponse(); } + public function abortChallengeAction(string $gameId): Response + { + try { + $this->connectFourCommandBus->handle( + new AbortCommand( + $gameId, + $this->security->getUser()->getUserIdentifier() + ) + ); + } finally { + return $this->redirectToRoute('lobby'); + } + } + public function resignAction(Request $request, string $gameId): JsonResponse { $this->connectFourCommandBus->handle( diff --git a/src/WebInterface/Presentation/Http/Form/OpenType.php b/src/WebInterface/Presentation/Http/Form/OpenType.php new file mode 100644 index 00000000..ee303ad2 --- /dev/null +++ b/src/WebInterface/Presentation/Http/Form/OpenType.php @@ -0,0 +1,92 @@ +add('size', ChoiceType::class, [ + 'label' => 'Size', + 'label_attr' => ['class' => 'btn'], + 'attr' => ['class' => 'btn-group w-100'], + 'choices' => self::sizes(), + 'choice_attr' => static fn() => ['class' => 'btn-check'], + 'expanded' => false, + 'constraints' => [ + new NotBlank(), + new Choice(choices: self::sizes()) + ] + ]) + ->add('variant', ChoiceType::class, [ + 'label' => 'Variant', + 'choices' => self::variants(), + 'choice_attr' => static fn(string $value): array => $value === 'popout' + ? ['disabled' => 'disabled'] + : [], + 'constraints' => [ + new NotBlank(), + new Choice(choices: self::variants()) + ] + ]) + ->add('color', ChoiceType::class, [ + 'label' => 'Color', + 'label_attr' => ['class' => 'btn'], + 'attr' => ['class' => 'btn-group w-100'], + 'choices' => self::colors(), + 'choice_attr' => static fn() => ['class' => 'btn-check'], + 'expanded' => true, + 'constraints' => [ + new NotBlank(), + new Choice(choices: self::colors()) + ] + ]) + ->add('open', SubmitType::class, [ + 'label' => 'Let\'s play!', + 'attr' => ['class' => 'btn-primary w-100', 'data-open-game-button' => ''], + 'row_attr' => ['class' => 'mb-0'] + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data' => ['size' => '7x6', 'variant' => 'standard', 'color' => -1] + ]); + } + + /** + * @return array + */ + private static function sizes(): array + { + return ['7 x 6' => '7x6', '8 x 7' => '8x7', '9 x 7' => '9x7', '10 x 7' => '10x7']; + } + + /** + * @return array + */ + private static function variants(): array + { + return ['Standard' => 'standard', 'PopOut' => 'popout']; + } + + /** + * @return array + */ + private static function colors(): array + { + return ['Red' => 1, 'Yellow' => 2, 'Random' => -1]; + } +} diff --git a/src/WebInterface/Presentation/Http/PageController.php b/src/WebInterface/Presentation/Http/PageController.php index d50c78f0..174b303a 100644 --- a/src/WebInterface/Presentation/Http/PageController.php +++ b/src/WebInterface/Presentation/Http/PageController.php @@ -10,14 +10,14 @@ use Gaming\ConnectFour\Application\Game\Query\OpenGamesQuery; use Gaming\ConnectFour\Application\Game\Query\RunningGamesQuery; use Gaming\WebInterface\Infrastructure\Security\Security; +use Gaming\WebInterface\Presentation\Http\Form\OpenType; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Twig\Environment; -final class PageController +final class PageController extends AbstractController { public function __construct( - private readonly Environment $twig, private readonly Bus $connectFourQueryBus, private readonly Security $security ) { @@ -25,34 +25,36 @@ public function __construct( public function lobbyAction(): Response { - return new Response( - $this->twig->render('@web-interface/lobby.html.twig', [ - 'maximumNumberOfGamesInList' => 10, - 'openGames' => $this->connectFourQueryBus->handle(new OpenGamesQuery()), - 'runningGames' => $this->connectFourQueryBus->handle(new RunningGamesQuery()), - 'user' => $this->security->getUser() - ]) - ); + return $this->render('@web-interface/lobby.html.twig', [ + 'maximumNumberOfGamesInList' => 10, + 'openGames' => $this->connectFourQueryBus->handle(new OpenGamesQuery()), + 'runningGames' => $this->connectFourQueryBus->handle(new RunningGamesQuery()), + 'user' => $this->security->getUser(), + 'openForm' => $this->createForm(OpenType::class) + ]); } public function gameAction(string $id): Response { - return new Response( - $this->twig->render('@web-interface/game.html.twig', [ - 'game' => $this->connectFourQueryBus->handle(new GameQuery($id)), - 'user' => $this->security->getUser() - ]) - ); + return $this->render('@web-interface/game.html.twig', [ + 'game' => $this->connectFourQueryBus->handle(new GameQuery($id)), + 'user' => $this->security->getUser() + ]); + } + + public function challengeAction(string $id): Response + { + return $this->render('@web-interface/challenge.html.twig', [ + 'game' => $this->connectFourQueryBus->handle(new GameQuery($id)) + ]); } public function profileAction(Request $request): Response { - return new Response( - $this->twig->render('@web-interface/profile.html.twig', [ - 'games' => $this->connectFourQueryBus->handle( - new GamesByPlayerQuery($this->security->getUser()->getUserIdentifier()) - )->games() - ]) - ); + return $this->render('@web-interface/profile.html.twig', [ + 'games' => $this->connectFourQueryBus->handle( + new GamesByPlayerQuery($this->security->getUser()->getUserIdentifier()) + )->games() + ]); } } diff --git a/src/WebInterface/Presentation/Http/View/challenge.html.twig b/src/WebInterface/Presentation/Http/View/challenge.html.twig new file mode 100644 index 00000000..fc26bdba --- /dev/null +++ b/src/WebInterface/Presentation/Http/View/challenge.html.twig @@ -0,0 +1,33 @@ +{% extends '@web-interface/layout/condensed.html.twig' %} + +{% set page_title = 'Finding your next match...' %} + +{% block content %} +

+ Finding your next match +

+
+

+ While we find the best match for you, feel free to explore other games or browse around. + Once your match is ready, you'll be automatically redirected. +

+
+ +
+
+
Running games
+
+

+ Feature not implemented yet. +

+
+ + + +{% endblock %} diff --git a/src/WebInterface/Presentation/Http/View/layout/forms.html.twig b/src/WebInterface/Presentation/Http/View/layout/forms.html.twig new file mode 100644 index 00000000..bf94090f --- /dev/null +++ b/src/WebInterface/Presentation/Http/View/layout/forms.html.twig @@ -0,0 +1,22 @@ +{% use "bootstrap_5_layout.html.twig" %} + +{%- block choice_label -%} + {% if expanded is defined and expanded %} + {{ label }} + {% else %} + {{- parent() -}} + {% endif %} +{%- endblock choice_label %} + +{%- block choice_widget_expanded -%} +
+ {%- for child in form %} + {{- form_widget(child, { + required: expanded|default(false) ? false : required, + parent_label_class: label_attr.class|default(''), + translation_domain: choice_translation_domain, + valid: valid, + }) -}} + {% endfor -%} +
+{%- endblock choice_widget_expanded %} diff --git a/src/WebInterface/Presentation/Http/View/lobby.html.twig b/src/WebInterface/Presentation/Http/View/lobby.html.twig index 3a0ae290..7519d3d4 100644 --- a/src/WebInterface/Presentation/Http/View/lobby.html.twig +++ b/src/WebInterface/Presentation/Http/View/lobby.html.twig @@ -27,9 +27,21 @@
- - Open a new game - +
diff --git a/tests/acceptance/GameCest.php b/tests/acceptance/GameCest.php index 3c72099b..099942b7 100644 --- a/tests/acceptance/GameCest.php +++ b/tests/acceptance/GameCest.php @@ -9,10 +9,10 @@ class GameCest { public function iCanOpenAGameAndAbortIt(AcceptanceTester $I): void { - $gameId = $this->prepareOpenGameScenario($I); + $this->prepareOpenGameScenario($I); - $I->click('.table-success'); - $I->waitForElementNotVisible('[data-game-id="' . $gameId . '"]'); + $I->submitForm('[data-abort-form]', []); + $I->retrySeeInCurrentUrl('/'); } public function iCanAbortAGameWithAJoinedFriend(AcceptanceTester $I): void @@ -48,10 +48,14 @@ static function (AcceptanceTester $I): void { private function prepareOpenGameScenario(AcceptanceTester $I): string { $I->amOnPage('/'); + $I->click('label[for="open-game-dropdown"]'); + $I->waitForElementVisible('[data-open-game-button]'); $I->click('[data-open-game-button]'); - $I->waitForElement('.table-success'); - return $I->grabAttributeFrom('.table-success', 'data-game-id'); + $I->retrySeeCurrentUrlMatches('#^/challenge/(.*)$#'); + preg_match('#^/challenge/(.*)$#', $I->grabFromCurrentUrl(), $matches); + + return $matches[1]; } private function prepareRunningGameScenario(AcceptanceTester $I, Friend $friend): string diff --git a/tests/unit/ConnectFour/Domain/Game/Board/SizeTest.php b/tests/unit/ConnectFour/Domain/Game/Board/SizeTest.php index 47478d2f..1496eaaf 100644 --- a/tests/unit/ConnectFour/Domain/Game/Board/SizeTest.php +++ b/tests/unit/ConnectFour/Domain/Game/Board/SizeTest.php @@ -46,8 +46,6 @@ public function itShouldThrowAnExceptionOnInvalidSizes(int $width, int $height): public function wrongSizeProvider(): array { return [ - [3, 3], - [5, 5], [-1, 3], [2, -3], [-1, -3], diff --git a/tests/unit/ConnectFour/Domain/Game/GameTest.php b/tests/unit/ConnectFour/Domain/Game/GameTest.php index a026e06c..5fe993f6 100644 --- a/tests/unit/ConnectFour/Domain/Game/GameTest.php +++ b/tests/unit/ConnectFour/Domain/Game/GameTest.php @@ -542,9 +542,10 @@ private function createDrawnGame(): Game { $game = Game::open( GameId::generate(), - Configuration::custom( + new Configuration( new Size(2, 2), - WinningRules::standard() + WinningRules::standard(), + Stone::Red ), 'playerId1' );