From 06f77e8341a5c45f36b4568920623747e81a10a5 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Thu, 19 Mar 2026 14:14:56 -0300 Subject: [PATCH 1/4] chore (DiceModifier): Change DiceModifier to RollModifier --- .../UseCase/Dice/RollDice/RollDiceInput.php | 6 ++--- .../UseCase/Dice/RollDice/RollDiceUseCase.php | 3 +-- src/Core/Exceptions/DomainException.php | 9 ------- .../{DiceModifier.php => RollModifier.php} | 2 +- .../Dice/RollDice/RollDiceInputTest.php | 6 ++--- .../Dice/RollDice/RollDiceUseCaseTest.php | 16 ++++++------- ...eModifierTest.php => RollModifierTest.php} | 24 +++++++++---------- 7 files changed, 28 insertions(+), 38 deletions(-) delete mode 100644 src/Core/Exceptions/DomainException.php rename src/Domain/ValueObjects/{DiceModifier.php => RollModifier.php} (98%) rename tests/Domain/ValueObjects/{DiceModifierTest.php => RollModifierTest.php} (83%) diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceInput.php b/src/Application/UseCase/Dice/RollDice/RollDiceInput.php index 3b97804..2576813 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceInput.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceInput.php @@ -7,13 +7,13 @@ use Eco\Error; use Eco\Result; use RPGPlayground\Domain\ValueObjects\Dice; -use RPGPlayground\Domain\ValueObjects\DiceModifier; +use RPGPlayground\Domain\ValueObjects\RollModifier; final class RollDiceInput { /** * @param Dice $dice The dice to roll - * @param array $modifiers The modifiers to apply to the roll + * @param array $modifiers The modifiers to apply to the roll * @param int $multiplier The number of times to roll the dice * @throws \InvalidArgumentException */ @@ -25,7 +25,7 @@ private function __construct( /** * @param Dice $dice The dice to roll - * @param array $modifiers The modifiers to apply to the roll + * @param array $modifiers The modifiers to apply to the roll * @param int $multiplier The number of times to roll the dice * @return Result * */ diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php index 057eb10..3a66330 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php @@ -4,7 +4,6 @@ namespace RPGPlayground\Application\UseCase\Dice\RollDice; -use Eco\Error; use Eco\Result; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceInput; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceOutput; @@ -16,7 +15,7 @@ final class RollDiceUseCase * @param RollDiceInput $input * @return Result * @throws \Random\RandomException if the system entropy source fails. - * @throws \LogicException if an invalid symbol somehow bypasses DiceModifier::fromString. + * @throws \LogicException if an invalid symbol somehow bypasses RollModifier::fromString. */ public static function handle(RollDiceInput $input): Result { diff --git a/src/Core/Exceptions/DomainException.php b/src/Core/Exceptions/DomainException.php deleted file mode 100644 index d96c205..0000000 --- a/src/Core/Exceptions/DomainException.php +++ /dev/null @@ -1,9 +0,0 @@ -unwrap(); diff --git a/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php b/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php index c9d4842..e1dd837 100644 --- a/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php +++ b/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php @@ -9,7 +9,7 @@ use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceOutput; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceUseCase; use RPGPlayground\Domain\ValueObjects\Dice; -use RPGPlayground\Domain\ValueObjects\DiceModifier; +use RPGPlayground\Domain\ValueObjects\RollModifier; final class RollDiceUseCaseTest extends TestCase { @@ -75,7 +75,7 @@ public function test_single_multiplier_matches_d1_roll(): void public function test_addition_modifier_is_applied(): void { // D1 always rolls 1 → +4 = 5 - $input = $this->makeInput(new Dice(1), modifiers: [DiceModifier::fromString('+4')]); + $input = $this->makeInput(new Dice(1), modifiers: [RollModifier::fromString('+4')]); $output = RollDiceUseCase::handle($input)->unwrap(); $this->assertSame(5, $output->rollValue); @@ -84,7 +84,7 @@ public function test_addition_modifier_is_applied(): void public function test_subtraction_modifier_is_applied(): void { // D1 always rolls 1 → -1 = 0 - $input = $this->makeInput(new Dice(1), modifiers: [DiceModifier::fromString('-1')]); + $input = $this->makeInput(new Dice(1), modifiers: [RollModifier::fromString('-1')]); $output = RollDiceUseCase::handle($input)->unwrap(); $this->assertSame(0, $output->rollValue); @@ -93,7 +93,7 @@ public function test_subtraction_modifier_is_applied(): void public function test_multiplication_modifier_is_applied(): void { // D1 always rolls 1 → x3 = 3 - $input = $this->makeInput(new Dice(1), modifiers: [DiceModifier::fromString('x3')]); + $input = $this->makeInput(new Dice(1), modifiers: [RollModifier::fromString('x3')]); $output = RollDiceUseCase::handle($input)->unwrap(); $this->assertSame(3, $output->rollValue); @@ -102,7 +102,7 @@ public function test_multiplication_modifier_is_applied(): void public function test_division_modifier_is_applied(): void { // 3x D1 = 3 → /3 = 1 - $input = $this->makeInput(new Dice(1), modifiers: [DiceModifier::fromString('/3')], multiplier: 3); + $input = $this->makeInput(new Dice(1), modifiers: [RollModifier::fromString('/3')], multiplier: 3); $output = RollDiceUseCase::handle($input)->unwrap(); $this->assertSame(1, $output->rollValue); @@ -112,8 +112,8 @@ public function test_multiple_modifiers_are_applied_in_order(): void { // D1 = 1 → +9 = 10 → /2 = 5 $input = $this->makeInput(new Dice(1), modifiers: [ - DiceModifier::fromString('+9'), - DiceModifier::fromString('/2'), + RollModifier::fromString('+9'), + RollModifier::fromString('/2'), ]); $output = RollDiceUseCase::handle($input)->unwrap(); @@ -136,7 +136,7 @@ public function test_no_modifiers_returns_raw_roll(): void /** * @param Dice $dice - * @param array $modifiers + * @param array $modifiers * @param int $multiplier * @return RollDiceInput */ diff --git a/tests/Domain/ValueObjects/DiceModifierTest.php b/tests/Domain/ValueObjects/RollModifierTest.php similarity index 83% rename from tests/Domain/ValueObjects/DiceModifierTest.php rename to tests/Domain/ValueObjects/RollModifierTest.php index 5ef5997..effef68 100644 --- a/tests/Domain/ValueObjects/DiceModifierTest.php +++ b/tests/Domain/ValueObjects/RollModifierTest.php @@ -5,9 +5,9 @@ namespace RPGPlayground\Tests\Domain\ValueObjects; use PHPUnit\Framework\TestCase; -use RPGPlayground\Domain\ValueObjects\DiceModifier; +use RPGPlayground\Domain\ValueObjects\RollModifier; -final class DiceModifierTest extends TestCase +final class RollModifierTest extends TestCase { // ------------------------------------------------------------------------- // fromString — happy path @@ -19,7 +19,7 @@ public function test_from_string_parses_symbol_and_value( string $expectedSymbol, int $expectedValue, ): void { - $dm = DiceModifier::fromString($modifier); + $dm = RollModifier::fromString($modifier); $this->assertSame($expectedSymbol, $dm->symbol); $this->assertSame($expectedValue, $dm->value); @@ -49,7 +49,7 @@ public function test_from_string_throws_on_invalid_symbol(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid modifier symbol: o'); - DiceModifier::fromString('o2'); + RollModifier::fromString('o2'); } #[\PHPUnit\Framework\Attributes\DataProvider('invalidSymbolProvider')] @@ -57,7 +57,7 @@ public function test_from_string_throws_for_unknown_symbols(string $modifier): v { $this->expectException(\InvalidArgumentException::class); - DiceModifier::fromString($modifier); + RollModifier::fromString($modifier); } /** @@ -80,35 +80,35 @@ public static function invalidSymbolProvider(): array public function test_apply_addition(): void { - $result = DiceModifier::fromString('+5')->apply(10); + $result = RollModifier::fromString('+5')->apply(10); $this->assertSame(15, $result); } public function test_apply_subtraction(): void { - $result = DiceModifier::fromString('-3')->apply(10); + $result = RollModifier::fromString('-3')->apply(10); $this->assertSame(7, $result); } public function test_apply_multiplication_star(): void { - $result = DiceModifier::fromString('*2')->apply(10); + $result = RollModifier::fromString('*2')->apply(10); $this->assertSame(20, $result); } public function test_apply_multiplication_x(): void { - $result = DiceModifier::fromString('x2')->apply(10); + $result = RollModifier::fromString('x2')->apply(10); $this->assertSame(20, $result); } public function test_apply_division_slash(): void { - $result = DiceModifier::fromString('/4')->apply(20); + $result = RollModifier::fromString('/4')->apply(20); $this->assertSame(5, $result); } @@ -120,14 +120,14 @@ public function test_apply_division_slash(): void public function test_apply_division_rounds_up_with_ceil(): void { // 10 / 3 = 3.33... → ceil → 4 - $result = DiceModifier::fromString('/3')->apply(10); + $result = RollModifier::fromString('/3')->apply(10); $this->assertSame(4, $result); } public function test_apply_subtraction_can_go_negative(): void { - $result = DiceModifier::fromString('-15')->apply(10); + $result = RollModifier::fromString('-15')->apply(10); $this->assertSame(-5, $result); } From dfde56753da706d36d15ed29197364af10754390 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Thu, 19 Mar 2026 15:14:50 -0300 Subject: [PATCH 2/4] feat (RollDice): Add advantage and disadvantage attributes --- .../UseCase/Dice/RollDice/RollDiceInput.php | 15 +++- .../UseCase/Dice/RollDice/RollDiceUseCase.php | 32 ++++++- src/Domain/Enums/Roll/RollAttribute.php | 21 +++++ .../ValueObjects/{ => Roll}/RollModifier.php | 2 +- .../Dice/RollDice/RollDiceInputTest.php | 2 +- .../Dice/RollDice/RollDiceUseCaseTest.php | 90 +++++++++++++++++-- tests/Domain/Enums/Roll/RollAttributeTest.php | 23 +++++ .../{ => Roll}/RollModifierTest.php | 4 +- 8 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 src/Domain/Enums/Roll/RollAttribute.php rename src/Domain/ValueObjects/{ => Roll}/RollModifier.php (97%) create mode 100644 tests/Domain/Enums/Roll/RollAttributeTest.php rename tests/Domain/ValueObjects/{ => Roll}/RollModifierTest.php (97%) diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceInput.php b/src/Application/UseCase/Dice/RollDice/RollDiceInput.php index 2576813..134c59b 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceInput.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceInput.php @@ -6,8 +6,9 @@ use Eco\Error; use Eco\Result; +use RPGPlayground\Domain\Enums\Roll\RollAttribute; use RPGPlayground\Domain\ValueObjects\Dice; -use RPGPlayground\Domain\ValueObjects\RollModifier; +use RPGPlayground\Domain\ValueObjects\Roll\RollModifier; final class RollDiceInput { @@ -15,12 +16,14 @@ final class RollDiceInput * @param Dice $dice The dice to roll * @param array $modifiers The modifiers to apply to the roll * @param int $multiplier The number of times to roll the dice + * @param RollAttribute $attribute The attributes to apply to the roll * @throws \InvalidArgumentException */ private function __construct( public readonly Dice $dice, public readonly array $modifiers, public readonly int $multiplier = 1, + public readonly ?RollAttribute $attribute = null, ) {} /** @@ -29,14 +32,18 @@ private function __construct( * @param int $multiplier The number of times to roll the dice * @return Result * */ - public static function create(Dice $dice, array $modifiers = [], int $multiplier = 1): Result - { + public static function create( + Dice $dice, + array $modifiers = [], + int $multiplier = 1, + ?RollAttribute $attribute = null, + ): Result { try { if ($multiplier < 1) { throw new \InvalidArgumentException('Multiplier must be greater than or equal to 1.'); } - return Result::ok(new self($dice, $modifiers, $multiplier)); + return Result::ok(new self($dice, $modifiers, $multiplier, $attribute)); } catch (\InvalidArgumentException $e) { return Result::fail(Error::generic($e->getMessage())); } diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php index 3a66330..a38c5fe 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php @@ -8,6 +8,7 @@ use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceInput; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceOutput; use RPGPlayground\Domain\Actions\Dice\RollDiceAction; +use RPGPlayground\Domain\Enums\Roll\RollAttribute; final class RollDiceUseCase { @@ -19,11 +20,14 @@ final class RollDiceUseCase */ public static function handle(RollDiceInput $input): Result { - $total = 0; + $rolls = self::roll($input); - for ($i = 0; $i < $input->multiplier; $i++) { - $total += RollDiceAction::roll($input->dice); - } + $total = $input->attribute !== null + ? match ($input->attribute) { + RollAttribute::Advantage => max($rolls), + RollAttribute::Disadvantage => min($rolls), + } + : array_sum($rolls); foreach ($input->modifiers as $modifier) { $total = $modifier->apply($total); @@ -31,4 +35,24 @@ public static function handle(RollDiceInput $input): Result return Result::ok(new RollDiceOutput($total)); } + + /** + * @param RollDiceInput $input + * @return array + * @throws \Random\RandomException if the system entropy source fails. + */ + private static function roll(RollDiceInput $input): array + { + $rolls = []; + + for ($i = 0; $i < $input->multiplier; $i++) { + $rolls[] = RollDiceAction::roll($input->dice); + } + + if ($input->attribute !== null) { + $rolls[] = RollDiceAction::roll($input->dice); + } + + return $rolls; + } } diff --git a/src/Domain/Enums/Roll/RollAttribute.php b/src/Domain/Enums/Roll/RollAttribute.php new file mode 100644 index 0000000..2d49c19 --- /dev/null +++ b/src/Domain/Enums/Roll/RollAttribute.php @@ -0,0 +1,21 @@ +makeInput(new Dice(1)); $output = RollDiceUseCase::handle($input)->unwrap(); $this->assertSame(1, $output->rollValue); } + // ------------------------------------------------------------------------- + // Advantage + // ------------------------------------------------------------------------- + + public function test_advantage_returns_highest_of_two_d1_rolls(): void + { + // D1 always rolls 1 — max([1, 1]) = 1 + $input = $this->makeInput(new Dice(1), attribute: RollAttribute::Advantage); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertSame(1, $output->rollValue); + } + + public function test_advantage_result_is_within_dice_range(): void + { + $input = $this->makeInput(new Dice(20), attribute: RollAttribute::Advantage); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertGreaterThanOrEqual(1, $output->rollValue); + $this->assertLessThanOrEqual(20, $output->rollValue); + } + + public function test_advantage_with_modifier_applied_after(): void + { + // D1 advantage = max([1, 1]) = 1 → +4 = 5 + $input = $this->makeInput( + new Dice(1), + modifiers: [RollModifier::fromString('+4')], + attribute: RollAttribute::Advantage, + ); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertSame(5, $output->rollValue); + } + + // ------------------------------------------------------------------------- + // Disadvantage + // ------------------------------------------------------------------------- + + public function test_disadvantage_returns_lowest_of_two_d1_rolls(): void + { + // D1 always rolls 1 — min([1, 1]) = 1 + $input = $this->makeInput(new Dice(1), attribute: RollAttribute::Disadvantage); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertSame(1, $output->rollValue); + } + + public function test_disadvantage_result_is_within_dice_range(): void + { + $input = $this->makeInput(new Dice(20), attribute: RollAttribute::Disadvantage); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertGreaterThanOrEqual(1, $output->rollValue); + $this->assertLessThanOrEqual(20, $output->rollValue); + } + + public function test_disadvantage_with_modifier_applied_after(): void + { + // D1 disadvantage = min([1, 1]) = 1 → +4 = 5 + $input = $this->makeInput( + new Dice(1), + modifiers: [RollModifier::fromString('+4')], + attribute: RollAttribute::Disadvantage, + ); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertSame(5, $output->rollValue); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- /** - * @param Dice $dice + * @param Dice $dice * @param array $modifiers - * @param int $multiplier - * @return RollDiceInput + * @param int $multiplier + * @param RollAttribute|null $attribute */ - private function makeInput(Dice $dice, array $modifiers = [], int $multiplier = 1): RollDiceInput - { - return RollDiceInput::create($dice, $modifiers, $multiplier)->unwrap(); + private function makeInput( + Dice $dice, + array $modifiers = [], + int $multiplier = 1, + ?RollAttribute $attribute = null, + ): RollDiceInput { + return RollDiceInput::create($dice, $modifiers, $multiplier, $attribute)->unwrap(); } } diff --git a/tests/Domain/Enums/Roll/RollAttributeTest.php b/tests/Domain/Enums/Roll/RollAttributeTest.php new file mode 100644 index 0000000..990b6b7 --- /dev/null +++ b/tests/Domain/Enums/Roll/RollAttributeTest.php @@ -0,0 +1,23 @@ +assertTrue(RollAttribute::Advantage->isAdvantage()); + $this->assertFalse(RollAttribute::Advantage->isDisadvantage()); + } + + public function test_is_disadvantage(): void + { + $this->assertFalse(RollAttribute::Disadvantage->isAdvantage()); + $this->assertTrue(RollAttribute::Disadvantage->isDisadvantage()); + } +} diff --git a/tests/Domain/ValueObjects/RollModifierTest.php b/tests/Domain/ValueObjects/Roll/RollModifierTest.php similarity index 97% rename from tests/Domain/ValueObjects/RollModifierTest.php rename to tests/Domain/ValueObjects/Roll/RollModifierTest.php index effef68..d5fc32e 100644 --- a/tests/Domain/ValueObjects/RollModifierTest.php +++ b/tests/Domain/ValueObjects/Roll/RollModifierTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace RPGPlayground\Tests\Domain\ValueObjects; +namespace RPGPlayground\Tests\Domain\ValueObjects\Roll; use PHPUnit\Framework\TestCase; -use RPGPlayground\Domain\ValueObjects\RollModifier; +use RPGPlayground\Domain\ValueObjects\Roll\RollModifier; final class RollModifierTest extends TestCase { From e11f4e51519663f84ccc4c33a871beaaf2c5494e Mon Sep 17 00:00:00 2001 From: valb-mig Date: Thu, 19 Mar 2026 15:15:48 -0300 Subject: [PATCH 3/4] docs: Update gitignore and vscode settings --- .gitignore | 3 ++- .vscode/tasks.json | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a2f2663..4e64840 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ coverage .phpunit.result.cache obsidian -.obsidian \ No newline at end of file +.obsidian +index.php \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 73de7f0..250ade3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -11,7 +11,7 @@ "group": "tests" }, "runOptions": { - "runOn": "folderOpen" + "runOn": "default" } }, { @@ -24,7 +24,7 @@ "group": "analysis" }, "runOptions": { - "runOn": "folderOpen" + "runOn": "default" } } ] From dd5682072f17d0014704ac183ee6ae126929d57f Mon Sep 17 00:00:00 2001 From: valb-mig Date: Thu, 19 Mar 2026 15:20:25 -0300 Subject: [PATCH 4/4] chore: Remove issue from tesk --- .github/issues.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/issues.json b/.github/issues.json index 4fa1a25..32960f8 100644 --- a/.github/issues.json +++ b/.github/issues.json @@ -1,7 +1,2 @@ [ - { - "title": "(feat): Advantage dices", - "body": "Implement advantage and disadvantage mechanics for dice rolls.\n\nAdvantage: roll two dice of the same type and keep the highest result.\nDisadvantage: roll two dice of the same type and keep the lowest result.\n\nThis is a common mechanic in tabletop RPG systems such as D&D 5e and should integrate naturally with the existing `RollDiceInput` and `RollDiceUseCase` pipeline.", - "labels": ["enhancement"] - } ] \ No newline at end of file