Update cli
parent
f192f1f1f3
commit
18224b1ea7
|
@ -26,10 +26,9 @@
|
||||||
pre-commit.settings.hooks = {
|
pre-commit.settings.hooks = {
|
||||||
autoflake.enable = true;
|
autoflake.enable = true;
|
||||||
black.enable = true;
|
black.enable = true;
|
||||||
mypy.enable = true;
|
# mypy.enable = true;
|
||||||
ruff.enable = true;
|
ruff.enable = true;
|
||||||
};
|
};
|
||||||
pre-commit.settings.settings.mypy.binPath = "${pkgs.python311Packages.mypy}/bin/mypy";
|
|
||||||
packages.default = package;
|
packages.default = package;
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|
|
@ -58,7 +58,7 @@ python = ["3.11"]
|
||||||
[tool.hatch.envs.typing]
|
[tool.hatch.envs.typing]
|
||||||
extra-dependencies = ["mypy"]
|
extra-dependencies = ["mypy"]
|
||||||
[tool.hatch.envs.typing.scripts]
|
[tool.hatch.envs.typing.scripts]
|
||||||
check = "mypy --install-types {args:roll tests}"
|
check = "mypy --install-types --non-interactive {args:roll tests}"
|
||||||
|
|
||||||
[tool.hatch.envs.lint]
|
[tool.hatch.envs.lint]
|
||||||
detached = true
|
detached = true
|
||||||
|
|
|
@ -1,50 +1,92 @@
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from roll.__about__ import __version__
|
from roll.__about__ import __version__
|
||||||
from roll.cli.roll_param import ROLL
|
from roll.cli.roll_param import ROLL
|
||||||
from roll.roll import Roll
|
from roll.roll import Roll
|
||||||
|
from roll.throw import Throw
|
||||||
|
|
||||||
|
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
|
||||||
context_settings={"help_option_names": ["-h", "--help"]},
|
|
||||||
invoke_without_command=True,
|
|
||||||
)
|
|
||||||
@click.version_option(version=__version__, prog_name="roll")
|
@click.version_option(version=__version__, prog_name="roll")
|
||||||
@click.argument("rolls", nargs=-1, type=ROLL)
|
@click.argument("roll", type=ROLL, default="1d20")
|
||||||
def roll(rolls: list[Roll]):
|
def cli(roll: Roll | Literal["advantage"] | Literal["disadvantage"]) -> None:
|
||||||
"""Throw each roll specified in ROLLS and print the results.
|
"""throw a ROLL and print the results
|
||||||
|
|
||||||
Rolls are specified as
|
ROLL is specified as
|
||||||
|
|
||||||
DdS[(+|-)M]
|
DdS[(+|-)M]
|
||||||
|
|
||||||
where D = # of dice, S = sides per die, and M = optional modifier.
|
where D = # of dice, S = sides per die, and M = optional modifier
|
||||||
|
|
||||||
Example usage:
|
if no ROLL is provided, a 1d20 is thrown
|
||||||
|
|
||||||
|
instead of a ROLL specifier, you can use a substring of "advantage"
|
||||||
|
or "disadvantage" to throw 2d20 and take the appropriate result
|
||||||
|
|
||||||
|
example usage:
|
||||||
|
|
||||||
\b
|
\b
|
||||||
$ roll 2d20+3 3d6-1
|
$ roll 2d20+3
|
||||||
rolling 2d20+3:
|
rolling 2d20+3:
|
||||||
1: | 2
|
1: | 2
|
||||||
2: | 13
|
2: | 13
|
||||||
mod: +3
|
mod: +3
|
||||||
total: 18
|
total: 18
|
||||||
\b
|
\b
|
||||||
rolling 3d6-1:
|
$ roll
|
||||||
1: | 3
|
rolling 1d20:
|
||||||
2: | 3
|
1: | 11
|
||||||
3: | 1
|
total: 11
|
||||||
mod: -1
|
\b
|
||||||
total: 6
|
$ roll advantage
|
||||||
|
rolling 2d20 with advantage:
|
||||||
|
1: | 13
|
||||||
|
1: | 2
|
||||||
|
total: 13
|
||||||
|
\b
|
||||||
|
$ roll dis
|
||||||
|
rolling 2d20 with disadvantage:
|
||||||
|
1: | 1
|
||||||
|
2: | 19
|
||||||
|
total: 1
|
||||||
|
critical miss!
|
||||||
"""
|
"""
|
||||||
for roll in rolls:
|
if isinstance(roll, str):
|
||||||
click.echo()
|
throw = _advantage(roll)
|
||||||
click.echo(f"rolling {roll.to_str()}:")
|
else:
|
||||||
throw = roll.throw()
|
throw = _throw(roll)
|
||||||
for i, result in enumerate(throw.results):
|
click.echo(f"total:\t{throw.total: >5}")
|
||||||
click.echo(f"{i + 1}:\t| {result: >3}")
|
if throw.is_critical_hit: # pragma: no cover (random)
|
||||||
if roll.modifier:
|
click.echo("critical hit!")
|
||||||
mod = roll.modifier_str()
|
elif throw.is_critical_miss: # pragma: no cover (random)
|
||||||
click.echo(f"mod:\t {mod: >4}")
|
click.echo("critical miss!")
|
||||||
click.echo(f"total:\t {throw.total: >3}")
|
|
||||||
|
|
||||||
|
def _advantage(advantage: str) -> Throw:
|
||||||
|
"""print the results of an advantage or disadvantage roll"""
|
||||||
|
click.echo(f"throwing 2d20 with {advantage}:")
|
||||||
|
roll = Roll()
|
||||||
|
fst = roll.throw()
|
||||||
|
snd = roll.throw()
|
||||||
|
click.echo(f"1:\t|{fst.total: >4}")
|
||||||
|
click.echo(f"2:\t|{snd.total: >4}")
|
||||||
|
if advantage == "advantage":
|
||||||
|
throw = fst if fst.total > snd.total else snd
|
||||||
|
else:
|
||||||
|
throw = fst if fst.total < snd.total else snd
|
||||||
|
return throw
|
||||||
|
|
||||||
|
|
||||||
|
def _throw(roll: Roll) -> Throw:
|
||||||
|
"""print the results of a roll"""
|
||||||
|
click.echo(f"throwing {roll.to_str()}:")
|
||||||
|
throw = roll.throw()
|
||||||
|
for i, result in enumerate(throw.results, start=1):
|
||||||
|
click.echo(f"{i}:\t|{result: >4}")
|
||||||
|
if roll.modifier is not None:
|
||||||
|
click.echo(f"mod:\t{roll.modifier_str(): >5}")
|
||||||
|
return throw
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from roll.roll import Roll
|
from roll.roll import Roll
|
||||||
|
@ -6,14 +8,20 @@ from roll.roll import Roll
|
||||||
class RollParam(click.ParamType):
|
class RollParam(click.ParamType):
|
||||||
name = "roll"
|
name = "roll"
|
||||||
|
|
||||||
def convert(self, value: str | Roll, param: click.Parameter | None, ctx: click.Context | None) -> Roll:
|
def convert(
|
||||||
"""Parse a Roll from a command line string."""
|
self, value: str | Roll, param: click.Parameter | None, ctx: click.Context | None
|
||||||
|
) -> Roll | Literal["advantage"] | Literal["disadvantage"]:
|
||||||
|
"""parse a Roll from a command line string"""
|
||||||
if isinstance(value, Roll):
|
if isinstance(value, Roll):
|
||||||
return value
|
return value
|
||||||
|
elif "advantage".startswith(value):
|
||||||
|
return "advantage"
|
||||||
|
elif "disadvantage".startswith(value):
|
||||||
|
return "disadvantage"
|
||||||
try:
|
try:
|
||||||
return Roll.from_str(value)
|
return Roll.from_str(value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.fail(f"invalid roll: {value!r}, caused by {e}", param, ctx)
|
self.fail(f"{e}", param, ctx)
|
||||||
|
|
||||||
|
|
||||||
ROLL = RollParam()
|
ROLL = RollParam()
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from roll.cli import cli
|
||||||
|
from roll.roll import Roll
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runner() -> CliRunner:
|
||||||
|
return CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_smoke(runner: CliRunner):
|
||||||
|
result = runner.invoke(cli, [])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.startswith("throwing 1d20:\n1:\t|")
|
||||||
|
assert "total" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["2d20", "4d6-2", "9d100+3"])
|
||||||
|
def test_cli_roll(runner: CliRunner, value: str):
|
||||||
|
result = runner.invoke(cli, [value])
|
||||||
|
roll = Roll.from_str(value)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.startswith(f"throwing {value}:\n1:\t|")
|
||||||
|
assert len(result.output.splitlines()) >= roll.dice_count + 2
|
||||||
|
assert "total" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("advantage", ["advantage", "disadvantage"])
|
||||||
|
def test_cli_advantage(runner: CliRunner, advantage: str):
|
||||||
|
result = runner.invoke(cli, [advantage])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.startswith(f"throwing 2d20 with {advantage}:\n1:\t|")
|
||||||
|
assert "total" in result.output
|
|
@ -0,0 +1,38 @@
|
||||||
|
import click
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from roll.cli.roll_param import RollParam
|
||||||
|
from roll.roll import Roll
|
||||||
|
|
||||||
|
|
||||||
|
def test_roll_param_passthrough():
|
||||||
|
roll = Roll()
|
||||||
|
result = RollParam().convert(roll, None, None)
|
||||||
|
assert result == roll
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["1d20", "2d20+3", "4d6-3"])
|
||||||
|
def test_roll_param_valid(value):
|
||||||
|
result = RollParam().convert(value, None, None)
|
||||||
|
assert isinstance(result, Roll)
|
||||||
|
assert result.to_str() == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["advantage", "disadvantage"])
|
||||||
|
def test_roll_param_advantage(value):
|
||||||
|
result = RollParam().convert(value, None, None)
|
||||||
|
assert result == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["d90", "0d", "d-1", "aba", "000", "1d1d1d"])
|
||||||
|
def test_roll_param_invalid(value):
|
||||||
|
with pytest.raises(click.exceptions.BadParameter) as e:
|
||||||
|
RollParam().convert(value, None, None)
|
||||||
|
assert value in e.value.message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["0d0", "0d20", "1d0", "1d1"])
|
||||||
|
def test_roll_param_invalid_properties(value):
|
||||||
|
with pytest.raises(click.exceptions.BadParameter) as e:
|
||||||
|
RollParam().convert(value, None, None)
|
||||||
|
assert "sides" in e.value.message or "dice" in e.value.message
|
Loading…
Reference in New Issue