Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"symfony/framework-bundle": "8.1.*",
"symfony/messenger": "8.1.*",
"symfony/runtime": "8.1.*",
"symfony/serializer": "8.1.*",
"symfony/yaml": "8.1.*"
},
"require-dev": {
Expand Down
101 changes: 100 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion config/reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@
* }>,
* },
* serializer?: bool|array{ // Serializer configuration
* enabled?: bool|Param, // Default: false
* enabled?: bool|Param, // Default: true
* enable_attributes?: bool|Param, // Default: true
* name_converter?: scalar|Param|null,
* circular_reference_handler?: scalar|Param|null,
Expand Down
27 changes: 12 additions & 15 deletions config/services.yaml
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json

# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:

services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
autowire: true
autoconfigure: true

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/Chess/'

Chess\:
resource: '../src/Chess/'

# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
Chess\Infrastructure\Http\Controller\GameController:
arguments:
$commandBus: '@command.bus'
$queryBus: '@query.bus'
tags: ['controller.service_arguments']
27 changes: 15 additions & 12 deletions src/Chess/Domain/Model/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ final class Game
#[ORM\Column(type: 'json')]
private array $boardState = [];

private Board $board;
private ?Board $board = null;
private Color $currentTurnEnum;

public function __construct(
Expand All @@ -50,20 +50,13 @@ public static function start(): self
);
}

#[ORM\PostLoad]
public function postLoad(): void
{
$this->currentTurnEnum = Color::{$this->currentTurn};
$this->board = Board::fromArray($this->boardState);
}

public function play(Move $move, MoveValidator $validator): void
{
if ($this->isOver) {
throw new \DomainException('Cannot play a move on a finished game.');
}

$piece = $this->board->pieceAt($move->from());
$piece = $this->getBoard()->pieceAt($move->from());

if (null === $piece) {
throw new \DomainException(\sprintf('No piece on square "%s".', $move->from()->toString()));
Expand All @@ -73,8 +66,8 @@ public function play(Move $move, MoveValidator $validator): void
throw new \DomainException(\sprintf('It is %s\'s turn.', $this->currentTurnEnum->name));
}

$validator->validate($move, $this->board);
$this->board->movePiece($move->from(), $move->to());
$validator->validate($move, $this->getBoard());
$this->getBoard()->movePiece($move->from(), $move->to());
$this->currentTurnEnum = $this->currentTurnEnum->opposite();
$this->currentTurn = $this->currentTurnEnum->name;
$this->syncBoardState();
Expand All @@ -97,11 +90,21 @@ public function id(): GameId

public function board(): Board
{
return $this->getBoard();
}

private function getBoard(): Board
{
if (null === $this->board) {
$this->currentTurnEnum = Color::{$this->currentTurn};
$this->board = Board::fromArray($this->boardState);
}

return $this->board;
}

private function syncBoardState(): void
{
$this->boardState = $this->board->toArray();
$this->boardState = $this->getBoard()->toArray();
}
}
65 changes: 65 additions & 0 deletions src/Chess/Infrastructure/Http/Controller/GameController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Chess\Infrastructure\Http\Controller;

use Chess\Application\Command\PlayMove\PlayMoveCommand;
use Chess\Application\Command\StartGame\StartGameCommand;
use Chess\Application\Query\GetGameState\GetGameStateQuery;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\HandledStamp;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api')]
final class GameController
{
public function __construct(
private readonly MessageBusInterface $commandBus,
private readonly MessageBusInterface $queryBus,
) {
}

#[Route('/games', methods: ['POST'])]
public function startGame(): JsonResponse
{
$envelope = $this->commandBus->dispatch(new StartGameCommand());
$gameId = $envelope->last(HandledStamp::class)?->getResult();

return new JsonResponse(['gameId' => $gameId->toString()], Response::HTTP_CREATED);
}

#[Route('/games/{id}', methods: ['GET'])]
public function getGameState(string $id): JsonResponse
{
$envelope = $this->queryBus->dispatch(new GetGameStateQuery($id));
$dto = $envelope->last(HandledStamp::class)?->getResult();

return new JsonResponse([
'gameId' => $dto->gameId,
'currentTurn' => $dto->currentTurn,
'isOver' => $dto->isOver,
'pieces' => $dto->pieces,
]);
}

#[Route('/games/{id}/moves', methods: ['POST'])]
public function playMove(string $id, Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);

if (!isset($data['from'], $data['to'])) {
return new JsonResponse(
['error' => 'Missing "from" or "to" fields.'],
Response::HTTP_BAD_REQUEST
);
}

$this->commandBus->dispatch(new PlayMoveCommand($id, $data['from'], $data['to']));

return new JsonResponse(null, Response::HTTP_NO_CONTENT);
}
}
Loading