This commit is contained in:
Aleksandr Zaitsev 2024-03-11 15:17:09 +02:00
commit c8fdc370d5
63 changed files with 9281 additions and 0 deletions

30
.env Normal file
View File

@ -0,0 +1,30 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=817b5146c5c5923837d6ad605da79a14
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###

6
.env.test Normal file
View File

@ -0,0 +1,6 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###
###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
assets/vendor/installed.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php return array (
'@hotwired/stimulus' =>
array (
'version' => '3.2.2',
'dependencies' =>
array (
),
'extraFiles' =>
array (
),
),
'@hotwired/turbo' =>
array (
'version' => '7.3.0',
'dependencies' =>
array (
),
'extraFiles' =>
array (
),
),
);

17
bin/console Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};

23
bin/phpunit Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
}
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}

84
composer.json Normal file
View File

@ -0,0 +1,84 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.11",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.1",
"fzaninotto/faker": "^1.5",
"symfony/console": "7.0.*",
"symfony/dotenv": "7.0.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "7.0.*",
"symfony/monolog-bundle": "^3.0",
"symfony/runtime": "7.0.*",
"symfony/serializer": "7.0.*",
"symfony/string": "7.0.*",
"symfony/translation": "7.0.*",
"symfony/twig-bundle": "7.0.*",
"symfony/uid": "7.0.*",
"symfony/validator": "7.0.*",
"symfony/yaml": "7.0.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.0.*"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^7.0"
}
}

6877
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
config/bundles.php Normal file
View File

@ -0,0 +1,11 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
];

View File

@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

View File

@ -0,0 +1,50 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

View File

@ -0,0 +1,16 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
#csrf_protection: true
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true
#fragments: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

View File

@ -0,0 +1,62 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
path: php://stderr
level: debug
formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: [deprecation]
path: php://stderr
formatter: monolog.formatter.json

View File

@ -0,0 +1,10 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
when@prod:
framework:
router:
strict_requirements: null

View File

@ -0,0 +1,7 @@
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
providers:

View File

@ -0,0 +1,6 @@
twig:
file_name_pattern: '*.twig'
when@test:
twig:
strict_variables: true

View File

@ -0,0 +1,11 @@
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false

5
config/preload.php Normal file
View File

@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

5
config/routes.yaml Normal file
View File

@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

View File

@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

24
config/services.yaml Normal file
View File

@ -0,0 +1,24 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# 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.
# 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/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240310202612 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE division (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', tournament_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', title VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_1017471433D1A3E7 (tournament_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE game (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE game_score (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', game_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', player_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', score INT DEFAULT NULL, INDEX IDX_AA4EDEE48FD905 (game_id), INDEX IDX_AA4EDE99E6F5DF (player_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE player (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', tournament_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', division_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', title VARCHAR(255) NOT NULL, register_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_98197A6533D1A3E7 (tournament_id), INDEX IDX_98197A6541859289 (division_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE playoff_game (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', tournament_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', game_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', stage INT NOT NULL, INDEX IDX_E058C8A133D1A3E7 (tournament_id), UNIQUE INDEX UNIQ_E058C8A1E48FD905 (game_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE qualifying_game (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', division_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', game_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', INDEX IDX_EA15519D41859289 (division_id), UNIQUE INDEX UNIQ_EA15519DE48FD905 (game_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE tournament (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', title VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE division ADD CONSTRAINT FK_1017471433D1A3E7 FOREIGN KEY (tournament_id) REFERENCES tournament (id)');
$this->addSql('ALTER TABLE game_score ADD CONSTRAINT FK_AA4EDEE48FD905 FOREIGN KEY (game_id) REFERENCES game (id)');
$this->addSql('ALTER TABLE game_score ADD CONSTRAINT FK_AA4EDE99E6F5DF FOREIGN KEY (player_id) REFERENCES player (id)');
$this->addSql('ALTER TABLE player ADD CONSTRAINT FK_98197A6533D1A3E7 FOREIGN KEY (tournament_id) REFERENCES tournament (id)');
$this->addSql('ALTER TABLE player ADD CONSTRAINT FK_98197A6541859289 FOREIGN KEY (division_id) REFERENCES division (id)');
$this->addSql('ALTER TABLE playoff_game ADD CONSTRAINT FK_E058C8A133D1A3E7 FOREIGN KEY (tournament_id) REFERENCES tournament (id)');
$this->addSql('ALTER TABLE playoff_game ADD CONSTRAINT FK_E058C8A1E48FD905 FOREIGN KEY (game_id) REFERENCES game (id)');
$this->addSql('ALTER TABLE qualifying_game ADD CONSTRAINT FK_EA15519D41859289 FOREIGN KEY (division_id) REFERENCES division (id)');
$this->addSql('ALTER TABLE qualifying_game ADD CONSTRAINT FK_EA15519DE48FD905 FOREIGN KEY (game_id) REFERENCES game (id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE division DROP FOREIGN KEY FK_1017471433D1A3E7');
$this->addSql('ALTER TABLE game_score DROP FOREIGN KEY FK_AA4EDEE48FD905');
$this->addSql('ALTER TABLE game_score DROP FOREIGN KEY FK_AA4EDE99E6F5DF');
$this->addSql('ALTER TABLE player DROP FOREIGN KEY FK_98197A6533D1A3E7');
$this->addSql('ALTER TABLE player DROP FOREIGN KEY FK_98197A6541859289');
$this->addSql('ALTER TABLE playoff_game DROP FOREIGN KEY FK_E058C8A133D1A3E7');
$this->addSql('ALTER TABLE playoff_game DROP FOREIGN KEY FK_E058C8A1E48FD905');
$this->addSql('ALTER TABLE qualifying_game DROP FOREIGN KEY FK_EA15519D41859289');
$this->addSql('ALTER TABLE qualifying_game DROP FOREIGN KEY FK_EA15519DE48FD905');
$this->addSql('DROP TABLE division');
$this->addSql('DROP TABLE game');
$this->addSql('DROP TABLE game_score');
$this->addSql('DROP TABLE player');
$this->addSql('DROP TABLE playoff_game');
$this->addSql('DROP TABLE qualifying_game');
$this->addSql('DROP TABLE tournament');
}
}

38
phpunit.xml.dist Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
convertDeprecationsToExceptions="false"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<extensions>
</extensions>
</phpunit>

9
public/index.php Normal file
View File

@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

0
src/Controller/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,49 @@
<?php
namespace App\Controller;
use App\Entity\Division;
use App\Entity\Player;
use App\Entity\Tournament;
use App\Repository\DivisionRepository;
use App\Repository\PlayerRepository;
use App\Repository\TournamentRepository;
use App\Service\PlayOffGameService;
use App\Service\QualifyingGameService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class TournamentController extends AbstractController
{
public function __construct(
private TournamentRepository $tournamentRepository,
private DivisionRepository $divisionRepository,
private PlayerRepository $playerRepository,
private QualifyingGameService $qualifyingGameService,
private PlayOffGameService $playOffGameService,
) {}
#[Route(path: '/tournament/{tournamentId}', name: 'tournament_get', methods: ['GET'])]
public function getTournament($tournamentId): Response
{
$tournament = $this->tournamentRepository->find($tournamentId);
$isQualifyingGameEnd = true;
foreach ($tournament->getDivisionList() as $division) {
$this->qualifyingGameService->scheduleGames($division);
$isQualifyingGameEnd = $this->qualifyingGameService->isQualificationGamesComplete($division) && $isQualifyingGameEnd;
}
if ($isQualifyingGameEnd) {
$qualificationWinners = $this->qualifyingGameService->getQualificationGameWinners($tournament);
$this->playOffGameService->scheduleGames($tournament, $qualificationWinners);
}
return $this->render(
'tournament.html.twig',
['tournament' => $tournament]
);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Controller;
use App\Entity\Division;
use App\Entity\Player;
use App\Entity\Tournament;
use App\Repository\DivisionRepository;
use App\Repository\Game\GameScoreRepository;
use App\Repository\PlayerRepository;
use App\Repository\TournamentRepository;
use App\Service\PlayOffGameService;
use App\Service\QualifyingGameService;
use Faker\Factory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class TournamentGeneratorController extends AbstractController
{
private const DIVISION_COUNT = 2;
private const DIVISION_PLAYERS_MIN = 6;
private const DIVISION_PLAYERS_MAX = 8;
private const PLAYER_GAME_MIN_SCORE = 0;
private const PLAYER_GAME_MAX_SCORE = 5;
public function __construct(
private TournamentRepository $tournamentRepository,
private DivisionRepository $divisionRepository,
private PlayerRepository $playerRepository,
private GameScoreRepository $gameScoreRepository,
private QualifyingGameService $qualifyingGameService,
private PlayOffGameService $playOffGameService
) {}
#[Route(path: '/', name: 'tournament_generate', methods: ['get'])]
public function index(Request $request): Response
{
$faker = Factory::create();
$tournament = new Tournament($faker->city);
$tournament = $this->tournamentRepository->save($tournament);
for ($d = 1; $d <= self::DIVISION_COUNT; $d++) {
$division = new Division($faker->company, $tournament);
$division = $this->divisionRepository->save($division);
$playerCount = $faker->numberBetween(self::DIVISION_PLAYERS_MIN, self::DIVISION_PLAYERS_MAX);
for ($p = 1; $p <= $playerCount; $p++) {
$player = new Player($faker->unique()->lastName, $tournament, $division);
$this->playerRepository->save($player);
}
}
foreach ($tournament->getDivisionList() as $division) {
$this->qualifyingGameService->scheduleGames($division);
foreach ($division->getGameList() as $divisionGame) {
foreach ($divisionGame->getScoreList() as $score) {
$score->setScore($faker->biasedNumberBetween(self::PLAYER_GAME_MIN_SCORE, self::PLAYER_GAME_MAX_SCORE));
$this->gameScoreRepository->save($score);
}
}
}
$isQualifyingGameEnd = false;
foreach ($tournament->getDivisionList() as $division) {
$this->qualifyingGameService->scheduleGames($division);
$isQualifyingGameEnd = $this->qualifyingGameService->isQualificationGamesComplete($division);
if (!$isQualifyingGameEnd) break;
}
if ($isQualifyingGameEnd) {
$qualificationWinners = $this->qualifyingGameService->getQualificationGameWinners($tournament);
$firstStage = $qualificationWinners->count() / 2;
$this->playOffGameService->scheduleGames($tournament, $qualificationWinners);
for($stage = $firstStage; $stage >= 1; $stage = $stage / 2) {
$stageGames = $tournament->getStageGames($stage);
foreach ($stageGames as $game) {
$scoreNumberList = range(0, self::DIVISION_PLAYERS_MAX);
foreach ($game->getScoreList() as $score) {
$scoreNumber = $faker->randomElement($scoreNumberList);
$scoreNumberList = array_diff($scoreNumberList, [$scoreNumber]);
$score->setScore($scoreNumber);
$this->gameScoreRepository->save($score);
}
}
$winnerList = $tournament->getStageWinnerList($stage);
$this->playOffGameService->scheduleGames($tournament, $winnerList);
}
$tournament = $this->tournamentRepository->save($tournament);
}
return $this->redirectToRoute('tournament_get', ['tournamentId' => $tournament->getId()]);
}
}

0
src/Entity/.gitignore vendored Normal file
View File

177
src/Entity/Division.php Normal file
View File

@ -0,0 +1,177 @@
<?php
namespace App\Entity;
use App\Entity\Game\Game;
use App\Entity\Game\QualifyingGame;
use App\Repository\DivisionRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
#[ORM\Entity(repositoryClass: DivisionRepository::class)]
#[ORM\Table(name: 'division')]
class Division
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?string $id = null;
public function __construct(
#[ORM\Column(type: Types::STRING, length: 255 ,nullable: false)]
private readonly string $title,
#[ORM\ManyToOne(targetEntity: Tournament::class, cascade: ['persist'], inversedBy: 'divisionList')]
#[ORM\JoinColumn(nullable: false)]
private readonly Tournament $tournament,
#[ORM\OneToMany(targetEntity: Player::class, mappedBy: 'division')]
private Collection $playerList = new ArrayCollection(),
/**
* @var Collection<int, Game>
*/
#[ORM\OneToMany(targetEntity: QualifyingGame::class, mappedBy: 'division')]
private Collection $gameList = new ArrayCollection(),
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private readonly DateTimeImmutable $createdAt = new DateTimeImmutable(),
)
{
$this->tournament->addDivision($this);
}
/**
* @return string|null
*/
public function getId(): ?string
{
return $this->id;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @return Tournament
*/
public function getTournament(): Tournament
{
return $this->tournament;
}
/**
* @return DateTimeImmutable
*/
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
/**
* @return Collection<int, Player>
*/
public function getPlayerList(): Collection
{
return $this->playerList;
}
/**
* @param Player $player
* @return Division
*/
public function addPlayer(Player $player): static
{
if (!$this->playerList->contains($player)) {
$this->playerList->add($player);
}
return $this;
}
/**
* @return Collection<int, QualifyingGame>
*/
public function getGameList(): Collection
{
return $this->gameList;
}
/**
* @param QualifyingGame $game
* @return Division
*/
public function addGame(QualifyingGame $game): static
{
if (!$this->gameList->contains($game)) {
$this->gameList->add($game);
}
return $this;
}
/**
* @param Player ...$players
* @return QualifyingGame|null
*/
public function getGameForPlayers(Player ...$players): ?QualifyingGame
{
$playerIdList = [];
foreach ($players as $player) $playerIdList[] = $player->getId();
$gameList = $this->gameList->filter(
function (QualifyingGame $qualifyingGame) use ($playerIdList) {
$scorePlayerIdList = [];
foreach ($qualifyingGame->getScoreList() as $scoreList) {
$scorePlayerIdList[] = $scoreList->getPlayer()->getId();
}
return empty(array_diff($scorePlayerIdList, $playerIdList));
}
);
return $gameList->count() > 0 ? $gameList->first() : null;
}
/**
* @param Player $player
* @return int
*/
public function getPlayerTotalScore(Player $player): int
{
$totalScore = 0;
foreach ($this->gameList as $game) {
foreach ($game->getScoreList() as $score) {
if ($score->getPlayer() == $player) $totalScore += $score->getScore();
}
}
return $totalScore;
}
/**
* @param Player $player
* @return int
*/
public function getPlayerTotalWin(Player $player): int
{
$totalScore = 0;
foreach ($this->gameList as $game) {
foreach ($game->getScoreList() as $score) {
if(is_null($score->getScore())) continue 2;
}
$criteria = new Criteria();
$criteria->orderBy(['score' => Order::Descending])
->setMaxResults(1);
$score = $game->getScoreList()->matching($criteria)->first();
if ($score->getPlayer() === $player) $totalScore += 1;
}
return $totalScore;
}
}

0
src/Entity/Game/.gitignore vendored Normal file
View File

91
src/Entity/Game/Game.php Normal file
View File

@ -0,0 +1,91 @@
<?php
namespace App\Entity\Game;
use App\Entity\Player;
use App\Repository\Game\GameRepository;
use App\Service\Game\PlayerScore;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
#[ORM\Entity(repositoryClass: GameRepository::class)]
#[ORM\Table(name: 'game')]
class Game implements GameInterface
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?string $id = null;
public function __construct(
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private readonly DateTimeImmutable $createdAt = new DateTimeImmutable(),
#[ORM\OneToMany(targetEntity: GameScore::class, mappedBy: 'game', cascade: ['persist'], orphanRemoval: true)]
private Collection $scoreList = new ArrayCollection(),
) {}
public function getId(): ?string
{
return $this->id;
}
/**
* @return DateTimeImmutable
*/
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
/**
* @return Collection<int, GameScore>
*/
public function getScoreList(): Collection
{
return $this->scoreList;
}
/**
* @param GameScore $score
* @return Game
*/
public function addScore(GameScore $score): Game
{
if (!$this->scoreList->contains($score)) {
$this->scoreList->add($score);
}
return $this;
}
/**
* @param GameScore $score
* @return Game
*/
public function removeScore(GameScore $score): Game
{
if ($this->scoreList->contains($score)) {
$this->scoreList->removeElement($score);
}
return $this;
}
/**
* @param Player $player
* @return PlayerScore|null
*/
public function getPlayerScore(Player $player): ?GameScore
{
$gameScore = $this->scoreList->filter(
function (GameScore $playerScore) use ($player) {
return $playerScore->getPlayer() === $player;
}
);
return $gameScore->count() > 0 ? $gameScore->first() : null;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Entity\Game;
use App\Enum\GameStatus;
use Doctrine\Common\Collections\Collection;
use DateTimeImmutable;
interface GameInterface
{
/**
* @return DateTimeImmutable
*/
public function getCreatedAt(): DateTimeImmutable;
/**
* @return Collection<int, GameScore>
*/
public function getScoreList(): Collection;
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Entity\Game;
use App\Entity\Player;
use App\Repository\Game\GameScoreRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
#[ORM\Entity(repositoryClass: GameScoreRepository::class)]
#[ORM\Table(name: 'game_score')]
class GameScore
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?string $id = null;
public function __construct(
#[ORM\ManyToOne(cascade: ['persist'])]
private readonly Game $game,
#[ORM\ManyToOne]
private readonly Player $player,
#[ORM\Column(nullable: true)]
private ?int $score = null,
) {
}
/**
* @return string|null
*/
public function getId(): ?string
{
return $this->id;
}
/**
* @return Game
*/
public function getGame(): Game
{
return $this->game;
}
/**
* @return Player
*/
public function getPlayer(): Player
{
return $this->player;
}
/**
* @return int|null
*/
public function getScore(): ?int
{
return $this->score;
}
/**
* @param int|null $score
* @return GameScore
*/
public function setScore(?int $score): GameScore
{
$this->score = $score;
return $this;
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Entity\Game;
use App\Entity\Player;
use App\Entity\Tournament;
use App\Enum\GameStatus;
use App\Repository\Game\QualifyingGameRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
#[ORM\Entity(repositoryClass: QualifyingGameRepository::class)]
#[ORM\Table(name: 'playoff_game')]
class PlayOffGame implements GameInterface
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?string $id = null;
public function __construct(
#[ORM\ManyToOne(cascade: ['remove'])]
private readonly Tournament $tournament,
#[ORM\Column(nullable: false)]
private readonly int $stage,
#[ORM\OneToOne(cascade: ['remove', 'persist'])]
private readonly Game $game,
) {
$this->tournament->addGame($this);
}
/**
* @return string|null
*/
public function getId(): ?string
{
return $this->id;
}
/**
* @return Tournament
*/
public function getTournament(): Tournament
{
return $this->tournament;
}
/**
* @return int
*/
public function getStage(): int
{
return $this->stage;
}
/**
* @return DateTimeImmutable
*/
public function getCreatedAt(): DateTimeImmutable
{
return $this->game->getCreatedAt();
}
/**
* @return Collection<int, GameScore>
*/
public function getScoreList(): Collection
{
return $this->game->getScoreList();
}
/**
* @param GameScore $score
* @return PlayOffGame
*/
public function addScore(GameScore $score): static
{
if (!$this->game->getScoreList()->contains($score)) {
$this->game->getScoreList()->add($score);
}
return $this;
}
/**
* @param GameScore $score
* @return PlayOffGame
*/
public function removeScore(GameScore $score): PlayOffGame
{
if ($this->game->getScoreList()->contains($score)) {
$this->game->getScoreList()->removeElement($score);
}
return $this;
}
/**
* @param Player $player
* @return GameScore|null
*/
public function getPlayerScore(Player $player): ?GameScore
{
return $this->game->getPlayerScore($player);
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace App\Entity\Game;
use App\Entity\Division;
use App\Entity\Player;
use App\Repository\Game\QualifyingGameRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
#[ORM\Entity(repositoryClass: QualifyingGameRepository::class)]
#[ORM\Table(name: 'qualifying_game')]
class QualifyingGame implements GameInterface
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?string $id = null;
public function __construct(
#[ORM\ManyToOne(cascade: ['remove'])]
private Division $division,
#[ORM\OneToOne(cascade: ['remove', 'persist'])]
private Game $game,
) {
$this->division->addGame($this);
}
/**
* @return string|null
*/
public function getId(): ?string
{
return $this->id;
}
/**
* @return Division
*/
public function getDivision(): Division
{
return $this->division;
}
/**
* @return Game
*/
public function getGame(): Game
{
return $this->game;
}
/**
* @return DateTimeImmutable
*/
public function getCreatedAt(): DateTimeImmutable
{
return $this->game->getCreatedAt();
}
/**
* @return Collection<int, GameScore>
*/
public function getScoreList(): Collection
{
return $this->game->getScoreList();
}
/**
* @param GameScore $score
* @return QualifyingGame
*/
public function addScore(GameScore $score): static
{
if (!$this->game->getScoreList()->contains($score)) {
$this->game->getScoreList()->add($score);
}
return $this;
}
/**
* @param GameScore $score
* @return QualifyingGame
*/
public function removeScore(GameScore $score): QualifyingGame
{
if ($this->game->getScoreList()->contains($score)) {
$this->game->getScoreList()->removeElement($score);
}
return $this;
}
public function getPlayerScore(Player $player): ?GameScore
{
return $this->game->getPlayerScore($player);
}
}

88
src/Entity/Player.php Normal file
View File

@ -0,0 +1,88 @@
<?php
namespace App\Entity;
use App\Repository\PlayerRepository;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
#[ORM\Entity(repositoryClass: PlayerRepository::class)]
#[ORM\Table(name: 'player')]
class Player
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?string $id = null;
public function __construct(
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
private string $title,
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private readonly Tournament $tournament,
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private readonly Division $division,
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private readonly DateTimeImmutable $registerAt = new DateTimeImmutable(),
) {
$this->division->addPlayer($this);
}
/**
* @return string|null
*/
public function getId(): ?string
{
return $this->id;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @param string $title
* @return Player
*/
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
/**
* @return Tournament
*/
public function getTournament(): Tournament
{
return $this->tournament;
}
/**
* @return Division
*/
public function getDivision(): Division
{
return $this->division;
}
/**
* @return DateTimeImmutable
*/
public function getRegisterAt(): DateTimeImmutable
{
return $this->registerAt;
}
}

197
src/Entity/Tournament.php Normal file
View File

@ -0,0 +1,197 @@
<?php
namespace App\Entity;
use App\Entity\Game\Game;
use App\Entity\Game\PlayOffGame;
use App\Enum\TournamentStatus;
use App\Repository\TournamentRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
#[ORM\Entity(repositoryClass: TournamentRepository::class)]
#[ORM\Table(name: 'tournament')]
class Tournament
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?string $id = null;
public function __construct(
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
private string $title,
#[ORM\OneToMany(targetEntity: Division::class, mappedBy: 'tournament')]
private Collection $divisionList = new ArrayCollection(),
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private readonly DateTimeImmutable $createdAt = new DateTimeImmutable(),
/**
* @var Collection<int, PlayOffGame>
*/
#[ORM\OneToMany(targetEntity: PlayOffGame::class, mappedBy: 'tournament')]
private Collection $gameList = new ArrayCollection(),
) {}
/**
* @return string|null
*/
public function getId(): ?string
{
return $this->id;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @return Collection<int, Division>
*/
public function getDivisionList(): Collection
{
return $this->divisionList;
}
/**
* @param Division $division
* @return Tournament
*/
public function addDivision(Division $division): static
{
if (!$this->divisionList->contains($division)) {
$this->divisionList->add($division);
}
return $this;
}
/**
* @param Division $division
* @return Tournament
*/
public function removeDivision(Division $division): static
{
if ($this->divisionList->contains($division)) {
$this->divisionList->removeElement($division);
}
return $this;
}
/**
* @return DateTimeImmutable
*/
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
/**
* @param int $stage
* @return Collection<int, Player>
*/
public function getStageWinnerList(int $stage): Collection
{
$winnerScoreList = new ArrayCollection();
foreach ($this->gameList as $game) {
$winner = null;
if ($game->getStage() != $stage) continue;
foreach ($game->getScoreList() as $score) {
if (empty($winner)) $winner = $score;
$winner = $score->getScore() > $winner->getScore() ? $score : $winner;
}
if (!empty($winner)) $winnerScoreList->add($winner);
}
return $winnerScoreList;
}
/**
* @param int $stage
* @param Player ...$players
* @return PlayOffGame|null
*/
public function getStageGameForPlayers(int $stage, Player ...$players): ?PlayOffGame
{
$playerIdList = [];
foreach ($players as $player) $playerIdList[] = $player->getId();
$gameList = $this->getStageGames($stage)->filter(
function (PlayOffGame $playOffGame) use ($playerIdList) {
$scorePlayerIdList = [];
foreach ($playOffGame->getScoreList() as $scoreList) {
$scorePlayerIdList[] = $scoreList->getPlayer()->getId();
}
return empty(array_diff($scorePlayerIdList, $playerIdList));
}
);
return $gameList->count() > 0 ? $gameList->first() : null;
}
/**
* @param int $stage
* @return bool
*/
public function isStageComplete(int $stage): bool
{
$stageGames = $this->getStageGames($stage);
if ($stageGames->count() < $stage) return false;
$incompleteGames = $stageGames->filter(
function (PlayOffGame $playOffGame) use ($stage) {
if ($playOffGame->getStage() != $stage) return false;
foreach ($playOffGame->getScoreList() as $score) {
return is_null($score->getScore());
}
return false;
}
);
return $incompleteGames->count() <= 0;
}
/**
* @param $stage
* @return Collection<int, PlayOffGame>
*/
public function getStageGames($stage): Collection
{
return $this->gameList->filter(
function (PlayOffGame $playOffGame) use ($stage) {
return $playOffGame->getStage() === $stage;
}
);
}
/**
* @return array
*/
public function getPlayOffStageList(): array
{
$stageGameList = [];
if ($this->gameList->count() <= 0) return $stageGameList;
foreach ($this->gameList as $playOffGame) {
$stageGameList[$playOffGame->getStage()][] = $playOffGame;
}
return $stageGameList;
}
/**
* @param PlayOffGame $game
* @return $this
*/
public function addGame(PlayOffGame $game): static
{
if (!$this->gameList->contains($game)) {
$this->gameList->add($game);
}
return $this;
}
}

11
src/Enum/GameStatus.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Enum;
enum GameStatus: string
{
case NEW = 'new';
case ACTIVE = 'active';
case ENDED = 'ended';
}

9
src/Enum/GameType.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace App\Enum;
enum GameType: string
{
case TOURNAMENT = 'tournament';
case DIVISION = 'division';
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Enum;
enum TournamentStatus: string
{
case new = 'new';
case division = 'division';
case tournament = 'tournament';
case ended = 'ended';
}

11
src/Kernel.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

0
src/Repository/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,30 @@
<?php
namespace App\Repository;
use App\Entity\Division;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Division>
*
* @method Division|null find($id, $lockMode = null, $lockVersion = null)
* @method Division|null findOneBy(array $criteria, array $orderBy = null)
* @method Division[] findAll()
* @method Division[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class DivisionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Division::class);
}
public function save(Division $divisionEntity): Division
{
$this->getEntityManager()->persist($divisionEntity);
$this->getEntityManager()->flush();
return $divisionEntity;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Repository\Game;
use App\Entity\Game\Game;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Game>
*
* @method Game|null find($id, $lockMode = null, $lockVersion = null)
* @method Game|null findOneBy(array $criteria, array $orderBy = null)
* @method Game[] findAll()
* @method Game[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class GameRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Game::class);
}
public function save(Game $game): Game
{
$this->getEntityManager()->persist($game);
$this->getEntityManager()->flush();
return $game;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Repository\Game;
use App\Entity\Game\GameScore;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<GameScore>
*
* @method GameScore|null find($id, $lockMode = null, $lockVersion = null)
* @method GameScore|null findOneBy(array $criteria, array $orderBy = null)
* @method GameScore[] findAll()
* @method GameScore[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class GameScoreRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, GameScore::class);
}
public function save(GameScore $gameScore): GameScore
{
$this->getEntityManager()->persist($gameScore);
$this->getEntityManager()->flush();
return $gameScore;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Repository\Game;
use App\Entity\Game\PlayOffGame;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<PlayOffGame>
*
* @method PlayOffGame|null find($id, $lockMode = null, $lockVersion = null)
* @method PlayOffGame|null findOneBy(array $criteria, array $orderBy = null)
* @method PlayOffGame[] findAll()
* @method PlayOffGame[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PlayOffGameRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PlayOffGame::class);
}
public function save(PlayOffGame $playOffGame): PlayOffGame
{
$this->getEntityManager()->persist($playOffGame);
$this->getEntityManager()->flush();
return $playOffGame;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Repository\Game;
use App\Entity\Game\QualifyingGame;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<QualifyingGame>
*
* @method QualifyingGame|null find($id, $lockMode = null, $lockVersion = null)
* @method QualifyingGame|null findOneBy(array $criteria, array $orderBy = null)
* @method QualifyingGame[] findAll()
* @method QualifyingGame[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class QualifyingGameRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, QualifyingGame::class);
}
public function save(QualifyingGame $qualifyingGame): QualifyingGame
{
$this->getEntityManager()->persist($qualifyingGame);
$this->getEntityManager()->flush();
return $qualifyingGame;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Repository;
use App\Entity\Player;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Player>
*
* @method Player|null find($id, $lockMode = null, $lockVersion = null)
* @method Player|null findOneBy(array $criteria, array $orderBy = null)
* @method Player[] findAll()
* @method Player[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PlayerRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Player::class);
}
public function save(Player $playerEntity): Player
{
$this->getEntityManager()->persist($playerEntity);
$this->getEntityManager()->flush();
return $playerEntity;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Repository;
use App\Entity\Tournament;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Tournament>
*
* @method Tournament|null find($id, $lockMode = null, $lockVersion = null)
* @method Tournament|null findOneBy(array $criteria, array $orderBy = null)
* @method Tournament[] findAll()
* @method Tournament[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TournamentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Tournament::class);
}
public function save(Tournament $tournamentEntity): Tournament
{
$this->getEntityManager()->persist($tournamentEntity);
$this->getEntityManager()->flush();
return $tournamentEntity;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Service\Game;
use App\Entity\Player;
readonly class PlayerScore
{
public function __construct(
private Player $player,
private int $totalScore = 0,
private int $totalWin = 0,
) {}
/**
* @return Player
*/
public function getPlayer(): Player
{
return $this->player;
}
/**
* @return int
*/
public function getTotalScore(): int
{
return $this->totalScore;
}
/**
* @return int
*/
public function getTotalWin(): int
{
return $this->totalWin;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Service\Game;
use Doctrine\Common\Collections\ArrayCollection;
class PlayerScoreTable extends ArrayCollection
{
/**
* @param PlayerScore $playerScore
* @return PlayerScoreTable
*/
public function addPlayerScore(PlayerScore $playerScore): static
{
if (!$this->contains($playerScore)) {
$this->add($playerScore);
}
return $this;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Service;
use App\Entity\Game\Game;
use App\Entity\Game\GameScore;
use App\Entity\Player;
use App\Repository\Game\GameRepository;
readonly class GameService
{
public function __construct(
private GameRepository $gameRepository
) {}
/**
* @param Player ...$players
* @return Game
*/
public function scheduleGame(Player ...$players): Game
{
$game = new Game();
foreach ($players as $key => $player) {
foreach ($players as $opponent) {
if ($player === $opponent) continue;
$playerScore = new GameScore($game, $player);
$opponentScore = new GameScore($game, $opponent);
$game->addScore($playerScore)->addScore($opponentScore);
}
unset($players[$key]);
if (count($players) <= 1) break;
}
return $this->gameRepository->save($game);
}
public function addPlayerScore(Game $game, Player $player, int $score): void
{
$game->getPlayerScore($player)->setScore($score);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Service;
use App\Entity\Game\PlayOffGame;
use App\Entity\Tournament;
use App\Repository\Game\PlayOffGameRepository;
use Doctrine\Common\Collections\Collection;
readonly class PlayOffGameService
{
/**
* @param PlayOffGameRepository $playOffGameRepository
* @param GameService $gameService
*/
public function __construct(
private PlayOffGameRepository $playOffGameRepository,
private GameService $gameService
) {}
/**
* @param Tournament $tournament
* @param Collection $playerScoreTable
* @return void
*/
public function scheduleGames(Tournament $tournament, Collection $playerScoreTable): void
{
$stage = $playerScoreTable->count() / 2;
if ($stage <= 0) return;
if ($tournament->isStageComplete($stage)) {
$winnerList = $tournament->getStageWinnerList($stage);
$this->scheduleGames($tournament, $winnerList);
return;
}
$playerScoreTable = $playerScoreTable->toArray();
while (count($playerScoreTable) > 0) {
$playerScore = array_shift($playerScoreTable);
$opponentScore = array_pop($playerScoreTable);
if (is_null($tournament->getStageGameForPlayers($stage, $playerScore->getPlayer(), $opponentScore->getPlayer()))) {
$game = $this->gameService->scheduleGame($playerScore->getPlayer(), $opponentScore->getPlayer());
$playOffGame = new PlayOffGame($tournament, $stage, $game);
$this->playOffGameRepository->save($playOffGame);
}
}
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace App\Service;
use App\Entity\Division;
use App\Entity\Game\GameScore;
use App\Entity\Game\QualifyingGame;
use App\Entity\Tournament;
use App\Repository\Game\QualifyingGameRepository;
use App\Service\Game\PlayerScore;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
readonly class QualifyingGameService
{
const WINNER_COUNT = 4;
const WINNER_SORTING = [
'totalWin' => Order::Descending,
'totalScore' => Order::Descending,
];
/**
* @param QualifyingGameRepository $qualifyingGameRepository
* @param GameService $gameService
*/
public function __construct(
private QualifyingGameRepository $qualifyingGameRepository,
private GameService $gameService
) {}
/**
* @param Division $division
* @return void
*/
public function scheduleGames(Division $division): void
{
$players = clone $division->getPlayerList();
if ($players->count() < self::WINNER_COUNT + 1) return;
foreach ($players as $player) {
foreach ($players as $opponent) {
if ($division->getGameForPlayers($player, $opponent)) continue;
if ($player === $opponent) continue;
$game = $this->gameService->scheduleGame($player, $opponent);
$qualifyingGame = new QualifyingGame($division, $game);
$this->qualifyingGameRepository->save($qualifyingGame);
}
$players->removeElement($player);
}
}
/**
* @param Division $division
* @return bool
*/
public function isQualificationGamesComplete(Division $division): bool
{
$games = $division->getGameList();
if ($games->count() <= 0) return false;
foreach ($games as $game) {
$incompleteGames = $game->getScoreList()->filter(
function (GameScore $gameScore) {
return is_null($gameScore->getScore());
}
);
if ($incompleteGames->count() > 0) return false;
}
return true;
}
/**
* @param Division $division
* @return Collection<int, PlayerScore>
*/
public function getDivisionQualificationGameWinners(Division $division): Collection
{
$playerList = $division->getPlayerList();
$playerScoreTable = new ArrayCollection();
foreach ($playerList as $player) {
$winner = new PlayerScore(player: $player, totalScore: $division->getPlayerTotalScore($player), totalWin: $division->getPlayerTotalWin($player));
if (!$playerScoreTable->contains($winner)) $playerScoreTable->add($winner);
}
$criteria = new Criteria();
$criteria->orderBy(self::WINNER_SORTING)
->setMaxResults(self::WINNER_COUNT);
return $playerScoreTable->matching($criteria);
}
/**
* @param Tournament $tournament
* @return Collection<int, PlayerScore>
*/
public function getQualificationGameWinners(Tournament $tournament): Collection
{
$qualifyingWinners = [];
foreach ($tournament->getDivisionList() as $division) {
$qualifyingWinners = array_merge($qualifyingWinners, $this->getDivisionQualificationGameWinners($division)->toArray());
}
$qualifyingWinners = new ArrayCollection($qualifyingWinners);
$criteria = new Criteria();
$criteria->orderBy(self::WINNER_SORTING);
return $qualifyingWinners->matching($criteria);
}
}

185
symfony.lock Normal file
View File

@ -0,0 +1,185 @@
{
"doctrine/doctrine-bundle": {
"version": "2.11",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.10",
"ref": "c170ded8fc587d6bd670550c43dafcf093762245"
},
"files": [
"config/packages/doctrine.yaml",
"src/Entity/.gitignore",
"src/Repository/.gitignore"
]
},
"doctrine/doctrine-migrations-bundle": {
"version": "3.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.1",
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
},
"files": [
"config/packages/doctrine_migrations.yaml",
"migrations/.gitignore"
]
},
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "9.6",
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
},
"files": [
".env.test",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/console": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "da0c8be8157600ad34f10ff0c9cc91232522e047"
},
"files": [
"bin/console"
]
},
"symfony/flex": {
"version": "2.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
},
"files": [
".env"
]
},
"symfony/framework-bundle": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "6356c19b9ae08e7763e4ba2d9ae63043efc75db5"
},
"files": [
"config/packages/cache.yaml",
"config/packages/framework.yaml",
"config/preload.php",
"config/routes/framework.yaml",
"config/services.yaml",
"public/index.php",
"src/Controller/.gitignore",
"src/Kernel.php"
]
},
"symfony/maker-bundle": {
"version": "1.56",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/monolog-bundle": {
"version": "3.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.7",
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
},
"files": [
"config/packages/monolog.yaml"
]
},
"symfony/phpunit-bridge": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
},
"files": [
".env.test",
"bin/phpunit",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/routing": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d"
},
"files": [
"config/packages/routing.yaml",
"config/routes.yaml"
]
},
"symfony/translation": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b"
},
"files": [
"config/packages/translation.yaml",
"translations/.gitignore"
]
},
"symfony/twig-bundle": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
},
"files": [
"config/packages/twig.yaml",
"templates/base.html.twig"
]
},
"symfony/uid": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
}
},
"symfony/validator": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
},
"files": [
"config/packages/validator.yaml"
]
},
"twig/extra-bundle": {
"version": "v3.8.0"
}
}

30
templates/base.html.twig Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="" >
<head>
<meta charset="UTF-8">
<title>{% block title %}Tournament{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
{% block stylesheets %}
{% endblock %}
</head>
<body >
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">Tournament</a>
</div>
</nav>
{% block body %}{% endblock %}
</body>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
{% block javascripts %}
{% endblock %}
</html>

21
templates/index.html.twig Normal file
View File

@ -0,0 +1,21 @@
{% extends "base.html.twig" %}
{% block body %}
<div class="container">
<div class="text-center mt-5">
<form action="/tournament" method="post">
<fieldset>
<legend>Create tournament</legend>
<div class="mb-3">
<label for="title" class="form-label">Tournament title</label>
<input type="text" id="title" name="title" class="form-control" placeholder="Enter your tournament title">
</div>
<button type="submit" class="btn btn-primary">Generate tournament</button>
</fieldset>
</form>
</div>
<div class="text-center mt-5">
tournament list
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,73 @@
{% extends "base.html.twig" %}
{% block body %}
<div class="container">
<div class="text-center mt-5">
<h1>Tournament {{ tournament.getTitle() }}</h1>
</div>
{% for division in tournament.getDivisionList() %}
<div class="text-center mt-5">
<h2><span>{{ division.getTitle() }}</span> division qualification </h2>
{% set divisionPlayers = division.getPlayerList() %}
<table class="table table-striped">
<tr>
<th>Team</th>
{% for opponent in divisionPlayers %}
<th>{{ opponent.getTitle() }}</th>
{% endfor %}
<th>Win count</th>
<th>Total score</th>
</tr>
{% for player in divisionPlayers %}
<tr>
<th>{{ player.getTitle() }}</th>
{% for opponent in divisionPlayers %}
{% if player == opponent %}
<td class="dark-light">-</td>
{% else %}
<td>
{% set playersGame = division.getGameForPlayers(player,opponent) %}
{% if playersGame is not null %}
{{ playersGame.getPlayerScore(player).getScore() }}
: {{ playersGame.getPlayerScore(opponent).getScore() }}
{% endif %}
</td>
{% endif %}
{% endfor %}
<th>{{ division.getPlayerTotalWin(player) }}</th>
<th>{{ division.getPlayerTotalScore(player) }}</th>
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
<div class="text-center mt-5">
<h1>Play off</h1>
{% for stage, stageGameList in tournament.getPlayOffStageList() %}
<h3>1 / {{ stage }}</h3>
{% for index, playOffStageGame in stageGameList %}
<table class="table">
<thead>
<tr>
<th>Player</th>
<th>Score</th>
</tr>
</thead>
{% for palyerScore in playOffStageGame.getScoreList() %}
<tr>
<td>{{ palyerScore.getPlayer().getTitle() }}</td>
<td>{{ palyerScore.getScore() }}</td>
</tr>
{% endfor %}
</table>
{% endfor %}
{% endfor %}
</div>
<div class="text-center mt-5">
<h1>Winner</h1>
{% set winner = tournament.getStageWinnerList(1) %}
<h2>{{ winner.first().getPlayer().getTitle() }}</h2>
</div>
</div>
{% endblock %}

11
tests/bootstrap.php Normal file
View File

@ -0,0 +1,11 @@
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
if (file_exists(dirname(__DIR__).'/config/bootstrap.php')) {
require dirname(__DIR__).'/config/bootstrap.php';
} elseif (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}

0
translations/.gitignore vendored Normal file
View File