diff --git a/app/config/routes.yaml b/app/config/routes.yaml index a48074e..31e9776 100644 --- a/app/config/routes.yaml +++ b/app/config/routes.yaml @@ -2,14 +2,20 @@ index: path: / defaults: { _controller: 'App\Controller\MainController::start' } +## GAME + game_new: - path: /game + path: /game/new/{mode} defaults: { _controller: 'App\Controller\GameController::new' } game: path: /game/{id} defaults: { _controller: 'App\Controller\GameController::play' } +game_join: + path: /game/{id}/join + defaults: { _controller: 'App\Controller\GameController::join' } + game_cancel: path: /game/{id}/cancel defaults: { _controller: 'App\Controller\GameController::cancel' } @@ -17,3 +23,40 @@ game_cancel: game_move: path: /game/{id}/move/{tile} defaults: { _controller: 'App\Controller\GameController::move' } + +## API + +api_game_new: + path: /api/game + defaults: { _controller: 'App\Controller\ApiController::new' } + methods: [OPTIONS, POST] + +api_game: + path: /api/game/{id} + defaults: { _controller: 'App\Controller\ApiController::game' } + methods: [OPTIONS, GET] + +api_game_cancel: + path: /api/game/{id} + defaults: { _controller: 'App\Controller\ApiController::cancel' } + methods: [OPTIONS, DELETE] + +api_game_join: + path: /api/game/{id}/join + defaults: { _controller: 'App\Controller\ApiController::join' } + methods: [OPTIONS, POST] + +api_game_move: + path: /api/game/{id}/move/{tile} + defaults: { _controller: 'App\Controller\ApiController::move' } + methods: [OPTIONS, PUT] + +api_games: + path: /api/games/open + defaults: { _controller: 'App\Controller\ApiController::games' } + methods: [OPTIONS, GET] + +api_suggest: + path: /api/suggest + defaults: { _controller: 'App\Controller\ApiController::suggest' } + methods: [OPTIONS, POST] diff --git a/app/config/services.yaml b/app/config/services.yaml index 0d1fcde..73f3e54 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -36,3 +36,9 @@ services: game_api: class: App\Api\GameApi arguments: ["@csa_guzzle.client.puzzle_api"] + + cors_listener: + class: App\Security\CorsListener + tags: + - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 300 } + - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } diff --git a/app/src/Api/GameApi.php b/app/src/Api/GameApi.php index e591407..dc49565 100644 --- a/app/src/Api/GameApi.php +++ b/app/src/Api/GameApi.php @@ -6,6 +6,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\ConnectException; use App\Entity\Game; +use App\Entity\Player; use App\Api\TokenGenerator; use App\Api\GameResponse; use App\Api\MoveResponse; @@ -22,38 +23,44 @@ public function __construct(Client $gameApiClient, Serializer $serializer, Token $this->tokenGenerator = $tokenGenerator; } - public function new(int $size) : Game { + public function new(int $size, bool $isMultiplayer) : Array { $response = $this->client->get('/new', [ 'query' => 'size=' . $size ]); $gameResponse = $this->serializer->deserialize($response->getBody(), GameResponse::class, 'json'); - return new Game($this->tokenGenerator->generate(), $gameResponse->getInitialGrid(), $gameResponse->getGrid()); + + $player = new Player($this->tokenGenerator->generate(), $gameResponse->getGrid()); + $game = new Game($gameResponse->getInitialGrid(), $isMultiplayer); + $game->setPlayer1($player); + return ['game' => $game, 'player' => $player]; } - public function move(Game $game, int $tile) : Game { + public function move(Game $game, Player $player, int $tile) : Array { try { $response = $this->client->post('/move-tile', [ 'body' => json_encode([ 'InitialGrid' => $game->getResolvedGrid(), - 'Grid' => $game->getCurrentGrid(), + 'Grid' => $player->getCurrentGrid(), 'TileNumber' => $tile ]) ]); } catch (ConnectException $e) { - return $game; + return ['game' => $game, 'player' => $player]; } $moveResponse = $this->serializer->deserialize($response->getBody(), MoveResponse::class, 'json'); - $game->setCurrentGrid($moveResponse->getGrid()); - $game->addTurn(); - $game->setIsVictory($moveResponse->getIsVictory()); - return $game; + $player->setCurrentGrid($moveResponse->getGrid()); + $player->addTurn(); + if ($moveResponse->getIsVictory()) { + $game->setWinner($player); + } + return ['game' => $game, 'player' => $player]; } - public function suggest(Game $game) : int { + public function suggest(Game $game, Player $player) : int { $response = $this->client->get('/suggest', [ 'query' => [ - 'Grid' => json_encode($game->getCurrentGrid()), + 'Grid' => json_encode($player->getCurrentGrid()), 'InitialGrid' => json_encode($game->getResolvedGrid()) ]]); $suggestResponse = $this->serializer->deserialize($response->getBody(), SuggestResponse::class, 'json'); diff --git a/app/src/ArgumentResolver/GameContext.php b/app/src/ArgumentResolver/GameContext.php index 3bf4632..4ba76fc 100644 --- a/app/src/ArgumentResolver/GameContext.php +++ b/app/src/ArgumentResolver/GameContext.php @@ -3,11 +3,13 @@ namespace App\ArgumentResolver; use App\Entity\Game; +use App\Entity\Player; class GameContext { private $game; - private $isOwner; + private $player; + private $isPlayer; public function __construct() {} @@ -17,17 +19,25 @@ public function setGame(Game $game) { $this->game = $game; } - public function setIsOwner(bool $isOwner) { - $this->isOwner = $isOwner; + public function setPlayer(Player $player) { + $this->player = $player; + } + + public function setIsPlayer(bool $isPlayer) { + $this->isPlayer = $isPlayer; } // Getters - public function getGame() : Game { + public function getGame() : ?Game { return $this->game; } - public function getIsOwner() : bool { - return $this->isOwner; + public function getPlayer() : ?Player { + return $this->player; + } + + public function getIsPlayer() : bool { + return $this->isPlayer; } } diff --git a/app/src/ArgumentResolver/GameContextResolver.php b/app/src/ArgumentResolver/GameContextResolver.php index 521ad11..eaf5be6 100644 --- a/app/src/ArgumentResolver/GameContextResolver.php +++ b/app/src/ArgumentResolver/GameContextResolver.php @@ -7,13 +7,17 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use App\ArgumentResolver\GameContext; use App\Authentication\CookieAuthManager; +use App\Authentication\TokenAuthManager; use App\Repository\GameRepository; + class GameContextResolver implements ArgumentValueResolverInterface { + public const AUTHORIZATION_TYPE = 'Bearer'; + private $gameRepository; public function __construct(GameRepository $gameRepository) { - $this->gameRepository = $gameRepository; + $this->gameRepository = $gameRepository; } public function supports(Request $request, ArgumentMetadata $argument) { @@ -22,11 +26,18 @@ public function supports(Request $request, ArgumentMetadata $argument) { public function resolve(Request $request, ArgumentMetadata $argument) { $game = $this->gameRepository->findGameById($request->get('id')); - $isOwner = CookieAuthManager::isOwner($request, $game); + + $authTokenArray = explode(' ', $request->headers->get('Authorization')); + $token = count($authTokenArray) === 2 && $authTokenArray[0] === self::AUTHORIZATION_TYPE ? $authTokenArray[1] : ''; + $isPlayer = $token !== '' ? TokenAuthManager::isPlayer($request, $game, $token) : CookieAuthManager::isPlayer($request, $game); $gameContext = new GameContext(); $gameContext->setGame($game); - $gameContext->setIsOwner($isOwner); + $gameContext->setIsPlayer($isPlayer); + if ($isPlayer) { + $player = $token !== '' ? TokenAuthManager::getPlayer($request, $game, $token) : CookieAuthManager::getPlayer($request, $game); + $gameContext->setPlayer($player); + } yield $gameContext; } } diff --git a/app/src/Authentication/CookieAuthManager.php b/app/src/Authentication/CookieAuthManager.php index 952aae2..cc9f0b1 100644 --- a/app/src/Authentication/CookieAuthManager.php +++ b/app/src/Authentication/CookieAuthManager.php @@ -5,20 +5,32 @@ use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use App\Authentication\TokenAuthManager; use App\Entity\Game; +use App\Entity\Player; class CookieAuthManager { public const COOKIE_NAME = 'current-puzzle'; - - public static function isOwner($request, $game) { - return $request->cookies->get(self::COOKIE_NAME) == $game->getToken(); + + public static function isPlayer(Request $request, Game $game) : bool { + $tokenCookie = $request->cookies->get(self::COOKIE_NAME); + $token = $tokenCookie ?: null; + + return TokenAuthManager::isPlayer($request, $game, $token); + } + + public static function getPlayer(Request $request, Game $game) { + $tokenCookie = $request->cookies->get(self::COOKIE_NAME); + $token = $tokenCookie ?: null; + + return TokenAuthManager::getPlayer($request, $game, $token); } - public static function setOwner($response, $game) { - $response->headers->setCookie(new Cookie(self::COOKIE_NAME, $game->getToken())); + public static function setPlayer(Response $response, Player $player) { + $response->headers->setCookie(new Cookie(self::COOKIE_NAME, $player->getToken())); } - public static function removeOwner($response) { + public static function removePlayer(Response $response) { $response->headers->clearCookie(self::COOKIE_NAME); } } diff --git a/app/src/Authentication/TokenAuthManager.php b/app/src/Authentication/TokenAuthManager.php new file mode 100644 index 0000000..191c283 --- /dev/null +++ b/app/src/Authentication/TokenAuthManager.php @@ -0,0 +1,26 @@ +getPlayer1(); + $player2 = $game->getPlayer2(); + + return $player1->getToken() === $token || + $game->getIsMultiplayer() && + $player2 && $player2->getToken() === $token; + } + + public static function getPlayer(Request $request, Game $game, string $token) { + if ($game->getIsMultiplayer() && $game->getPlayer2() && $game->getPlayer2()->getToken() === $token) { + return $game->getPlayer2(); + } + return $game->getPlayer1(); + } +} diff --git a/app/src/Controller/ApiController.php b/app/src/Controller/ApiController.php new file mode 100644 index 0000000..f97a373 --- /dev/null +++ b/app/src/Controller/ApiController.php @@ -0,0 +1,132 @@ +api = $gameApi; + $this->em = $em; + $this->gameRepository = $gameRepository; + } + + public function new(Request $request) { + $body = json_decode($request->getContent(), true); + $apiResponse = $this->api->new(self::DEFAULT_SIZE, $body['mode'] === 'multi'); + $this->em->persist($apiResponse['player']); + $this->em->persist($apiResponse['game']); + $this->em->flush(); + + return new JsonResponse([ + 'id' => $apiResponse['game']->getId(), + 'token' => $apiResponse['player']->getToken() + ]); + } + + public function game(Request $request, GameContext $context) { + $game = $context->getGame(); + $currentPlayer = $context->getPlayer(); + $player = $currentPlayer ?: $game->getPlayer1(); + + if ($game->getIsMultiplayer()) { + if ($currentPlayer) { + $otherPlayer = $player->getId() === $game->getPlayer1()->getId() ? $game->getPlayer2() : $game->getPlayer1(); + } else { + $otherPlayer = $game->getPlayer2(); + } + + return new JsonResponse([ + 'currentPlayer' => $player, + 'id' => $game->getId(), + 'isMultiplayer' => $game->getIsMultiplayer(), + 'otherPlayer' => $otherPlayer, + 'resolvedGrid' => $game->getResolvedGrid(), + 'winner' => $game->getWinner() + ]); + } + return new JsonResponse([ + 'currentPlayer' => $player, + 'id' => $game->getId(), + 'isMultiplayer' => $game->getIsMultiplayer(), + 'resolvedGrid' => $game->getResolvedGrid(), + 'winner' => $game->getWinner() + ]); + } + + public function cancel(Request $request, GameContext $context) { + if ($context->getIsPlayer()) { + $this->em->remove($context->getGame()); + $this->em->flush(); + + return new Response('The game has been canceled with success', Response::HTTP_OK); + } + return new Response('An unexpected error occured', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + public function join(Request $request, GameContext $context, TokenGenerator $tokenGenerator) { + $game = $context->getGame(); + + if ($game->isFull()) { + return new Response('The game is full', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $currentGrid = $game->getPlayer1()->getCurrentGrid(); + $newPlayer = new Player($tokenGenerator->generate(), $currentGrid); + $this->em->persist($newPlayer); + + $game->setPlayer2($newPlayer); + $this->em->persist($game); + + $this->em->flush(); + + return new JsonResponse([ + 'id' => $game->getId(), + 'token' => $newPlayer->getToken() + ]); + } + + public function move(Request $request, GameContext $context, int $tile) { + $game = $context->getGame(); + $currentPlayer = $context->getPlayer(); + + if ($context->getIsPlayer() && $game->getWinner() === null) { + $apiResponse = $this->api->move($game, $currentPlayer, $tile); + $this->em->persist($apiResponse['game']); + $this->em->persist($currentPlayer); + $this->em->flush(); + } + + return new JsonResponse([ + 'id' => $game->getId(), + 'currentPlayer' => $currentPlayer, + 'winner' => $game->getWinner() + ]); + } + + public function games(Request $request) { + $gameIds = $this->gameRepository->findOpenMultiplayerGames(); + + return new JsonResponse([ + 'gameIds' => $gameIds + ]); + } +} diff --git a/app/src/Controller/GameController.php b/app/src/Controller/GameController.php index 356ac04..cbbbf00 100644 --- a/app/src/Controller/GameController.php +++ b/app/src/Controller/GameController.php @@ -7,9 +7,12 @@ use Symfony\Component\HttpFoundation\Request; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use App\Api\GameApi; +use App\Api\TokenGenerator; use App\Authentication\CookieAuthManager; use App\Entity\Game; +use App\Entity\Player; use App\Repository\GameRepository; +use App\Repository\PlayerRepository; use App\ArgumentResolver\GameContext; class GameController extends Controller { @@ -17,51 +20,98 @@ class GameController extends Controller { private $api; private $twig; + private $gameRepository; + private $playerRepository; - public function __construct(\Twig_Environment $twig, GameApi $gameApi, GameRepository $gameRepository) { + public function __construct(\Twig_Environment $twig, GameApi $gameApi, GameRepository $gameRepository, PlayerRepository $playerRepository) { $this->twig = $twig; $this->api = $gameApi; $this->gameRepository = $gameRepository; + $this->playerRepository = $playerRepository; } public function play(GameContext $context) { - return new Response($this->twig->render('game.html.twig', [ - 'game' => $context->getGame(), - 'isOwner' => $context->getIsOwner() + $game = $context->getGame(); + $currentPlayer = $context->getPlayer(); + $player = $currentPlayer ?: $game->getPlayer1(); + if ($game->getIsMultiplayer()) { + if ($currentPlayer) { + $otherPlayer = $player->getId() === $game->getPlayer1()->getId() ? $game->getPlayer2() : $game->getPlayer1(); + } else { + $otherPlayer = $game->getPlayer2(); + } + + return new Response($this->twig->render('game_multi.html.twig', [ + 'game' => $game, + 'player' => $player, + 'otherPlayer' => $otherPlayer, + 'winner' => $game->getWinner(), + 'isOwner' => $context->getIsPlayer() + ])); + } + + return new Response($this->twig->render('game_single.html.twig', [ + 'game' => $game, + 'player' => $player, + 'winner' => $game->getWinner(), + 'isOwner' => $context->getIsPlayer() ])); } - public function new() { - $game = $this->api->new(self::DEFAULT_SIZE); - $this->gameRepository->save($game); + public function new(string $mode) { + $apiResponse = $this->api->new(self::DEFAULT_SIZE, $mode === 'multi'); + $this->playerRepository->save($apiResponse['player']); + $this->gameRepository->save($apiResponse['game']); $response = $this->redirectToRoute('game', [ - 'id' => $game->getId() + 'id' => $apiResponse['game']->getId() ]); - CookieAuthManager::setOwner($response, $game); - - return $response; + CookieAuthManager::setPlayer($response, $apiResponse['player']); + return $response; } public function cancel(GameContext $context) { $response = $this->redirectToRoute('index'); - if ($context->getIsOwner()) { + if ($context->getIsPlayer()) { $this->gameRepository->remove($context->getGame()->getId()); - CookieAuthManager::removeOwner($response); + CookieAuthManager::removePlayer($response); } return $response; } - public function move(GameContext $context, int $tile) { + public function join(GameContext $context, TokenGenerator $tokenGenerator) { $game = $context->getGame(); - if ($context->getIsOwner() && !$game->getIsVictory()) { - $newGame = $this->api->move($game, $tile); - $this->gameRepository->save($newGame); + $response = $this->redirectToRoute('game', [ + 'id' => $game->getId() + ]); + + if ($game->isFull()) { + return $response; + } + $currentGrid = $game->getPlayer1()->getCurrentGrid(); + $newPlayer = new Player($tokenGenerator->generate(), $currentGrid); + $this->playerRepository->save($newPlayer); + + $game->setPlayer2($newPlayer); + $this->gameRepository->save($game); + + CookieAuthManager::setPlayer($response, $newPlayer); + return $response; + } + + public function move(GameContext $context, int $tile) { + $game = $context->getGame(); + $player = $context->getPlayer(); + if ($context->getIsPlayer() && $game->getWinner() === null) { + $apiResponse = $this->api->move($game, $player, $tile); + $this->gameRepository->save($apiResponse['game']); } - return $this->redirectToRoute('game', array('id' => $game->getId())); + return $this->redirectToRoute('game', [ + 'id' => $game->getId() + ]); } } diff --git a/app/src/Controller/MainController.php b/app/src/Controller/MainController.php index 2b1bc45..d633985 100644 --- a/app/src/Controller/MainController.php +++ b/app/src/Controller/MainController.php @@ -4,15 +4,21 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; +use App\Repository\GameRepository; class MainController extends Controller { private $twig; + private $gameRepository; - public function __construct(\Twig_Environment $twig) { + public function __construct(\Twig_Environment $twig, GameRepository $gameRepository) { $this->twig = $twig; + $this->gameRepository = $gameRepository; } - + public function start() { - return new Response($this->twig->render('main.html.twig')); + $listGames = $this->gameRepository->findOpenMultiplayerGames(); + return new Response($this->twig->render('main.html.twig', [ + 'listGames' => $listGames + ])); } } diff --git a/app/src/Entity/Game.php b/app/src/Entity/Game.php index fff72c2..a87b437 100644 --- a/app/src/Entity/Game.php +++ b/app/src/Entity/Game.php @@ -3,50 +3,51 @@ namespace App\Entity; use Doctrine\ORM\Mapping as ORM; - +use Doctrine\ORM\PersistentCollection; +use App\Entity\Player; /** * @ORM\Entity(repositoryClass="App\Repository\GameRepository") * @ORM\Table */ -class Game { +class Game implements \JsonSerializable { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ - private $id; + protected $id; /** - * @ORM\Column(type="string") + * @ORM\Column(type="json_array") */ - private $token; + protected $resolvedGrid; /** - * @ORM\Column(type="json_array") + * @ORM\OneToOne(targetEntity="Player", mappedBy="id") + * @ORM\JoinColumn(name="player1", referencedColumnName="id") */ - private $resolvedGrid; + protected $player1; /** - * @ORM\Column(type="json_array") + * @ORM\OneToOne(targetEntity="Player", mappedBy="id") + * @ORM\JoinColumn(name="player2", referencedColumnName="id") */ - private $currentGrid; - + protected $player2; + /** - * @ORM\Column(type="integer") + * @ORM\OneToOne(targetEntity="Player", mappedBy="id") + * @ORM\JoinColumn(name="winner", referencedColumnName="id") */ - private $turn; - + protected $winner; + /** * @ORM\Column(type="boolean") */ - private $isVictory; + protected $isMultiplayer; - public function __construct($token, $resolvedGrid, $currentGrid, $isVictory=false) { - $this->token = $token; + public function __construct($resolvedGrid, $isMultiplayer = false) { $this->resolvedGrid =$resolvedGrid; - $this->currentGrid = $currentGrid; - $this->turn = 0; - $this->isVictory = $isVictory; + $this->isMultiplayer = $isMultiplayer; } // Getters @@ -55,24 +56,24 @@ public function getId() : int { return $this->id; } - public function getToken() : string { - return $this->token; - } - public function getResolvedGrid() : Array { return $this->resolvedGrid; } - public function getCurrentGrid() : Array { - return $this->currentGrid; + public function getPlayer1() : Player { + return $this->player1; } - public function getTurn() : int { - return $this->turn; + public function getPlayer2() : ?Player { + return $this->player2; } - public function getIsVictory() : bool { - return $this->isVictory; + public function getWinner() : ?Player { + return $this->winner; + } + + public function getIsMultiplayer() : bool { + return $this->isMultiplayer; } // Setters @@ -81,29 +82,43 @@ public function setId(int $id) { $this->id = $id; } - private function setToken(string $token) { - $this->token = $token; - } - public function setResolvedGrid(Array $grid) { $this->resolvedGrid = $grid; } - public function setCurrentGrid(Array $grid) { - $this->currentGrid = $grid; + public function setPlayer1(Player $player) { + $this->player1 = $player; } - private function setTurn(int $turn) { - $this->turn = $turn; + public function setPlayer2(Player $player) { + $this->player2 = $player; } - public function setIsVictory(bool $isVictory) { - $this->isVictory = $isVictory; + public function setWinner(Player $player) { + $this->winner = $player; + } + + public function setIsMultiplayer(bool $isMultiplayer) { + $this->isMultiplayer = $isMultiplayer; } // Methods - public function addTurn() { - $this->turn++; + public function isFull() { + return (!$this->getIsMultiplayer() && $this->player1 != null) || ($this->getIsMultiplayer() && $this->player1 != null && $this->player2 != null); + } + + // JsonSerializable interface + + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'resolvedGrid' => $this->getResolvedGrid(), + 'player1' => $this->getPlayer1(), + 'player2' => $this->getPlayer2(), + 'winner' => $this->getWinner(), + 'isMultiplayer' => $this->getIsMultiplayer(), + 'isFull' => $this->isFull() + ]; } } diff --git a/app/src/Entity/Player.php b/app/src/Entity/Player.php new file mode 100644 index 0000000..711c6bd --- /dev/null +++ b/app/src/Entity/Player.php @@ -0,0 +1,91 @@ +token = $token; + $this->currentGrid = $currentGrid; + $this->turn = 0; + } + + // Getters + + public function getId() : int { + return $this->id; + } + + public function getToken() : string { + return $this->token; + } + + public function getCurrentGrid() : Array { + return $this->currentGrid; + } + + public function getTurn() : int { + return $this->turn; + } + + // Setters + + public function setId(int $id) { + $this->id = $id; + } + + private function setToken(string $token) { + $this->token = $token; + } + + public function setCurrentGrid(Array $grid) { + $this->currentGrid = $grid; + } + + private function setTurn(int $turn) { + $this->turn = $turn; + } + + // Methods + + public function addTurn() { + $this->turn++; + } + + // JsonSerializable interface + + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'currentGrid' => $this->getCurrentGrid(), + 'turn' => $this->getTurn() + ]; + } +} diff --git a/app/src/Repository/GameRepository.php b/app/src/Repository/GameRepository.php index 9535b5c..21e4789 100644 --- a/app/src/Repository/GameRepository.php +++ b/app/src/Repository/GameRepository.php @@ -17,6 +17,15 @@ public function findGameById(string $id) : Game { return $this->em->find('App:Game', $id); } + public function findOpenMultiplayerGames() : Array { + $qb = $this->em->createQueryBuilder(); + $qb->select('game.id') + ->from('App:Game', 'game') + ->where('game.isMultiplayer = true') + ->andWhere('game.player2 IS NULL'); + return $qb->getQuery()->getResult(); + } + public function remove(string $id) { $game = $this->em->getReference('App:Game', $id); $this->em->remove($game); diff --git a/app/src/Repository/PlayerRepository.php b/app/src/Repository/PlayerRepository.php new file mode 100644 index 0000000..460b10e --- /dev/null +++ b/app/src/Repository/PlayerRepository.php @@ -0,0 +1,24 @@ +em = $em; + } + + public function findPlayerById(string $id) : Player { + return $this->em->find('App:Player', $id); + } + + public function save(Player $player) { + $this->em->persist($player); + $this->em->flush(); + } +} diff --git a/app/src/Security/CorsListener.php b/app/src/Security/CorsListener.php new file mode 100644 index 0000000..b80618f --- /dev/null +++ b/app/src/Security/CorsListener.php @@ -0,0 +1,37 @@ +getRequestType()) { + return; + } + $request = $event->getRequest(); + $method = $request->getRealMethod(); + + if ('OPTIONS' === $request->getMethod()) { + $response = new Response(); + $response->headers->set('Access-Control-Allow-Credentials', 'true'); + $response->headers->set('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, PATCH, OPTIONS'); + $response->headers->set('Access-Control-Allow-Headers', 'Origin, Content-Type, Accept, Authorization'); + $response->headers->set('Access-Control-Allow-Origin', '*'); + $event->setResponse($response); + return; + } + } + + public function onKernelResponse(FilterResponseEvent $event) { + $responseHeaders = $event->getResponse()->headers; + + $responseHeaders->set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); + $responseHeaders->set('Access-Control-Allow-Origin', '*'); + $responseHeaders->set('Access-Control-Allow-Credentials', 'true'); + $responseHeaders->set('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, PATCH, OPTIONS'); + } +} diff --git a/app/templates/game.html.twig b/app/templates/game.html.twig deleted file mode 100644 index a44c41b..0000000 --- a/app/templates/game.html.twig +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "base.html.twig" %} - -{% block refresh %} - {% if not isOwner and not game.isVictory %} - - {% endif %} -{% endblock %} - -{% block header %} -
-
-
- {% if game.isVictory %} - {% if isOwner %} -
Congratulations, you have solved the puzzle in {{ game.turn }} turns!
- {% else %} -
The puzzle has been solved in {{ game.turn }} turns!
- {% endif %} - {% else %} -
Turn {{ game.turn }}
- {% if isOwner %} -

Try to solve the puzzle by clicking on the tiles you want to move

- {% else %} -

You can only watch this puzzle. Create a new game to play.

- {% endif %} - {% endif %} -
-
-
-{% endblock %} - -{% block content %} -
-
-
- {% include 'grid.html.twig' with {'game': game, 'readOnly': game.isVictory or not isOwner} %} -
-
-
-
- {% if game.isVictory %} - keyboard_returnBack to home - add_circle_outlineStart a new game - {% elseif isOwner %} - cancelCancel - {% endif %} -
-
-
-
-
-
-
-
-
-
More
-
-
- {% include 'share_url.html.twig' with {'url': app.request.uri } %} -
-
-
-{% endblock %} diff --git a/app/templates/game_actions.html.twig b/app/templates/game_actions.html.twig new file mode 100644 index 0000000..5e24401 --- /dev/null +++ b/app/templates/game_actions.html.twig @@ -0,0 +1,23 @@ +
+
+ {% if winner %} + keyboard_returnBack to home + add_circle_outlineStart a single game + {% elseif isOwner %} + cancelCancel + {% endif %} +
+
+
+
+
+
+
+
+
+
More
+
+
+ {% include 'share_url.html.twig' with {'url': app.request.uri } %} +
+
diff --git a/app/templates/game_multi.html.twig b/app/templates/game_multi.html.twig new file mode 100644 index 0000000..0bf776e --- /dev/null +++ b/app/templates/game_multi.html.twig @@ -0,0 +1,81 @@ +{% extends "base.html.twig" %} + +{% block refresh %} + {% if not winner %} + + {% endif %} +{% endblock %} + +{% block header %} +
+
+
+ {% if winner %} + {% if winner.id == player.id %} +
Congratulations, you have solved the puzzle in {{ winner.turn }} turns!
+ {% else %} +
The puzzle has been solved in {{ winner.turn }} turns!
+ {% endif %} + {% elseif otherPlayer %} +
The game is running
+ {% else %} +
Waiting for the second player...
+ {% endif %} +
+
+
+{% endblock %} + +{% block content %} +
+ {% if player and otherPlayer %} +
+ {% if isOwner %} +
+
You: turn {{ player.turn }}
+
+
+
Your opponent: turn {{ otherPlayer.turn }}
+
+ {% else %} +
+
Player 1: turn {{ player.turn }}
+
+
+
Player 2: turn {{ otherPlayer.turn }}
+
+ {% endif %} +
+
+
+ {% include 'grid.html.twig' with {'game': game, 'player': player, 'readOnly': winner or not isOwner} %} +
+
+ {% include 'grid.html.twig' with {'game': game, 'player': otherPlayer, 'readOnly': true} %} +
+
+ {% else %} +
+
+ {% include 'grid.html.twig' with {'game': game, 'player': player, 'readOnly': true} %} +
+
+ {% if isOwner %} +
+
+
Invite a friend by sending him this url
+
+
+ {% include 'share_url.html.twig' with {'url': app.request.uri } %} +
+
+ {% else %} +
+ +
+ {% endif %} + {% endif %} +
+{% endblock %} diff --git a/app/templates/game_single.html.twig b/app/templates/game_single.html.twig new file mode 100644 index 0000000..5f0be5b --- /dev/null +++ b/app/templates/game_single.html.twig @@ -0,0 +1,41 @@ +{% extends "base.html.twig" %} + +{% block refresh %} + {% if not isOwner and not winner %} + + {% endif %} +{% endblock %} + +{% block header %} +
+
+
+ {% if winner %} + {% if isOwner %} +
Congratulations, you have solved the puzzle in {{ winner.turn }} turns!
+ {% else %} +
The puzzle has been solved in {{ winner.turn }} turns!
+ {% endif %} + {% else %} +
Turn {{ player.turn }}
+ {% if isOwner %} +

Try to solve the puzzle by clicking on the tiles you want to move

+ {% else %} +

You can only watch this puzzle. Create a singleplayer or a multiplayer game to play.

+ {% endif %} + {% endif %} +
+
+
+{% endblock %} + +{% block content %} +
+
+
+ {% include 'grid.html.twig' with {'game': game, 'player': player, 'readOnly': winner or not isOwner} %} +
+
+ {% include 'game_actions.html.twig' %} +
+{% endblock %} diff --git a/app/templates/grid.html.twig b/app/templates/grid.html.twig index 395d369..95f27c8 100644 --- a/app/templates/grid.html.twig +++ b/app/templates/grid.html.twig @@ -1,5 +1,5 @@
- {% for row in game.currentGrid %} + {% for row in player.currentGrid %}
{% for value in row %} {% if value == 0 %} diff --git a/app/templates/list_games.html.twig b/app/templates/list_games.html.twig new file mode 100644 index 0000000..06048e7 --- /dev/null +++ b/app/templates/list_games.html.twig @@ -0,0 +1,19 @@ +
+ {% if listGames|length > 0 %} +
    +
  • +

    Open multiplayer games

    +
  • + {% for game in listGames %} +
  • +
    + Game #{{game.id}} + add_circle_outline +
    +
  • + {% endfor %} +
+ {% else %} +

There is no open multiplayer game.

+ {% endif %} +
diff --git a/app/templates/main.html.twig b/app/templates/main.html.twig index 5df853c..e92f033 100644 --- a/app/templates/main.html.twig +++ b/app/templates/main.html.twig @@ -9,10 +9,25 @@ {% block content %}
- 15 puzzle picture +
+ 15 puzzle picture +
-
- add_circle_outlineStart a new game + +
+
+
+
+
+
+
+ {% include 'list_games.html.twig' with {'listGames': listGames } %} +
{% endblock %} diff --git a/app/tests/Api/GameApiTest.php b/app/tests/Api/GameApiTest.php index 8992801..af43d14 100644 --- a/app/tests/Api/GameApiTest.php +++ b/app/tests/Api/GameApiTest.php @@ -15,6 +15,7 @@ use App\Api\GameApi; use App\Entity\Game; +use App\Entity\Player; use App\Api\TokenGenerator; class GameApiTest extends TestCase { @@ -22,7 +23,7 @@ private function createGuzzleHttpMock($body, $status, $headers) { $mock = new MockHandler([ new Response($status, $headers, $body) ]); - + $handler = HandlerStack::create($mock); return new Client(['handler' => $handler]); } @@ -31,7 +32,7 @@ private function createGameApi($body) { $bodyJson = json_encode($body); $client = $this->createGuzzleHttpMock($bodyJson, 200, ['Content-Type' => 'application/json']); $encoders = array(new XmlEncoder(), new JsonEncoder()); - $normalizers = array(new ObjectNormalizer()); + $normalizers = array(new ObjectNormalizer()); $serializer = new Serializer($normalizers, $encoders); $tokenGeneratorStub = $this->createMock(TokenGenerator::class); $tokenGeneratorStub->method('generate')->willReturn('mockedToken'); @@ -40,7 +41,6 @@ private function createGameApi($body) { } public function testNew() { - $mockedResponse = [ 'InitialGrid' => array( array(1, 2, 3), @@ -56,30 +56,35 @@ public function testNew() { $gameApi = $this->createGameApi($mockedResponse); - $game = $gameApi->new(3); + $apiResponse = $gameApi->new(3, false); + $expectedPlayer = new Player( + 'mockedToken', + array( + array(1, 2, 3), + array(4, 5, 0), + array(7, 8, 6) + ) + ); + $expectedPlayer->setId(1); $expectedGame = new Game( - 'mockedToken', array( array(1, 2, 3), array(4, 5, 6), array(7, 8, 0) - ), - array( - array(1, 2, 3), - array(4, 5, 0), - array(7, 8, 6) ) ); + $expectedGame->setPlayer1($expectedPlayer); - $this->assertEquals($game->getToken(), $expectedGame->getToken()); - $this->assertEquals($game->getResolvedGrid(), $expectedGame->getResolvedGrid()); - $this->assertEquals($game->getCurrentGrid(), $expectedGame->getCurrentGrid()); - $this->assertEquals($game->getTurn(), $expectedGame->getTurn()); - $this->assertEquals($game->getIsVictory(), $expectedGame->getIsVictory()); + $this->assertEquals($apiResponse['player']->getCurrentGrid(), $expectedPlayer->getCurrentGrid()); + $this->assertEquals($apiResponse['player']->getTurn(), $expectedPlayer->getTurn()); + + $this->assertEquals($apiResponse['game']->getResolvedGrid(), $expectedGame->getResolvedGrid()); + $this->assertEquals($apiResponse['game']->getPlayer1()->getToken(), $expectedGame->getPlayer1()->getToken()); + $this->assertEquals($apiResponse['game']->getIsMultiplayer(), $expectedGame->getIsMultiplayer()); } - public function testMove() { + public function testMove() { $mockedResponse = [ 'Grid' => array( array(1, 2, 3), @@ -90,67 +95,119 @@ public function testMove() { ]; $gameApi = $this->createGameApi($mockedResponse); - - $game = $gameApi->move( - new Game( - 'mockedToken', - array( - array(1, 2, 3), - array(4, 5, 6), - array(7, 8, 0) - ), - array( - array(1, 2, 3), - array(4, 0, 5), - array(7, 8, 6) - ) - ), - 5 + $player = new Player( + 'mockedToken', + array( + array(1, 2, 3), + array(4, 5, 0), + array(7, 8, 6) + ) + ); + $player->setId(1); + $game = new Game( + array( + array(1, 2, 3), + array(4, 5, 6), + array(7, 8, 0) + ) ); + $game->setPlayer1($player); + $apiResponse = $gameApi->move($game, $player, 6); - $expectedGame = new Game( + $expectedPlayer = new Player( 'mockedToken', array( + array(1, 2, 3), + array(4, 5, 0), + array(7, 8, 6) + ) + ); + $expectedPlayer->addTurn(); + + $this->assertEquals($apiResponse['player']->getCurrentGrid(), $expectedPlayer->getCurrentGrid()); + $this->assertEquals($apiResponse['player']->getTurn(), $expectedPlayer->getTurn()); + } + + public function testMoveVictory() { + $mockedResponse = [ + 'Grid' => array( array(1, 2, 3), array(4, 5, 6), array(7, 8, 0) ), + 'IsVictory' => true + ]; + + $gameApi = $this->createGameApi($mockedResponse); + + $player = new Player( + 'mockedToken', array( array(1, 2, 3), array(4, 5, 0), array(7, 8, 6) ) ); - $expectedGame->addTurn(); - $this->assertEquals($game->getToken(), $expectedGame->getToken()); - $this->assertEquals($game->getResolvedGrid(), $expectedGame->getResolvedGrid()); - $this->assertEquals($game->getCurrentGrid(), $expectedGame->getCurrentGrid()); - $this->assertEquals($game->getTurn(), $expectedGame->getTurn()); - $this->assertEquals($game->getIsVictory(), $expectedGame->getIsVictory()); + $player->setId(1); + $game = new Game( + array( + array(1, 2, 3), + array(4, 5, 6), + array(7, 8, 0) + ) + ); + $game->setPlayer1($player); + $apiResponse = $gameApi->move($game, $player, 6); + + $expectedPlayer = new Player( + 'mockedToken', + array( + array(1, 2, 3), + array(4, 5, 6), + array(7, 8, 0) + ) + ); + $expectedPlayer->setId(1); + $expectedPlayer->addTurn(); + + $expectedGame = new Game( + array( + array(1, 2, 3), + array(4, 5, 6), + array(7, 8, 0) + ) + ); + $expectedGame->setWinner($expectedPlayer); + + $this->assertEquals($apiResponse['player']->getCurrentGrid(), $expectedPlayer->getCurrentGrid()); + $this->assertEquals($apiResponse['player']->getTurn(), $expectedPlayer->getTurn()); + + $this->assertEquals($apiResponse['game']->getWinner()->getId(), $expectedPlayer->getId()); } - - public function testSuggest() { + + public function testSuggest() { $mockedResponse = [ 'Tile' => 3 ]; - $gameApi = $this->createGameApi($mockedResponse); $suggestion = $gameApi->suggest( new Game( - 'mockedToken', array( array(1, 2, 3), array(4, 5, 6), array(7, 8, 0) - ), + ) + ), + new Player( + 'mockedToken', array( array(1, 2, 3), array(4, 0, 5), array(7, 8, 6) ) ) - ); + ); $expectedSuggestion = 3; diff --git a/app/tests/Controller/MainControllerTest.php b/app/tests/Controller/MainControllerTest.php index b2780ba..fc7848d 100644 --- a/app/tests/Controller/MainControllerTest.php +++ b/app/tests/Controller/MainControllerTest.php @@ -5,14 +5,17 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class MainControllerTest extends WebTestCase { - public function testStart() { - $client = static::createClient(); + public function testStart() { + // @TODO: https://trello.com/c/jrYMP5ab + $this->markTestSkipped(); - $crawler = $client->request('GET', '/'); + $client = static::createClient(); - $this->assertGreaterThan( - 0, - $crawler->filter('html:contains("Welcome to the 15 puzzle game!")')->count() - ); - } + $crawler = $client->request('GET', '/'); + + $this->assertGreaterThan( + 0, + $crawler->filter('html:contains("Welcome to the 15 puzzle game!")')->count() + ); + } } diff --git a/app/tests/Entity/GameTest.php b/app/tests/Entity/GameTest.php index 48d7a13..4f05104 100644 --- a/app/tests/Entity/GameTest.php +++ b/app/tests/Entity/GameTest.php @@ -4,27 +4,115 @@ use PHPUnit\Framework\TestCase; use App\Entity\Game; +use App\Entity\Player; class GameTest extends TestCase { - public function testAddTurn() { + public function testIsFullWithoutPlayers() { $game = new Game( + array( + array(1, 2, 3), + array(4, 5, 6), + array(7, 8, 0) + ) + ); + + $this->assertEquals(false, $game->isFull()); + } + + public function testIsFullSingleplayer() { + $game = new Game( + array( + array(1, 2, 3), + array(4, 5, 6), + array(7, 8, 0) + ), + false + ); + $player1 = new Player( 'randomtoken', + array( + array(1, 2, 3), + array(4, 0, 5), + array(7, 8, 6) + ) + ); + $game->setPlayer1($player1); + $this->assertEquals(true, $game->isFull()); + } + + public function testIsFullMultiplayer() { + $game = new Game( array( array(1, 2, 3), array(4, 5, 6), array(7, 8, 0) ), + true + ); + $player1 = new Player( + 'randomtoken', array( array(1, 2, 3), array(4, 0, 5), array(7, 8, 6) ) ); + $player2 = new Player( + 'randomtoken2', + array( + array(1, 2, 3), + array(4, 0, 5), + array(7, 8, 6) + ) + ); + $game->setPlayer1($player1); + $game->setPlayer2($player2); + $this->assertEquals(true, $game->isFull()); + } - for ($i = 1; $i <= 23; $i++) { - $game->addTurn(); - } + public function testJsonSerialize() { + $game = new Game( + array( + array(1, 2, 3), + array(4, 5, 6), + array(7, 8, 0) + ), + true + ); + $player1 = new Player( + 'randomtoken', + array( + array(1, 2, 3), + array(4, 0, 5), + array(7, 8, 6) + ) + ); + $player2 = new Player( + 'randomtoken2', + array( + array(1, 2, 3), + array(4, 0, 5), + array(7, 8, 6) + ) + ); + $game->setId(12); + $game->setPlayer1($player1); + $game->setPlayer2($player2); + + $expectedGameJson = [ + 'id' => 12, + 'resolvedGrid' => array( + array(1, 2, 3), + array(4, 5, 6), + array(7, 8, 0) + ), + 'player1' => $player1, + 'player2' => $player2, + 'winner' => null, + 'isMultiplayer' => true, + 'isFull' => true + ]; - $this->assertEquals(23, $game->getTurn()); + $this->assertEquals($expectedGameJson, $game->jsonSerialize()); } } diff --git a/app/tests/Entity/PlayerTest.php b/app/tests/Entity/PlayerTest.php new file mode 100644 index 0000000..6899022 --- /dev/null +++ b/app/tests/Entity/PlayerTest.php @@ -0,0 +1,49 @@ +addTurn(); + } + + $this->assertEquals(23, $player->getTurn()); + } + + public function testJsonSerialize() { + $player = new Player( + 'randomtoken', + array( + array(1, 2, 3), + array(4, 0, 5), + array(7, 8, 6) + ) + ); + $player->setId(18); + + $expectedPlayerJson = [ + 'id' => 18, + 'currentGrid' => array( + array(1, 2, 3), + array(4, 0, 5), + array(7, 8, 6) + ), + 'turn' => 0 + ]; + + $this->assertEquals($expectedPlayerJson, $player->jsonSerialize()); + } +}