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
3 changes: 2 additions & 1 deletion src/Controller/BgaDeckController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

class BgaDeckController extends AbstractController
{
private const BGA_VALID_FORMATS = ['standard', 'nuc', 'singleton_nuc', 'sandbox'];
private const BGA_VALID_FORMATS = ['standard', 'nuc', 'singleton_nuc', 'sandbox', 'test'];

public function __construct(
private readonly DeckRepository $deckRepository,
Expand All @@ -43,6 +43,7 @@ public function collection(Request $request): JsonResponse
'SANDBOX' => 'sandbox',
'SINGLETON' => 'singleton',
'SINGLETON_NUC' => 'singleton_nuc',
'TEST' => 'test',
default => '',
};

Expand Down
31 changes: 28 additions & 3 deletions src/Controller/FormatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

class FormatController extends AbstractController
{
#[Route('/api/formats', name: 'api_formats', methods: ['GET'])]
public function __invoke(): JsonResponse
public function __invoke(Request $request): JsonResponse
{
return $this->json([
$includeHidden = $request->query->getBoolean('hiddenFormats');

$formats = [
[
'code' => 'sandbox',
'label' => 'Sandbox',
Expand All @@ -27,6 +30,22 @@ public function __invoke(): JsonResponse
'maxCopiesPerRarity' => null,
],
],
[
'code' => 'test',
'label' => 'Test',
'hidden' => true,
'minCards' => 4,
'maxCards' => 100,
'allowBanned' => true,
'allowSuspended' => true,
'limits' => [
'unique' => null,
'rare' => null,
'exalted' => null,
'maxCopiesPerName' => null,
'maxCopiesPerRarity' => null,
],
],
[
'code' => 'nuc',
'label' => 'NUC',
Expand Down Expand Up @@ -92,6 +111,12 @@ public function __invoke(): JsonResponse
'maxCopiesPerRarity' => 1,
],
],
]);
];

if (!$includeHidden) {
$formats = array_values(array_filter($formats, fn (array $format) => empty($format['hidden'])));
}

return $this->json($formats);
}
}
2 changes: 2 additions & 0 deletions src/Enum/DeckFormat.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum DeckFormat: string
case Singleton = 'singleton';
case SingletonNuc = 'singleton_nuc';
case Sandbox = 'sandbox';
case Test = 'test';

public static function fromInput(string $value): self
{
Expand All @@ -18,6 +19,7 @@ public static function fromInput(string $value): self
'singleton' => self::Singleton,
'singleton_nuc', 'singleton_no_unique' => self::SingletonNuc,
'sandbox' => self::Sandbox,
'test' => self::Test,
default => throw new \ValueError(sprintf('"%s" is not a valid DeckFormat.', $value)),
};
}
Expand Down
29 changes: 29 additions & 0 deletions src/Validator/Format/TestFormatValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace App\Validator\Format;

/**
* Test format rules:
* Same as Sandbox but without set restrictions on sets 6 and 7 (FUGUE, EOLE):
* - 4 to 100 cards (excluding hero) + 1 hero
* - No faction restriction
* - Suspended and banned cards allowed
* - Forbidden sets (FUGUE, EOLE) are allowed
*/
final class TestFormatValidator extends SandboxFormatValidator
{
public function getFormat(): string
{
return 'test';
}

protected function getAllowedSets(): array
{
return [...self::VALID_SETS, ...self::FORBIDDEN_SETS];
}

protected function getForbiddenSets(): array
{
return [];
}
}
27 changes: 27 additions & 0 deletions tests/Controller/FormatControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@ public function testFormatsEndpointReturnsAllFormats(): void
self::assertContains('singleton_nuc', $codes);
}

public function testHiddenFormatsAreNotReturnedByDefault(): void
{
$client = static::createClient();
$client->request('GET', '/api/formats');

self::assertResponseIsSuccessful();

$data = json_decode($client->getResponse()->getContent(), true);
$codes = array_column($data, 'code');

self::assertNotContains('test', $codes);
}

public function testHiddenFormatsAreReturnedWhenRequested(): void
{
$client = static::createClient();
$client->request('GET', '/api/formats?hiddenFormats=true');

self::assertResponseIsSuccessful();

$data = json_decode($client->getResponse()->getContent(), true);
$codes = array_column($data, 'code');

self::assertContains('test', $codes);
self::assertContains('sandbox', $codes);
}

public function testSandboxAllowsBannedAndSuspendedCards(): void
{
$client = static::createClient();
Expand Down
163 changes: 163 additions & 0 deletions tests/Validator/Format/TestFormatValidatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

namespace App\Tests\Validator\Format;

use App\Entity\Deck;
use App\Entity\DeckCard;
use App\Validator\Format\TestFormatValidator;
use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase;

class TestFormatValidatorTest extends TestCase
{
private TestFormatValidator $validator;

protected function setUp(): void
{
$this->validator = new TestFormatValidator();
}

// ── Helpers ───────────────────────────────────────────────────────────────

private function card(string $ref, int $qty = 1): DeckCard
{
$card = new DeckCard();
$card->setCardReference($ref);
$card->setQuantity($qty);

return $card;
}

private function deck(DeckCard ...$cards): Deck
{
$deck = $this->createStub(Deck::class);
$deck->method('getDeckCards')->willReturn(new ArrayCollection($cards));

return $deck;
}

private function data(
string $ref,
string $typeRef = 'PERMANENT',
string $faction = 'AX',
string $rarityRef = 'COMMON',
string $name = '',
): array {
return [
'reference' => $ref,
'cardType' => ['reference' => $typeRef],
'faction' => ['code' => $faction],
'rarity' => ['reference' => $rarityRef],
'name' => $name ?: $ref,
];
}

/**
* @return array{0: array<string, array>, 1: DeckCard[]}
*/
private function buildMinimalValidDeck(string $faction = 'AX'): array
{
$cardsData = [];
$deckCards = [];

$heroRef = 'ALT_CORE_B_AX_0_C';
$deckCards[] = $this->card($heroRef);
$cardsData[$heroRef] = $this->data($heroRef, 'HERO_MAIN', $faction);

for ($i = 1; $i <= 4; ++$i) {
$ref = sprintf('ALT_CORE_B_%s_%d_C', $faction, $i);
$deckCards[] = $this->card($ref);
$cardsData[$ref] = $this->data($ref, 'PERMANENT', $faction);
}

return [$cardsData, $deckCards];
}

public function testGetFormatIsTest(): void
{
self::assertSame('test', $this->validator->getFormat());
}

public function testValidDeckHasNoErrors(): void
{
[$cardsData, $deckCards] = $this->buildMinimalValidDeck();

$errors = $this->validator->validate($this->deck(...$deckCards), $cardsData);

self::assertSame([], $errors);
}

// ── Sets 6 and 7 (FUGUE, EOLE) are allowed ────────────────────────────────

public function testFugueSetIsAllowed(): void
{
[$cardsData, $deckCards] = $this->buildMinimalValidDeck();

$ref = 'ALT_FUGUE_B_AX_1_C';
$deckCards[] = $this->card($ref);
$cardsData[$ref] = $this->data($ref, 'PERMANENT', 'AX', 'COMMON', 'Fugue Card');

$errors = $this->validator->validate($this->deck(...$deckCards), $cardsData);

self::assertEmpty(array_filter($errors, fn ($e) => str_contains($e, 'FUGUE')));
}

public function testEoleSetIsAllowed(): void
{
[$cardsData, $deckCards] = $this->buildMinimalValidDeck();

$ref = 'ALT_EOLE_B_AX_1_C';
$deckCards[] = $this->card($ref);
$cardsData[$ref] = $this->data($ref, 'PERMANENT', 'AX', 'COMMON', 'Eole Card');

$errors = $this->validator->validate($this->deck(...$deckCards), $cardsData);

self::assertEmpty(array_filter($errors, fn ($e) => str_contains($e, 'EOLE')));
}

public function testLegalityDetailReportsSetsAsLegalForForbiddenSets(): void
{
[$cardsData, $deckCards] = $this->buildMinimalValidDeck();

$ref = 'ALT_FUGUE_B_AX_1_C';
$deckCards[] = $this->card($ref);
$cardsData[$ref] = $this->data($ref, 'PERMANENT', 'AX', 'COMMON', 'Fugue Card');

$detail = $this->validator->computeLegalityDetail($this->deck(...$deckCards), $cardsData);

self::assertTrue($detail['sets']);
}

// ── Same permissive rules as Sandbox ──────────────────────────────────────

public function testBannedAndSuspendedCardsAreAllowed(): void
{
[$cardsData, $deckCards] = $this->buildMinimalValidDeck();

$ref = 'ALT_CORE_B_AX_1_C';
$cardsData[$ref] = array_merge($cardsData[$ref], ['isBanned' => true, 'isSuspended' => true]);

$errors = $this->validator->validate($this->deck(...$deckCards), $cardsData);

self::assertEmpty(array_filter($errors, fn ($e) => str_contains($e, 'banned') || str_contains($e, 'suspended')));
}

public function testMultipleFactionsDeckHasNoErrors(): void
{
$heroRef = 'ALT_CORE_B_AX_0_C';
$cardsData = [
$heroRef => $this->data($heroRef, 'HERO_MAIN', 'AX'),
];
$deckCards = [$this->card($heroRef)];

foreach (['AX', 'LY', 'MU', 'OR', 'YZ'] as $i => $faction) {
$ref = sprintf('ALT_CORE_B_%s_%d_C', $faction, $i + 1);
$deckCards[] = $this->card($ref);
$cardsData[$ref] = $this->data($ref, 'PERMANENT', $faction);
}

$errors = $this->validator->validate($this->deck(...$deckCards), $cardsData);

self::assertSame([], $errors);
}
}
Loading