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
5 changes: 0 additions & 5 deletions .github/issues.json
Original file line number Diff line number Diff line change
@@ -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"]
}
]
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
coverage
.phpunit.result.cache
obsidian
.obsidian
.obsidian
index.php
4 changes: 2 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"group": "tests"
},
"runOptions": {
"runOn": "folderOpen"
"runOn": "default"
}
},
{
Expand All @@ -24,7 +24,7 @@
"group": "analysis"
},
"runOptions": {
"runOn": "folderOpen"
"runOn": "default"
}
}
]
Expand Down
19 changes: 13 additions & 6 deletions src/Application/UseCase/Dice/RollDice/RollDiceInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,44 @@

use Eco\Error;
use Eco\Result;
use RPGPlayground\Domain\Enums\Roll\RollAttribute;
use RPGPlayground\Domain\ValueObjects\Dice;
use RPGPlayground\Domain\ValueObjects\DiceModifier;
use RPGPlayground\Domain\ValueObjects\Roll\RollModifier;

final class RollDiceInput
{
/**
* @param Dice $dice The dice to roll
* @param array<DiceModifier> $modifiers The modifiers to apply to the roll
* @param array<RollModifier> $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,
) {}

/**
* @param Dice $dice The dice to roll
* @param array<DiceModifier> $modifiers The modifiers to apply to the roll
* @param array<RollModifier> $modifiers The modifiers to apply to the roll
* @param int $multiplier The number of times to roll the dice
* @return Result<self>
* */
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()));
}
Expand Down
35 changes: 29 additions & 6 deletions src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,55 @@

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;
use RPGPlayground\Domain\Actions\Dice\RollDiceAction;
use RPGPlayground\Domain\Enums\Roll\RollAttribute;

final class RollDiceUseCase
{
/**
* @param RollDiceInput $input
* @return Result<RollDiceOutput>
* @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
{
$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);
}

return Result::ok(new RollDiceOutput($total));
}

/**
* @param RollDiceInput $input
* @return array<int>
* @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;
}
}
9 changes: 0 additions & 9 deletions src/Core/Exceptions/DomainException.php

This file was deleted.

21 changes: 21 additions & 0 deletions src/Domain/Enums/Roll/RollAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace RPGPlayground\Domain\Enums\Roll;

enum RollAttribute: string
{
case Advantage = 'advantage';
case Disadvantage = 'disadvantage';

public function isAdvantage(): bool
{
return $this === self::Advantage;
}

public function isDisadvantage(): bool
{
return $this === self::Disadvantage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

declare(strict_types=1);

namespace RPGPlayground\Domain\ValueObjects;
namespace RPGPlayground\Domain\ValueObjects\Roll;

final class DiceModifier
final class RollModifier
{
public const array VALID_SYMBOLS = ['+', '-', '*', 'x', '/'];
private const string MODIFIER_PATTERN = '/^([+\-\/x\*])(\d+)$/u';
Expand Down
6 changes: 3 additions & 3 deletions tests/Application/UseCase/Dice/RollDice/RollDiceInputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use PHPUnit\Framework\TestCase;
use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceInput;
use RPGPlayground\Domain\ValueObjects\Dice;
use RPGPlayground\Domain\ValueObjects\DiceModifier;
use RPGPlayground\Domain\ValueObjects\Roll\RollModifier;

final class RollDiceInputTest extends TestCase
{
Expand Down Expand Up @@ -47,8 +47,8 @@ public function test_multiplier_defaults_to_one(): void
public function test_modifiers_are_stored_correctly(): void
{
$modifiers = [
DiceModifier::fromString('+5'),
DiceModifier::fromString('-2'),
RollModifier::fromString('+5'),
RollModifier::fromString('-2'),
];

$input = RollDiceInput::create(new Dice(20), $modifiers)->unwrap();
Expand Down
104 changes: 89 additions & 15 deletions tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceInput;
use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceOutput;
use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceUseCase;
use RPGPlayground\Domain\Enums\Roll\RollAttribute;
use RPGPlayground\Domain\ValueObjects\Dice;
use RPGPlayground\Domain\ValueObjects\DiceModifier;
use RPGPlayground\Domain\ValueObjects\Roll\RollModifier;

final class RollDiceUseCaseTest extends TestCase
{
Expand Down Expand Up @@ -75,7 +76,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);
Expand All @@ -84,7 +85,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);
Expand All @@ -93,7 +94,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);
Expand All @@ -102,7 +103,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);
Expand All @@ -112,8 +113,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();
Expand All @@ -123,25 +124,98 @@ public function test_multiple_modifiers_are_applied_in_order(): void

public function test_no_modifiers_returns_raw_roll(): void
{
// D1 always 1, no modifiers
$input = $this->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 array<DiceModifier> $modifiers
* @param int $multiplier
* @return RollDiceInput
* @param Dice $dice
* @param array<RollModifier> $modifiers
* @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();
}
}
Loading
Loading